Merge savya: shapes as ScriptableObjects + one piece prefab

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Savya Bikram Shah
2026-05-27 13:34:09 +05:45

107
Readme.md
View File

@@ -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/<theme>/<id>/{Template.asset, Pieces/, Regions/, PaperBackground.png}` | Authored drawings (under existing `Content/Gameplay/` root) |
| `Content/Gameplay/Drawings/<theme>/<id>/{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<ShapePieceDTO> Pieces { get; }
IReadOnlyList<ShapeSO> Pieces { get; } // shapes that get spawned in the tray for this drawing
IReadOnlyList<ColorRegionDTO> 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; ~80120 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<ShapePieceDTO> Pieces { get; } // for ShapeBuilder
IReadOnlyList<ShapeSO> Pieces { get; } // shapes spawned in tray (reusable across drawings)
IReadOnlyList<ColorRegionDTO> 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<SlotMarker>()` 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<string> 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<ShapeSO> 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 | — |