diff --git a/Readme.md b/Readme.md index 0f453f9..21437ce 100644 --- a/Readme.md +++ b/Readme.md @@ -184,7 +184,8 @@ Rough landing order for ColorBook scene to be playable: | `Core/Contracts/Features/Progression/IProgressionService.cs` | Progression contract (despite the name, it's a feature contract since it's game-specific) | | `Core/Data/Static/Features/Drawing/` (DrawingTemplateSO) | Authored drawing data | | `Core/Data/Static/Features/Coloring/` (ColorPaletteSO) | Authored palette data | -| `Core/Data/Dynamic/Features/Drawing/ShapePieceDTO.cs`, `ColorRegionDTO.cs` | Runtime drawing structs | +| `Core/Data/Dynamic/Features/Drawing/ColorRegionDTO.cs` | Runtime drawing struct (regions only — pieces use `ShapeSO`) | +| `Core/Data/Static/Features/Drawing/ShapeSO.cs` | Authored shape ScriptableObject (sprite + snap params) | | `Core/Data/Dynamic/Features/Coloring/PaintCommandDTO.cs` | Runtime coloring struct | | `Core/Data/Dynamic/Features/Gallery/SavedArtworkDTO.cs` | Runtime gallery struct | | `Core/Data/Dynamic/Features/Signals/` (DrawingSelectedSignal, ShapeAssembledSignal, ColorAppliedSignal, ArtworkCapturedSignal, ArtworkSavedSignal) | Cross-feature signal structs | @@ -197,7 +198,9 @@ Rough landing order for ColorBook scene to be playable: | `App/LifetimeScopes/{MainMenu,ColorBook,ArtBook}LifetimeScope.cs` | Per-scene scopes | | `App/Boot/AppBoot.cs` | Bootstrap entry point | | `Assets/Darkmatter/Scenes/{MainMenu,ColorBook,ArtBook}.unity` | Scenes | -| `Content/Gameplay/Drawings///{Template.asset, Pieces/, Regions/, PaperBackground.png}` | Authored drawings (under existing `Content/Gameplay/` root) | +| `Content/Gameplay/Drawings///{Template.asset, Drawing.prefab, Regions/, PaperBackground.png}` | Authored drawings — `Drawing.prefab` holds `SlotMarker`s at slot poses with `ShapeSO` refs | +| `Content/Gameplay/Shapes/*.asset` | Reusable `ShapeSO`s (one per shape; shared across drawings) | +| `Content/Gameplay/Prefabs/ShapePiece.prefab` | The single piece prefab (`ShapePieceUI` MB on root) | | `Content/Gameplay/Palettes/*.asset` | Color palettes | | `Content/Audio/{UI,Coloring}/` | SFX banks | @@ -383,20 +386,10 @@ public interface IDrawingTemplate { string DisplayName { get; } Sprite DefaultThumbnail { get; } // authored fallback (used when user has no captures for this template) Sprite PaperBackground { get; } - IReadOnlyList Pieces { get; } + IReadOnlyList Pieces { get; } // shapes that get spawned in the tray for this drawing IReadOnlyList Regions { get; } } -public readonly struct ShapePieceDTO { - public string PieceId { get; } - public Sprite Sprite { get; } // assigned to Image.sprite - public Vector2 SlotAnchoredPosition { get; } // canvas units, relative to SlotsParent - public Vector2 SlotSizeDelta { get; } // canvas units — target size when snapped - public float SlotRotationZ { get; } // degrees, local rotation when snapped - public float SnapRadius { get; } // canvas units; ~80–120 for toddlers - public float PreviewRadius { get; } // canvas units; ~2× snap radius -} - public readonly struct ColorRegionDTO { public string RegionId { get; } public Sprite Sprite { get; } // assigned to Image.sprite @@ -408,6 +401,33 @@ public readonly struct ColorRegionDTO { } ``` +### Shape authoring (`ShapeSO` + one prefab) + +Shapes are authored as ScriptableObject assets via the Project Create menu (`Assets > Create > Darkmatter > Drawing > Shape`). One asset per shape — reusable across many drawings. + +```csharp +namespace Darkmatter.Core.Data.Static.Features.Drawing; + +[CreateAssetMenu(menuName = "Darkmatter/Drawing/Shape", fileName = "Shape_")] +public sealed class ShapeSO : ScriptableObject +{ + [field: SerializeField] public string Id { get; private set; } + [field: SerializeField] public Sprite Sprite { get; private set; } + [field: SerializeField] public Vector2 DefaultSizeDelta { get; private set; } = new(256, 256); + [field: SerializeField] public float SnapRadius { get; private set; } = 100f; + [field: SerializeField] public float PreviewRadius { get; private set; } = 200f; +} +``` + +How the runtime uses it: + +1. **One piece prefab.** A `ShapePiecePrefab` in `Content/Gameplay/Prefabs/` carries `Image` + `ShapePieceUI`. The `ShapePieceUI` MonoBehaviour has a `[SerializeField] ShapeSO _shape` field — empty on the raw prefab, filled in by the controller at spawn time (or pre-assigned in inspector for testing scenes). +2. **`SlotMarker`** lives in the drawing's per-drawing scene/prefab at the slot's authored pose. Its `[SerializeField] ShapeSO _shape` field tells which shape fits this slot. The slot's `RectTransform` (`anchoredPosition`, `sizeDelta`, `localRotation`) *is* the target snap pose. +3. **Matching is by `ShapeSO` reference equality.** Piece P matches slot S iff `P._shape == S._shape`. No string-id lookups at runtime. +4. **Identity follows the asset.** Whenever `_shape` changes (inspector edit or runtime assign), `ShapePieceUI` re-applies `_shape.Sprite` to its `Image`, sets `RectTransform.sizeDelta = _shape.DefaultSizeDelta`, and exposes `PieceId => _shape.Id`. Done via `OnValidate` (editor) + `Awake` (runtime) + an explicit `Assign(ShapeSO)` method (controller-driven). + +> Optional future editor tool: a wizard window for bulk-creating `ShapeSO`s from a folder of sprites — sets `Id` from filename, assigns sprite, applies sensible default radii. For v1, the plain Create-Asset-Menu is enough. + ### Coloring > Contracts in `Darkmatter.Core.Contracts.Features.Coloring`; DTOs in `Darkmatter.Core.Data.Dynamic.Features.Coloring`. @@ -1268,7 +1288,8 @@ Toddler-mode error UI: | Class | Layer | Asmdef | |---|---|---| -| `IDrawingTemplate`, `ShapePieceDTO`, `ColorRegionDTO` | Core | `Core` | +| `IDrawingTemplate`, `ColorRegionDTO` | Core | `Core` | +| `ShapeSO` (ScriptableObject) | Core | `Core` | | `IPaperSurface` | Core | `Core` | | `ICommand`, `IUndoStack` | Core | `Core` | | `BoundedUndoStack` | Libs | `Libs.CommandStack` | @@ -1314,11 +1335,11 @@ public interface IDrawingTemplate { string DisplayName { get; } // user-facing Sprite DefaultThumbnail { get; } // 256×256 authored fallback for the catalog grid Sprite PaperBackground { get; } // image under all paper content - IReadOnlyList Pieces { get; } // for ShapeBuilder + IReadOnlyList Pieces { get; } // shapes spawned in tray (reusable across drawings) IReadOnlyList Regions { get; } // for Coloring } ``` -Implemented by `DrawingTemplateSO` (ScriptableObject) loaded via Addressables. +Implemented by `DrawingTemplateSO` (ScriptableObject) loaded via Addressables. The per-drawing slot positions live in the drawing's authored scene/prefab as `SlotMarker` MonoBehaviours, not in the template SO. > The catalog grid shows the latest user-captured thumbnail (via `IGalleryService.GetLatestThumbnailAsync`) when available, falling back to `DefaultThumbnail` when the user hasn't completed this template yet. The template itself stays immutable. @@ -1586,24 +1607,44 @@ public sealed class ShapeBuilderController : IDisposable { // pub: ShapeAssembledSignal ``` - **Internal:** counts `PieceSnappedSignal` against expected piece count. +- **Slot discovery:** after a drawing's per-drawing prefab is instantiated under `IPaperSurface.Root`, the controller queries `GetComponentsInChildren()` to discover all slots in the loaded drawing. Each slot's `_shape` field tells which `ShapeSO` it expects; matching pieces are spawned in the tray. #### `ShapePieceUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler` *(UI)* -The UI Image that the toddler drags. Lives under `TrayPanel` while idle, reparents under `IPaperSurface.PiecesParent` when snapped. +The UI Image that the toddler drags. One prefab; the assigned `ShapeSO` determines visual identity and snap params. + ```csharp public sealed class ShapePieceUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler { - public string PieceId { get; } - public bool IsLocked { get; } - public RectTransform RectTransform { get; } - public event Action Snapped; + [SerializeField] private ShapeSO _shape; // set by controller at spawn (or in inspector for testing) + [SerializeField] private Image _image; - public void Initialize(ShapePieceDTO dto, ShapePieceFsm fsm); + public string PieceId => _shape != null ? _shape.Id : null; + public ShapeSO Shape => _shape; + public RectTransform RectTransform => (RectTransform)transform; + public bool IsLocked { get; private set; } + public event Action Snapped; + + // Controller calls this at spawn time + public void Assign(ShapeSO shape) { + _shape = shape; + ApplyShape(); + } + + private void OnValidate() => ApplyShape(); // editor inspector edits + private void Awake() => ApplyShape(); // runtime safety + + private void ApplyShape() { + if (_shape == null || _image == null) return; + _image.sprite = _shape.Sprite; + RectTransform.sizeDelta = _shape.DefaultSizeDelta; + } } ``` - Handlers forward to `ShapePieceFsm` (`OnDragBegin / OnDrag(localPos) / OnDragEnd`). - `OnDrag` converts `PointerEventData.position` to canvas-local via `RectTransformUtility.ScreenPointToLocalPointInRectangle` against the piece's parent rect. - No collider, no Physics2D anywhere. +- **Identity follows the SO** — change `_shape` in inspector and the visual + ID update on the next `OnValidate`. At runtime, `Assign(...)` is the only mutation path. #### `ShapePieceFsm` *(Systems)* Per-piece state machine using `Libs.FSM`. States: `InTray → Dragging → Preview → (Snapped | Returning)`. @@ -1622,25 +1663,34 @@ public sealed class ShapePieceFsm { - **Returning enter**: DOTween back to tray slot (`anchoredPosition` from `TrayLayout`). #### `SlotMarker : MonoBehaviour` *(UI)* -The outline `Image` on `IPaperSurface.SlotsParent` showing where a piece should snap. Its `RectTransform` directly *is* the target pose — `ShapePieceFsm` reads `anchoredPosition`, `sizeDelta`, `localRotation` from it. +The outline `Image` on `IPaperSurface.SlotsParent` showing where a piece should snap. Authored per drawing — designer places one in the scene at each slot location, with its `RectTransform` set to the target pose and `_shape` field assigned to the matching `ShapeSO`. ```csharp public sealed class SlotMarker : MonoBehaviour { - public string SlotId; - public RectTransform RectTransform => transform as RectTransform; + [SerializeField] private ShapeSO _shape; // which shape fits here + [SerializeField] private Image _outline; // optional faint outline UI + + public ShapeSO Shape => _shape; + public string SlotId => _shape != null ? _shape.Id : null; + public RectTransform RectTransform => (RectTransform)transform; } ``` +- **Pose lives on this MB's `RectTransform`** — `anchoredPosition`, `sizeDelta`, `localRotation` directly. No pose data on the SO. +- **Matching:** `ShapePieceFsm` compares `_piece.Shape == _slot.Shape` (Unity Object reference equality). No string lookups. #### `TrayPanel : MonoBehaviour` *(UI)* HUD-side panel (on `HUDCanvas`) where pieces start out. Has a `HorizontalLayoutGroup` + `ContentSizeFitter`. Provides spawn anchors via `RectTransform Slot(int index)` for the controller. #### `ShapePieceFactory` *(Systems)* -Pool of `ShapePieceUI` GameObjects + their associated FSMs. Reused across template loads. +Pool of `ShapePieceUI` GameObjects (one prefab) + their associated FSMs. Reused across template loads. ```csharp public sealed class ShapePieceFactory { - public ShapePieceUI Spawn(ShapePieceDTO dto, RectTransform parent); + // Instantiates the single piece prefab under `parent`, calls Assign(shape) on it, + // and wires up its FSM with the matching SlotMarker. + public ShapePieceUI Spawn(ShapeSO shape, SlotMarker targetSlot, RectTransform parent); public void Despawn(ShapePieceUI piece); } ``` +- One prefab in `Content/Gameplay/Prefabs/ShapePiece.prefab` is instantiated repeatedly. Visual identity comes from the `ShapeSO` passed to `Assign`. #### `ShapeBuilderInputBinder` *(Systems)* With UI handlers on the piece itself, an explicit input binder isn't strictly needed — drag events route via the EventSystem. Keep this class only if you need to listen for "any tap outside any piece" (e.g. to dismiss a preview). Otherwise skip. @@ -1919,7 +1969,8 @@ Implemented as `MonoBehaviour` per feature/service so scopes can drag them in th | `DrawingCatalogController` | Feature | Grid logic | catalog, bus | | `DrawingCatalogPresenter` | Feature | UI bridge | view, controller, catalog | | `ShapeBuilderController` | Feature | Piece spawn + snap tracking | catalog, factory, paper, tray, bus, cfg | -| `ShapePieceUI` | Feature | Draggable UI piece (Image + drag handlers) | fsm | +| `ShapeSO` | Core asset | Authored shape (sprite + snap params) | — | +| `ShapePieceUI` | Feature | Draggable UI piece prefab; holds `[SerializeField] ShapeSO _shape` | fsm | | `ShapePieceFsm` | Feature | Per-piece state machine | ui, slot, cfg, audio, bus | | `SlotMarker` | Feature | Slot outline UI Image at target pose | — | | `TrayPanel` | Feature | HUD-side tray with LayoutGroup | — |