ShapeBuilder Done

This commit is contained in:
Savya Bikram Shah
2026-05-28 13:20:36 +05:45
parent 3c2e486529
commit b38f4d592c
33 changed files with 450 additions and 25 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3841157d2ef124a25b2c16956605ca40
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
{
"name": "Features.AppBoot"
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a00b1d9e55c264fb785b78914db69e05
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8e69d4466105646aabc1668844cac5b7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
{
"name": "Features.GameplayFlow",
"rootNamespace": "Darkmatter.Features.GameplayFlow",
"references": [
"GUID:6a0a834eb41764f12ba55c3fb04a40cb",
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 995166e584dda4ff98501f62b07aa9cb
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: adb650c457f6843c2a5704f12bcd0160
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 46180d2f7b7a14e42b77afb776b3a129
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,20 @@
using Darkmatter.Core.Contracts.Features.GameplayFlow;
using Darkmatter.Features.GameplayFlow.SceneRefs;
using Darkmatter.Libs.Installers;
using UnityEngine;
using VContainer;
using VContainer.Unity;
namespace Darkmatter.Features.GameplayFlow
{
public class GameplayFlowFeatureModule : MonoBehaviour, IModule
{
[SerializeField] private GameplaySceneRefs sceneRefs;
public void Register(IContainerBuilder builder)
{
if (sceneRefs != null)
builder.RegisterComponent<IGameplaySceneRefs>(sceneRefs);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c784b80c101814d24bf8820233bb0f62

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 38e2583efd5f84627b5ff0fc7599bf03
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,24 @@
using Darkmatter.Core.Contracts.Features.GameplayFlow;
using UnityEngine;
namespace Darkmatter.Features.GameplayFlow.SceneRefs
{
public class GameplaySceneRefs : MonoBehaviour, IGameplaySceneRefs
{
[SerializeField] private RectTransform paperRoot;
[SerializeField] private RectTransform slotsParent;
[SerializeField] private RectTransform piecesParent;
[SerializeField] private RectTransform regionsParent;
[SerializeField] private RectTransform hudRoot;
[SerializeField] private RectTransform trayPanel;
public RectTransform PaperRoot => paperRoot;
public RectTransform SlotsParent => slotsParent;
public RectTransform PiecesParent => piecesParent;
public RectTransform RegionsParent => regionsParent;
public RectTransform HudRoot => hudRoot;
public RectTransform TrayPanel => trayPanel;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f2e39895945b94467b1677388b2fb896

View File

@@ -4,11 +4,6 @@ using UnityEngine;
namespace Darkmatter.Features.ShapeBuilder.Commands
{
/// <summary>
/// Undoable snap. Captures the piece's pre-snap pose + parent at ctor time;
/// Execute drives the normal snap tween; Undo restores the pre-snap state
/// and re-enables drag.
/// </summary>
internal sealed class SnapPieceCommand : ICommand
{
private readonly ShapePiece _piece;

View File

@@ -1,7 +1,10 @@
using Darkmatter.Core.Contracts.Features.ShapeBuilder;
using Darkmatter.Core.Data.Static.Features.ShapeBuilder;
using Darkmatter.Features.ShapeBuilder.Systems;
using Darkmatter.Libs.Installers;
using UnityEngine;
using VContainer;
using VContainer.Unity;
namespace Darkmatter.Features.ShapeBuilder.Installers
{
@@ -12,10 +15,8 @@ namespace Darkmatter.Features.ShapeBuilder.Installers
public void Register(IContainerBuilder builder)
{
if (config != null)
builder.RegisterInstance(config);
// ShapePiece instances are MonoBehaviours instantiated by the
// ShapeBuilderController; nothing to register here for them.
builder.RegisterComponent(config);
builder.Register<IShapeBuilderController, ShapeBuilderController>(Lifetime.Singleton);
}
}
}
}

View File

@@ -0,0 +1,169 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Cysharp.Threading.Tasks;
using Darkmatter.Core.Contracts.Features.DrawingCatalog;
using Darkmatter.Core.Contracts.Features.History;
using Darkmatter.Core.Contracts.Features.ShapeBuilder;
using Darkmatter.Core.Contracts.Services.Assets;
using Darkmatter.Core.Contracts.Services.Audio;
using Darkmatter.Core.Contracts.Features.GameplayFlow;
using Darkmatter.Core.Data.Signals.Features.ShapeBuilder;
using Darkmatter.Core.Data.Static.Features.ShapeBuilder;
using Darkmatter.Features.ShapeBuilder.UI;
using Darkmatter.Libs.Observer;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Darkmatter.Features.ShapeBuilder.Systems
{
public class ShapeBuilderController : IShapeBuilderController, IDisposable
{
private const string PiecePrefabKey = "ShapeBuilder/Piece";
private string _currentTemplateId;
private int _expected;
private int _snapped;
private GameObject _drawingInstance;
private GameObject _piecePrefab;
private IDisposable _snappedSub;
private readonly List<string> _snappedPieceIds = new();
private readonly ShapeBuilderConfig _cfg;
private readonly ISfxPlayer _sfx;
private readonly IEventBus _bus;
private readonly IUndoStack _undo;
private readonly IAssetProviderService _assetProviderService;
private readonly IEventBus _eventBus;
private readonly IGameplaySceneRefs _refs;
public ShapeBuilderController(
ShapeBuilderConfig cfg,
ISfxPlayer sfx,
IEventBus bus,
IUndoStack undo,
IAssetProviderService assetProviderService,
IEventBus eventBus,
IGameplaySceneRefs refs)
{
_cfg = cfg;
_sfx = sfx;
_bus = bus;
_undo = undo;
_assetProviderService = assetProviderService;
_eventBus = eventBus;
_refs = refs;
}
public async UniTask InitializeAsync(CancellationToken cancellationToken)
{
await TryLoadPiecePrefabAsync(cancellationToken);
_snappedSub = _eventBus.Subscribe<PieceSnappedSignal>(OnPieceSnapped);
}
private void OnPieceSnapped(PieceSnappedSignal obj)
{
_snappedPieceIds.Add(obj.PieceId);
CheckIfShapeAssembled();
}
public async UniTask BuildAsync(
IDrawingTemplate template,
IReadOnlyCollection<string> preSnappedIds,
CancellationToken ct = default)
{
Clear();
if (template.Prefab == null)
throw new System.Exception($"No drawing layout prefab for '{template.Id}'");
_drawingInstance = Object.Instantiate(template.Prefab, _refs.PaperRoot);
ct.ThrowIfCancellationRequested();
var slots = _drawingInstance.GetComponentsInChildren<SlotMarker>(includeInactive: true);
await TryLoadPiecePrefabAsync(ct);
int count = template.Pieces.Count;
float trayW = _refs.TrayPanel.rect.width;
float pitch = trayW / (count + 1);
_currentTemplateId = template.Id;
_expected = count;
_snapped = 0;
CreateShapePieceInstances(template, preSnappedIds, count, slots, pitch, trayW);
CheckIfShapeAssembled();
}
private void CreateShapePieceInstances(IDrawingTemplate template, IReadOnlyCollection<string> preSnappedIds,
int count,
SlotMarker[] slots, float pitch, float trayW)
{
for (int i = 0; i < count; i++)
{
var shape = template.Pieces[i];
var slot = FindSlotForShape(slots, shape);
if (slot == null)
{
Debug.LogError($"[ShapeBuilder] No SlotMarker for '{shape.Id}' in '{template.Id}'");
continue;
}
var trayPos = new Vector2(pitch * (i + 1) - trayW * 0.5f, 0f);
bool preSnapped = preSnappedIds != null && preSnappedIds.Contains(shape.Id);
var go = Object.Instantiate(_piecePrefab, _refs.TrayPanel);
go.name = $"Piece_{shape.Id}";
var piece = go.GetComponent<ShapePiece>();
piece.Setup(shape, slot, _cfg, _sfx, _bus, _undo, trayPos, preSnapped);
if (preSnapped) _snapped++;
}
}
private void CheckIfShapeAssembled()
{
if (_expected > 0 && _snapped == _expected)
_bus.Publish(new ShapeAssembledSignal(_currentTemplateId));
}
private async UniTask TryLoadPiecePrefabAsync(CancellationToken ct)
{
if (_piecePrefab == null)
{
_piecePrefab = await _assetProviderService.LoadAssetAsync<GameObject>(
PiecePrefabKey, progress: null, cancellationToken: ct);
if (_piecePrefab == null)
throw new System.Exception($"No piece prefab at '{PiecePrefabKey}'");
}
}
private static SlotMarker FindSlotForShape(SlotMarker[] slots, ShapeSO shape)
{
foreach (var s in slots)
if (s.Shape == shape)
return s;
return null;
}
public IReadOnlyCollection<string> GetSnappedPieceIds()
{
return _snappedPieceIds;
}
public void Clear()
{
}
public void Dispose()
{
_snappedSub?.Dispose();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2964329afc8ef47fe8817d4a701b8fe1

View File

@@ -12,12 +12,6 @@ using UnityEngine.UI;
namespace Darkmatter.Features.ShapeBuilder.UI
{
/// <summary>
/// Single-MB shape piece. Handles drag, reactive preview lerp, snap,
/// and return-to-tray. Snap is wrapped in a <see cref="SnapPieceCommand"/>
/// pushed onto the shared <see cref="IUndoStack"/> so it participates in
/// the same undo/redo flow as coloring.
/// </summary>
[RequireComponent(typeof(RectTransform))]
public sealed class ShapePiece : MonoBehaviour,
IBeginDragHandler, IDragHandler, IEndDragHandler
@@ -116,7 +110,7 @@ namespace Darkmatter.Features.ShapeBuilder.UI
_slot.RectTransform.anchoredPosition);
if (dist <= _cfg.SnapRadius)
_undo.Push(new SnapPieceCommand(this)); // Push calls Execute → SnapInternal
_undo.Push(new SnapPieceCommand(this));
else
ReturnToTray();
}
@@ -132,7 +126,6 @@ namespace Darkmatter.Features.ShapeBuilder.UI
RectTransform.localRotation = Quaternion.Slerp(Quaternion.identity, slot.localRotation, t);
}
// ---- Command-driven snap / un-snap ----
internal void SnapInternal()
{
Lock();