# Color Book — Architecture Guide A toddler-targeted (ages 2–6) coloring book game built on the same **Strict Modular Monolith** pattern as the Bus Game. Powered by **VContainer** for DI, **UniTask** for async, **Addressables** for shipped content, and a **Canvas-only UI** render strategy. This document is the canonical reference for the Color Book game's structure. The Bus Game's [Darkmatter Architecture Guide](../Assets/Darkmatter_Architecture_Guide.md) is the parent contract; this doc only adds game-specific structure. --- ## 1. Game Flow Four scenes. Each gets its own scope. Root services persist across all of them. ``` ┌─────────────────┐ │ Boot.unity │ RootLifetimeScope — services + cross-scene singletons │ │ AppBoot: Addressables.Init → Progression.Load → Catalog.Init └────────┬────────┘ │ Scenes.LoadAsync("MainMenu") ▼ ┌─────────────────┐ │ MainMenu.unity │ Spine mascot looping idle. Single "Play" button. │ │ Tap → SetLastOpened(null) → load Colorbook └────────┬────────┘ │ Scenes.LoadAsync("Colorbook") ▼ ┌─────────────────┐ ◀─────────────────────────┐ │ Colorbook.unity │ Catalog grid of drawings. │ (back from Gameplay returns here; │ │ Each cell shows: cached │ catalog cells refresh with cached │ │ user thumbnail if any, │ thumbnails written during gameplay) │ │ else DefaultThumbnail. │ │ │ Tap cell → │ │ │ Progression.SetLastOpened(id) → load Gameplay └────────┬────────┘ │ │ Scenes.LoadAsync("Gameplay") │ ▼ │ ┌─────────────────┐ │ │ Gameplay.unity │ Active drawing experience │ │ │ │ │ Reads Progression.LastOpenedTemplateId │ │ Reads Progression.GetProgress(id) → null, │ │ Building, or Coloring │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ GameplayState.Building │ │ │ │ • Pieces in tray, drag → snap │ │ │ │ • Pre-snapped pieces auto-locked │ │ │ │ if resuming │ │ │ │ • Back tap → save partial state ─┼────┤ │ │ + load Colorbook │ │ │ └────────────────┬─────────────────────┘ │ │ │ ShapeAssembledSignal │ │ │ (save phase + thumb) │ │ ▼ │ │ ┌──────────────────────────────────────┐ │ │ │ GameplayState.Coloring │ │ │ │ • Tap color → tap region → paint │ │ │ │ • Undo / Redo any time │ │ │ │ • Autosave (debounced 500 ms) │ │ │ │ • Save tap → capture + native ─┼────┐ │ │ Photos save + cache thumb │ │ │ │ • Next tap → save + mark complete │ │ │ │ + advance to next drawing ──────┼─┐ │ │ │ • Back tap → save + load Colorbook─┼────┤ │ └──────────────────────────────────────┘ │ │ │ │ │ └────────────────────────────────────────────┼──┘ │ ┌──────────────────────────┘ │ AdvanceToNextDrawing: │ Catalog.NextUnseen(currentId) → reload Gameplay └─ stays in Gameplay.unity, no scene transition ``` The user views captured drawings inside the phone's native **Photos** app — there is no in-app gallery viewer. `ICaptureService` produces PNG bytes; `IGalleryService` is a thin shim over a native plugin that writes those bytes into the device's photo library. The Save System (§13) decides *when* to capture and save. --- ## 2. Philosophy Identical to Bus Game: - **Vertical slices** — code grouped by Feature, not by type. - **Strict layering** — dependencies flow downward only. - **Composition over inheritance** — wired by DI. - **Code vs Content** — `Code/` for logic, `Contents/` for assets. Game-specific additions: - **Toddler-first UX** — large hitboxes, forgiving snap radii, no fail states, no timers. - **Stateless replay** — every action (paint, snap) is an `ICommand` so undo/redo and auto-save are trivial. - **Capture-as-truth** — the gallery is a folder of PNGs on disk, not a serialized scene graph. What the child sees is what gets saved. --- ## 3. Dependency Graph ``` App ──────────┐ ▼ Features ──► Core ◄── Services ▲ └── Libs ``` ### Forbidden actions - Services cannot reference Features. - Features cannot reference Service implementations — only Core contracts via DI. - Features cannot reference other Features — use `IEventBus` signals or Core contracts. - Core may not contain logic. Interfaces, enums, DTOs, signal records only. - Core may reference `UniTask` for async contract signatures. Nothing else. --- ## 4. Folder Structure This section reflects the **actual project on disk today**. Empty folders that have been reserved for upcoming work are marked `(planned)`; everything else has at least one file in it. Aspirational additions for the rest of the game are listed in §4c at the bottom. ### 4a. Actual layout on disk ``` Assets/Darkmatter/ ├── Scenes/ │ └── Boot.unity ← only scene wired so far │ ├── Content/ ← singular ("Content", not "Contents") │ └── Gameplay/ │ └── PaperRig/ ← stale folder — paper rig dropped; safe to delete │ ├── Data/ │ ├── Inputs/ (Input System .inputactions) │ └── Settings/ │ ├── Persistance/Resources/ (ProtectedPlayerPrefs settings) │ ├── Rendering/ (URP renderer + asset) │ └── Scenes/URP2DSceneTemplate.unity │ └── Code/ ├── App/ │ └── LifetimeScopes/ │ └── RootLifetimeScope.cs ← scope loads serialized IServiceModule list │ Darkmatter.App.asmdef │ ├── Core/ (asmdef name: `Core`, namespace root `Darkmatter.Core.*`) │ ├── Compatibility/ │ │ └── IsExternalInit.cs (C#9 init shim for older runtimes) │ ├── Contracts/ │ │ ├── Paper/ ← stale empty folder — delete (Paper is no longer a feature) │ │ └── Services/ │ │ ├── Assets/IAssetProviderService.cs │ │ ├── Audio/IAudioService.cs, ISfxPlayer.cs │ │ ├── Camera/ICameraService.cs │ │ ├── Capture/ ← (planned — ICaptureService) │ │ ├── Inputs/IInputReader.cs │ │ └── Scenes/ISceneService.cs │ ├── Data/ │ │ ├── Dynamic/Services/Audio/ (AudioHandle, AudioRequest) │ │ └── Static/Services/Audio/ (SfxCatalogSO) │ └── Enums/ │ └── Services/ │ ├── Audio/ (AudioChannel, AudioPlayMode, SfxId) │ ├── Camera/CameraType.cs (MainCamera, UICamera — ArtCamera not added yet) │ └── Scenes/GameScene.cs │ ├── Features/ ← (planned — empty folder today) │ ├── Libs/ │ ├── FSM/ (IState, State, StateMachine + Docs) │ │ Libs.FSM.asmdef │ ├── Installers/ (IServiceModule — Register(IContainerBuilder)) │ │ Libs.Installers.asmdef │ ├── Observer/ (IEventBus, EventBus — note: not named "EventBus") │ │ Libs.Observer.asmdef │ ├── PlayerPrefs/ (ProtectedPlayerPrefs — used in place of a Persistence service) │ │ ├── Editor/ Libs.PlayerPrefs.Editor.asmdef │ │ └── Runtime/ Libs.PlayerPrefs.asmdef │ └── UI/ (ToggleButton, ToggleButtonGroup) │ Libs.UI.asmdef │ └── Services/ ├── Analytics/ │ ├── Installers/AnalyticsServiceModule.cs │ └── Systems/FirebaseAnalyticsSystem.cs │ Services.Analytics.asmdef ├── Assets/ │ ├── AddressableAssetProviderService.cs │ └── AddressableLoadHandleTracker.cs │ Services.Assets.asmdef ├── Audio/ │ ├── AudioService.cs │ └── SfxPlayer.cs │ Services.Audio.asmdef ├── Camera/ │ ├── Installers/CameraServiceModule.cs │ └── Service/CameraService.cs │ Services.Camera.asmdef ├── Inputs/ │ ├── Generated/GameInputs.cs (Input System codegen) │ ├── Installers/InputServiceModule.cs │ └── Readers/InputReaderSO.cs │ Services.Inputs.asmdef └── Scenes/ └── SceneService.cs Services.Scenes.asmdef ``` ### 4b. Conventions visible in current code - **Asmdef per Service / Lib / App / Core.** No Feature asmdefs yet (folder is empty). - **Core sub-tree shape:** `Core/{Compatibility, Contracts, Data, Enums}` — deeply nested by Service rather than by topic. Game-specific Core types (Drawing, Coloring, Paper, Gallery, History, Progression, Signals) will be added under either `Core/Contracts//` or new top-level `Core//` — pick one convention before adding the first one. - **Service sub-shape varies** — some are flat (`Services/Audio/*.cs`), some split (`Services/Camera/Installers/`, `Services/Camera/Service/`). The "official" per-feature shape (§4d) hasn't been validated against real code yet. - **Installer pattern:** `MonoBehaviour, IServiceModule` with `Register(IContainerBuilder builder)`. **Not** `IInstaller.Install(...)` as older sections of this doc imply. New installers must follow `IServiceModule`. - **No `AppBoot`** entry point exists yet. `RootLifetimeScope` only iterates a serialized `MonoBehaviour[] serviceModules` and calls `Register` on each `IServiceModule`. Boot sequence in §29 is aspirational. - **No Persistence service.** `Libs/PlayerPrefs` (the `ProtectedPlayerPrefs` library) is in place for small-key state. JSON persistence for gallery sidecars will need a separate service when Gallery lands. - **Camera service is a registry**, not a fitter. `ICameraService.Register/Get` only — no aspect-fit logic. ### 4c. Planned additions (not on disk yet) All new game code follows the same nesting pattern as existing Services — `Contracts/Features//`, `Data/{Dynamic,Static}/Features//`, `Features//` — and asmdefs drop the `Darkmatter.` prefix to match `Core`, `Services.Audio`, `Libs.Observer`. Rough landing order for ColorBook scene to be playable: | Path | Role | |---|---| | `Core/Contracts/Services/Capture/ICaptureService.cs` | Capture service contract — returns PNG bytes | | `Core/Contracts/Services/Gallery/IGalleryService.cs` | Native-gallery save shim | | `Core/Contracts/Features/Drawing/IDrawingTemplate.cs`, `IDrawingTemplateCatalog.cs` | Drawing template contracts | | `Core/Contracts/Features/Coloring/IColorPalette.cs` | Palette contract | | `Core/Contracts/Features/Progression/IProgressionService.cs` | Progression contract | | `Core/Data/Static/Features/Drawing/DrawingTemplateSO.cs` | Authored drawing data | | `Core/Data/Static/Features/Drawing/ShapeSO.cs` | Authored shape (sprite + snap params, reusable) | | `Core/Data/Static/Features/Coloring/ColorPaletteSO.cs` | Authored palette data | | `Core/Data/Dynamic/Features/Drawing/ColorRegionDTO.cs` | Runtime region struct | | `Core/Data/Dynamic/Features/Coloring/PaintCommandDTO.cs` | Runtime coloring struct | | `Core/Data/Dynamic/Features/Signals/*.cs` (DrawingSelectedSignal, ShapeAssembledSignal, ColorAppliedSignal, PaperSavedSignal) | Cross-feature signal structs | | `Core/Enums/Services/Camera/CameraType.cs` | Add `CaptureCamera` enum value to existing file | | `Services/Capture/` (+ `Services.Capture.asmdef`) | `RenderTextureCaptureService` drives the disabled `CaptureCamera` | | `Services/Gallery/` (+ `Services.Gallery.asmdef`) | `NativeGallerySaveService` — wraps the native gallery plugin | | `Features/MainMenu/` (+ `Features.MainMenu.asmdef`) | Spine mascot + Play button | | `Features/{DrawingCatalog,ShapeBuilder,Coloring,Capture,Progression,ColorBookFlow}/` (+ asmdefs each) | Game features | | `App/LifetimeScopes/{MainMenu,ColorBook}LifetimeScope.cs` | Per-scene scopes | | `App/Boot/AppBoot.cs` | Bootstrap entry point | | `Assets/Darkmatter/Scenes/{MainMenu,ColorBook}.unity` | Scenes (no ArtBook — captures go to phone Photos) | | `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 | ### 4d. Per-feature folder layout (matches existing Services pattern) Look at `Services/Camera/` (`Installers/` + `Service/`) and `Services/Analytics/` (`Installers/` + `Systems/`) — that's the convention. Features adopt the same shape, adding `UI/` or `Views/` only when there's something to put in them. ``` Features/[Name]/ ├── Installers/ IServiceModule — VContainer registration ├── Systems/ (or Service/) Controllers, repositories, factories (pure C#) ├── UI/ (only if the feature has Canvas UI) │ ├── *Presenter.cs Pure C#, listens to model, drives view │ └── *View.cs MonoBehaviour, setters only ├── Views/ (only if the feature has world-space MonoBehaviours) └── Features..asmdef ``` Rules of thumb pulled from current Services: - **Use `Service/` (singular)** when the feature has exactly one main implementation class (like `Services/Camera/Service/CameraService.cs`). - **Use `Systems/` (plural)** when there are multiple pure-C# coordinators (like `Services/Analytics/Systems/`). - **Skip nesting entirely** when the feature has only 1–2 files at root (like `Services/Audio/AudioService.cs` + `SfxPlayer.cs` flat). - **`Docs/` is per-folder** in current code — drop a `Docs/` inside any sub-folder that needs notes, don't make a global feature-level Docs. --- ## 5. Namespaces & Asmdef naming C# namespace pattern is `Darkmatter..[.]` — the `Darkmatter.` prefix stays on namespaces. Examples already in code: - `Darkmatter.Core.Contracts.Services.Camera` ([ICameraService.cs](Assets/Darkmatter/Code/Core/Contracts/Services/Camera/ICameraService.cs)) - `Darkmatter.Services.Camera` ([CameraService.cs](Assets/Darkmatter/Code/Services/Camera/Service/CameraService.cs)) - `Darkmatter.Services.Camera.Installers` ([CameraServiceModule.cs](Assets/Darkmatter/Code/Services/Camera/Installers/CameraServiceModule.cs)) - `Darkmatter.Libs.Installers` ([IServiceModule.cs](Assets/Darkmatter/Code/Libs/Installers/IServiceModule.cs)) **Asmdef names drop the `Darkmatter.` prefix.** Existing pattern: | Namespace | Asmdef | |---|---| | `Darkmatter.Core.*` | `Core` | | `Darkmatter.App` | `Darkmatter.App` (one exception — keep as-is, don't churn) | | `Darkmatter.Libs.Observer` | `Libs.Observer` | | `Darkmatter.Libs.FSM` | `Libs.FSM` | | `Darkmatter.Libs.Installers` | `Libs.Installers` | | `Darkmatter.Libs.PlayerPrefs` | `Libs.PlayerPrefs` (+ `Libs.PlayerPrefs.Editor`) | | `Darkmatter.Libs.UI` | `Libs.UI` | | `Darkmatter.Services.Audio` | `Services.Audio` | | `Darkmatter.Services.Assets` | `Services.Assets` | | `Darkmatter.Services.Camera` | `Services.Camera` | | `Darkmatter.Services.Inputs` | `Services.Inputs` | | `Darkmatter.Services.Scenes` | `Services.Scenes` | | `Darkmatter.Services.Analytics` | `Services.Analytics` | New asmdefs follow the same convention: `Services.Capture`, `Services.Gallery`, `Libs.CommandStack`, `Features.Paper`, `Features.Coloring`, etc. --- ## 6. Scenes & Lifetime Scopes Four scenes, each with its own scope. Root scope persists across all scene changes. | Scene | Scope | Status | Contents | |---|---|---|---| | `Boot.unity` | `RootLifetimeScope` | ✅ exists | All Services + Libs + cross-scene singletons (`IProgressionSystem`, `IDrawingTemplateCatalog`). Persists forever. | | `MainMenu.unity` | `MainMenuLifetimeScope` | ⚠️ planned | Spine mascot, single "Play" button. | | `Colorbook.unity` | `ColorbookLifetimeScope` | ⚠️ planned | `ColorbookFlow` + `DrawingCatalog`. Catalog grid where the player picks a drawing. | | `Gameplay.unity` | `GameplayLifetimeScope` | ⚠️ scene exists, scope empty | `ShapeBuilder` + `Coloring` + `History` + `Capture` (the feature wrapper) + `GameplayFlow`. The active drawing experience. | Only `Boot.unity` and an empty `Gameplay.unity` exist today; the four scene scope classes need full implementation (only `RootLifetimeScope` is functional). Scopes nest: `Root → (MainMenu | Colorbook | Gameplay)`. Services and cross-scene features (Progression, DrawingTemplate) resolve from the root parent. Scene scopes only register their own features. **No in-app gallery** — captured drawings go to the phone's native Photos app via `IGalleryService`. The catalog grid in `Colorbook.unity` shows the user's progress by reading cached thumbnails from `IProgressionSystem`. ### Boot chain (planned) No `AppBoot` class exists yet — today `RootLifetimeScope` only registers services and stops. When `AppBoot` is added (as an `IAsyncStartable` registered via `RootLifetimeScope`), it should run once, in order: 1. `IAssetProviderService.InitializeAsync()` — Addressables init. 2. `IProgressionSystem.LoadAsync()` — hydrate per-template state from PlayerPrefs. 3. `IDrawingTemplateCatalog.InitializeAsync()` — batch-load all `DrawingTemplateSO`s by Addressables label. 4. Optional: preload UI sounds, palette assets. 5. `ISceneService.LoadAsync(MainMenu)`. Failures show a child-friendly retry screen; never crash. --- ## 7. Rendering Strategy **Full Canvas UI.** No `SpriteRenderer`, no `Physics2D`, no offscreen `RenderTexture` for the live view. The paper, slots, pieces, and color regions are all `Image` components on a Screen-Space-Camera canvas. Standard Unity UI eventing (`IPointerDownHandler`, `IDragHandler`) handles all input. ``` ┌──────────────────────────────────────────────────────────┐ │ PaperCanvas (Screen Space - Camera, UICamera) │ │ layer: PaperUI │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ PaperPanel (RectTransform, 2048×2048 ref units) │ │ │ │ ├─ BackgroundImage │ │ │ │ ├─ SlotsPanel (slot Image outlines) │ │ │ │ ├─ PiecesPanel (draggable piece Images) │ │ │ │ └─ RegionsPanel (colorable region Images) │ │ │ └──────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────┐ │ HUDCanvas (Screen Space - Overlay, OR separate camera) │ │ layer: HUDUI │ │ ├─ Palette panel │ │ ├─ Undo / Redo buttons │ │ ├─ Capture / Next buttons │ │ └─ Tray panel (during build phase) │ └──────────────────────────────────────────────────────────┘ CaptureCamera (disabled by default, one-shot Render() on capture) orthographic, projection cloned from UICamera cullingMask = PaperUI only targetTexture = temp RT allocated per capture (2048×2048) ``` ### Cameras | Camera | Render mode | Culling Mask | Render Target | Purpose | |---|---|---|---|---| | `UICamera` | Screen-Space - Camera (orthographic) | `PaperUI`, `HUDUI` | Screen | Normal display each frame. | | `CaptureCamera` | Orthographic, disabled | `PaperUI` only | temp `RenderTexture` | One-shot `Render()` invoked by `ICaptureService.CaptureAsync()`. | `CaptureCamera` shares `UICamera`'s position, orthographic size, and clip planes so the captured frame matches what the player sees — minus the HUD because of the culling mask. ### Layers | Layer | Used by | |---|---| | `PaperUI` | `PaperCanvas` and all of its children (background, slots, pieces, regions, completion FX). Visible in capture. | | `HUDUI` | `HUDCanvas` and tray panel (palette, undo/redo, capture button, drawing catalog grid, etc.). Excluded from capture. | | `EventSystem` | Unity's input layer — managed automatically. | ### Why full UI | Need | Choice | Why | |---|---|---| | Tap-to-paint region | `Image` + `Image.alphaHitTestMinimumThreshold` + `IPointerClickHandler` | Tight alpha-based hit shape per region. No mesh / collider authoring. Tap events route through `EventSystem` natively. | | Drag/drop shape pieces | `Image` + `IBeginDragHandler` / `IDragHandler` / `IEndDragHandler` | Standard Unity UI drag. Pointer events come in canvas-local coords already. No screen→world math anywhere. | | Visual transition during drag → snap | `DOAnchorPos`, `DOSizeDelta`, `DOLocalRotate` | All pose is in `RectTransform` units. The "transition" is a tween over canvas-local values — no swap of render context. | | Capture to PNG | Dedicated `CaptureCamera` with `cullingMask = PaperUI` | One `Render()` call into a temp RT. HUD physically can't appear. | | Multi-resolution support | `CanvasScaler` on `PaperCanvas` (Scale With Screen Size) | Reference resolution `2048 × 2048`, Match = 1 (height). All `anchoredPosition` units are constant across devices. | | HUD layout independent of paper | `HUDCanvas` (separate Canvas) | HUD scales/anchors per its own rules without affecting the paper layout. | | Drawing catalog grid, palette, etc. | Standard UI (`GridLayoutGroup`, `ScrollRect`, `Button`) | Anchors handle aspect ratios. Async thumbnail loader. | ### Multi-resolution rule The paper content is **canvas-unit-stable.** Author every drawing against a fixed 2048 × 2048 reference resolution. Slot positions, piece sizes, region rects, hit shapes — all expressed in `anchoredPosition` / `sizeDelta` units. `CanvasScaler` on `PaperCanvas` does the screen mapping. `PaperPanel` is anchored center, fixed 2048×2048 (or whatever you pick for the reference). On a wider screen, `CanvasScaler` pillarboxes the panel; on a narrower screen, it letterboxes. The panel's contents never resize relative to each other. If you want a backdrop (wood/cloth behind the paper area), it's a sibling `Image` of `PaperPanel` (still on `PaperUI` layer) sized to fill the canvas. The backdrop *is* captured into the PNG by default — set its layer to `HUDUI` if you want it excluded. ### Tradeoff vs the old RT-paper-rig design | Concern | RT-paper-rig (old) | Canvas-only (current) | |---|---|---| | Paper contracts | 2 contracts + ~80 lines of math | gone (paper is just RectTransforms in scene) | | Input pipeline | `IInputReader` → bridge → `Physics2D.OverlapPoint` | native `EventSystem` (`IPointerDownHandler` etc.) | | Coloring hit shape | `PolygonCollider2D` from `Sprite.Editor` physics shape | `Image.alphaHitTestMinimumThreshold = 0.5f` on the region sprite | | Per-frame render passes | 2 (ArtCamera into RT + UICamera draws RawImage) | 1 (UICamera draws everything) | | Capture | read persistent RT | one-shot `CaptureCamera.Render()` | | Coordinate gotchas | mismatches between screen / RT / world | none — everything is canvas-local | If you ever need world-space effects (particle sparkles that physically explode outside the paper, free-draw brush stroke, pinch zoom on the artwork), revisit the RT approach. For the v1 tap-to-fill + drag-to-snap design, Canvas-only is correct. --- ## 8. Core Contracts All Core types are pure data or interfaces. ### Drawing > Contracts live in `Darkmatter.Core.Contracts.Features.Drawing`; DTOs in `Darkmatter.Core.Data.Dynamic.Features.Drawing`. ```csharp namespace Darkmatter.Core.Contracts.Features.Drawing; public interface IDrawingTemplate { string Id { get; } string DisplayName { get; } Sprite DefaultThumbnail { get; } // authored fallback (used when user has no captures for this template) Sprite PaperBackground { get; } IReadOnlyList Pieces { get; } // shapes that get spawned in the tray for this drawing IReadOnlyList Regions { get; } } public readonly struct ColorRegionDTO { public string RegionId { get; } public Sprite Sprite { get; } // assigned to Image.sprite public Vector2 AnchoredPosition { get; } // canvas units, relative to PaperRoot (region's authored parent) public Vector2 SizeDelta { get; } // canvas units public Color InitialColor { get; } // usually white // Hit shape comes from the sprite alpha — set Image.alphaHitTestMinimumThreshold = 0.5. // No polygon path needed; sprite import settings ("Read/Write Enabled") provide it. } ``` ### 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`. ```csharp namespace Darkmatter.Core.Contracts.Features.Coloring; public interface IColorPalette { string Id { get; } IReadOnlyList Colors { get; } } public readonly struct PaintCommandDTO { public string RegionId { get; } public Color FromColor { get; } public Color ToColor { get; } } ``` ### Scene composition (no Paper feature) The paper area is just a `PaperPanel` GameObject in the `Gameplay.unity` scene. There is **no `IPaperSurface` contract** and no Paper feature. The single cross-feature anchor — `PaperRoot` — is exposed via a small `GameplaySceneRefs : MonoBehaviour, IGameplaySceneRefs`: ```csharp public sealed class GameplaySceneRefs : MonoBehaviour, IGameplaySceneRefs { [SerializeField] private RectTransform paperRoot; public RectTransform PaperRoot => paperRoot; } ``` Scene refs hold only **what must be shared across features**. Everything else — the shape tray, the color palette, the drawing-instance prefab (which carries its own `SlotMarker`s + `ColorRegionView`s as children) — is owned by its feature's **holder view**. This keeps `GameplaySceneRefs` from becoming a god object as features are added. The drawing prefab is instantiated under `PaperRoot`; slot markers and color regions are pre-authored as children of that prefab, so neither `SlotsParent` nor `RegionsParent` needs its own ref. The `CaptureCamera` will live on a separate scene MB (planned, owned by the Capture feature). Registered once via `GameplayLifetimeScope`: ```csharp builder.RegisterComponent(_sceneRefs); ``` Features inject `IGameplaySceneRefs` (or their own holder views) directly. ### History > Contracts in `Darkmatter.Core.Contracts.Features.History`. ```csharp namespace Darkmatter.Core.Contracts.Features.History; public interface ICommand { void Execute(); void Undo(); } public interface IUndoStack { bool CanUndo { get; } bool CanRedo { get; } void Push(ICommand cmd); // executes + appends void Undo(); void Redo(); void Clear(); } ``` ### Capture & Gallery Two separate, independent services. **Capture** produces PNG bytes. **Gallery** writes those bytes into the device's native photo library. Neither service knows about the other; the orchestration lives in the Capture feature. ```csharp namespace Darkmatter.Core.Contracts.Services.Capture; public interface ICaptureService { // Renders the disabled CaptureCamera into a temp RT, ReadPixels into a Texture2D, // encodes PNG, releases the RT. Returns the encoded bytes. UniTask CaptureAsync(); } namespace Darkmatter.Core.Contracts.Services.Gallery; public interface IGalleryService { // Saves the given PNG bytes into the device's native photo library // under the given album. Native plugin handles platform permissions. UniTask SaveToDeviceAsync(byte[] png, string albumName = "Color Book"); } ``` - `ICaptureService` owns the `CaptureCamera` reference (a disabled `Camera` in the ColorBook scene). The camera's `cullingMask` is set to `PaperUI` so the HUD physically cannot appear in the PNG. Paper background is just an `Image` on `PaperUI` — no compositing pass. - `IGalleryService` is a **thin shim over a native gallery plugin** (NativeGallery, NativeShare, or a custom plugin). It does **not** save thumbnails, does **not** maintain a file list, does **not** delete entries. Everything in the device library is owned by the OS. - There is no `SavedArtworkDTO`, no sidecar JSON, no in-app gallery file system. The user views their drawings in the phone's Photos app. ### Signals > Signal structs live in `Darkmatter.Core.Data.Dynamic.Features.Signals` (runtime data, cross-feature). ```csharp namespace Darkmatter.Core.Data.Dynamic.Features.Signals; public readonly struct DrawingSelectedSignal { public string TemplateId { get; } } public readonly struct ShapeAssembledSignal { public string TemplateId { get; } } public readonly struct ColorAppliedSignal { public string RegionId { get; } public Color Color { get; } } public readonly struct PaperCapturedSignal { public string TemplateId { get; } // captured, before native-gallery save } public readonly struct PaperSavedSignal { public string TemplateId { get; } // PNG written to phone library } ``` --- ## 9. Feature Responsibilities ### `MainMenu` - Lives in `MainMenu.unity`. Single primary action: **Play** (→ `ColorBook` scene). There is no in-app gallery viewer — captured drawings live in the phone's Photos app. - Hosts a **Spine character mascot** (via `SkeletonGraphic` for Canvas). Authored animations include idle loop, wave, react-to-button, victory dance. - `MenuMascotPresenter` (pure C#) drives the mascot from code: subscribes to Play-button hover / click events and the model's idle timer, calls `IMenuMascotView.Play(animName, loop)`. - View is setter-only. Spine-Unity's `SkeletonGraphic.AnimationState.SetAnimation(track, name, loop)` is encapsulated behind `IMenuMascotView`. - Mascot's skeleton + atlas ship via Addressables (see §10). ### `DrawingCatalog` - Loads the catalog manifest (list of available template IDs). - Presents a scrollable grid of thumbnails (Canvas), one per template. - Each cell shows `IDrawingTemplate.DefaultThumbnail` (the authored fallback sprite). The user's captured drawings live in the phone's Photos app, not in the catalog grid. - On select → fires `DrawingSelectedSignal(templateId)` and unloads the catalog UI. ### `ShapeBuilder` - Listens to `DrawingSelectedSignal` (raised by the Colorbook scene before scene change; resume reads `LastOpenedTemplateId` in Gameplay scope startup). - Loads the per-drawing prefab via `IDrawingTemplateCatalog`, instantiates it under `GameplaySceneRefs.PaperRoot`. The prefab carries the `SlotMarker`s at their authored poses. - **`ShapeHolderView`** owns the tray `RectTransform` + slide-in/out animation (PrimeTween — `Tween.UIAnchoredPosition` + `Tween.Alpha` on `CanvasGroup`). Default hide direction is right (off-screen +X), tunable in inspector. - **`ShapeHolderPresenter`** subscribes to `ShapeBuilderStartedSignal` → `view.Show()` and `ShapeAssembledSignal` → `view.Hide()`. No controller poking the view directly. - **`IShapePieceFactory`** (`ShapePieceFactory` impl) owns the per-piece dependencies (`ShapeBuilderConfig`, `ISfxPlayer`, `IEventBus`, `IUndoStack`) and the spawn parent (read from `ShapeHolderView.SpawnRoot`). Factory creates one `ShapePiece` MB and runs its `Setup`. Controller calls `factory.Create(prefab, shape, slot, trayPos, preSnapped)` per piece. - `ShapeBuilderController` only orchestrates: loads piece prefab via `IAssetProviderService`, computes tray positions from `holder.SpawnWidth`, publishes `ShapeBuilderStartedSignal` before spawn, and listens for `PieceSnappedSignal` to count down to `ShapeAssembledSignal`. If `progress.Phase == ShapeBuilding`, pieces in `progress.SnappedPieces` are passed to the factory with `preSnapped: true`. - `ShapePiece` is a single MB handling all three behaviors inline: drag (Unity UI `IBeginDrag / IDrag / IEndDrag`), reactive preview lerp when within `cfg.PreviewRadius`, snap (PrimeTween — `Tween.UIAnchoredPosition` / `UISizeDelta` / `LocalRotation`) on release inside `cfg.SnapRadius`, otherwise tween back to tray. - Publishes `PieceSnappedSignal(pieceId)` on lock. Controller counts against expected; fires `ShapeAssembledSignal(templateId)` when all locked. ### `Coloring` - Listens to `ShapeAssembledSignal`. - Regions are pre-authored as children of the drawing prefab (under `PaperRoot`). Each is a `ColorRegionView` (UI `Image` + `IPointerClickHandler`). On `Awake`, the view sets `Image.alphaHitTestMinimumThreshold` from a serialized field (default `0.01f`; tune up to `0.5f` for tighter hits) so taps on transparent pixels pass through to the next region below. - **`ColorPaletteHolderView`** owns the palette container `RectTransform` + slide-in/out animation (same pattern as `ShapeHolderView` — `Tween.UIAnchoredPosition` + `Tween.Alpha` on `CanvasGroup`, hides off-screen right). - **`ColorPaletteHolderPresenter`** subscribes to `ShapeAssembledSignal` → `view.Show()`. Hide is invoked from scene tear-down or future phase-exit hook. - **`ColorButton` + `IColorButtonFactory`** (planned): each color is its own self-contained MB (swatch `Image` + `IPointerClickHandler`). `ColorButtonFactory` instantiates the button prefab under `ColorPaletteHolderView.SpawnRoot` and calls `button.Setup(color, state, sfx)`. On click, the button writes directly to `ColoringStateRepository.SelectColor(myColor)` — no view/presenter intermediary. `ColoringController` iterates `IColorPalette.Colors` once on init and calls `factory.Create(color)` per entry. Mirrors `ShapePieceFactory` for symmetry. - Each region's `OnPointerClick` → `ColoringController.PaintRegion(view)`. - Controller builds `PaintRegionCommand(regionId, oldColor, newColor)` and pushes to `IUndoStack`. Command sets `Image.color` on Execute/Undo. - Publishes `ColorAppliedSignal` for SFX / sparkle effects. - **Resume:** if `progress.RegionColors` is non-empty, spawned regions are initialized with those saved colors instead of `region.InitialColor`. - **Autosave hook:** after each `PaintRegion`, debounces 500 ms then calls `GameplayFlowController.AutosaveAsync` so the colors hit disk without thumbnail re-render. See §13. ### `History` - Owns the scoped `IUndoStack` for the current Gameplay session. - Cleared on Gameplay scope startup (new drawing = fresh history). - Capped at 20 entries. - UI: two big arrow buttons; disabled state when `CanUndo` / `CanRedo` is false. ### `Capture` - Wraps `ICaptureService.CaptureAsync()` (one-shot `CaptureCamera.Render()` into a temp RT, ReadPixels, EncodeToPNG). Returns raw PNG bytes. - The **Capture feature does NOT decide what to do with the bytes** — `GameplayFlowController` calls it, then routes to gallery + thumbnail cache depending on the trigger. See §13. ### `Progression` - Single source of truth for per-template user state. Implements `IProgressionSystem`, persists `DrawingProgress` records via `Libs.PlayerPrefs` (single JSON blob under `PlayerPrefsKeys.Progression`). - Internally stores thumbnails per template as PNG files in `Application.persistentDataPath/thumbs/{safeId}.png` (large blobs don't belong in PlayerPrefs). - `ProgressionRepository` does the IO; `ProgressionSystem` keeps an in-memory cache and exposes a clean API. - Exposes: `GetProgress(id)`, `SaveProgressAsync(progress)`, `SaveProgressAsync(progress, png)`, `ClearProgressAsync(id)`, `IsCompleted(id)`, `CompletedTemplateIds`, `LastOpenedTemplateId / SetLastOpened`, `GetCachedThumbnailAsync(id)`. - See §13 for the full save matrix. ### `ColorbookFlow` (in `Colorbook.unity`) - The orchestrator for the catalog scene. - On scope start: calls `_drawingCatalog.InitializeAsync()` to populate the visible-IDs list. - Subscribes to `DrawingSelectedSignal`: `_progression.SetLastOpened(id)` + `_scenes.LoadAsync(Gameplay)`. ### `GameplayFlow` (in `Gameplay.unity`) - The orchestrator for the active drawing scene. - On scope start: reads `_progression.LastOpenedTemplateId`, fetches its `DrawingProgress`, enters either Building (no progress / Phase==ShapeBuilding) or Coloring (Phase==Coloring) state. - Handles all save points (§13): Back button, ShapeAssembled transition, Save button, Next button, app lifecycle pause/quit. - Uses `Libs.FSM` (StateMachine) for `Building` ↔ `Coloring` runtime states. --- ## 10. Addressables Strategy Mirror the Bus Game pattern via `IAssetProviderService`. ### What ships through Addressables | Asset | Why | |---|---| | `DrawingTemplate` ScriptableObject (per drawing) | Many; load on demand. | | `ShapeSO` assets | Reused across drawings; load once per drawing batch. | | Region sprites | Heavy; loaded per drawing. | | Paper backgrounds | Per template, sometimes shared. | | Color palette SOs | Swap per theme. | | Audio clips (tap, snap, complete, sparkle) | Shared SFX bank. | | Spine mascot (`SkeletonDataAsset` + atlas) | Heavy textures; load with `MainMenu` scene, release on scene exit. | ### What does NOT use Addressables - HUD prefabs (palette button, undo icon) — always loaded with scene. - Core UI canvases. - Boot scene assets. - User-saved gallery PNGs — those live in `Application.persistentDataPath`. ### Group layout ``` Drawings_Animals (label: drawing, animals) Drawings_Vehicles (label: drawing, vehicles) Drawings_Shapes (label: drawing, shapes) Shapes_Library (label: shape) — reusable ShapeSO assets Palettes (label: palette) Audio_UI (label: sfx, ui) Audio_Coloring (label: sfx, coloring) Spine_MainMenu (label: spine, menu) — mascot skeleton + atlas ``` ### Lifecycle - Catalog loads **thumbnail handles only** (cheap). - On select → full template loads (pieces + regions + paper). - On "Next" or scene exit → previous template `Release`d before next loads. - This bound matters on toddler tablets with limited RAM. ### Remote groups (future) Drawing packs ship as remote bundles. New theme packs (Christmas, Dinosaurs) update without an app store release. --- ## 11. Persistence Only one in-app persistent store — small settings + progression. Captured drawings go to the device's native photo library (managed by the OS, not by the app). ### Settings + progression via `Libs.PlayerPrefs` `ProtectedPlayerPrefs` (in `Libs/PlayerPrefs/`) is a lightweight encrypted-string wrapper around Unity's `PlayerPrefs`. Used for: - Completed template IDs (JSON-encoded list). - Last opened drawing. - Audio volume, simple toggles. A higher-level `IProgressionService` reads/writes these keys; consumers never touch `PlayerPrefs` directly. ### Native photo library (gallery) Captured PNGs go to the phone's Photos app via `IGalleryService.SaveToDeviceAsync(bytes, albumName)`. The app does **not**: - Write `.png` files to `persistentDataPath`. - Generate or store thumbnails locally. - Maintain any sidecar JSON / index. - Provide list / load / delete operations. The user opens the phone's Photos app to view, share, or delete their drawings. iOS / Android handle permissions and album organization. --- ## 12. Capture Pipeline A dedicated `CaptureCamera` lives in the ColorBook scene, disabled by default. It renders only the `PaperUI` layer into a temp `RenderTexture` when capture fires. The PNG bytes are then handed to the native gallery plugin — no local file IO. ``` [Save button or Next button] │ ▼ CaptureController.SaveAsync(templateId) │ ▼ ICaptureService.CaptureAsync() │ ├─ rt = RenderTexture.GetTemporary(2048, 2048, 0, ARGB32) ├─ _captureCam.targetTexture = rt ├─ _captureCam.Render() (one-shot; cullingMask = PaperUI only) ├─ _captureCam.targetTexture = null ├─ prev = RenderTexture.active ├─ RenderTexture.active = rt ├─ tex = new Texture2D(2048, 2048, RGBA32, false) ├─ tex.ReadPixels(full rect, 0, 0); tex.Apply() ├─ RenderTexture.active = prev ├─ RenderTexture.ReleaseTemporary(rt) ├─ bytes = tex.EncodeToPNG() (on worker via UniTask.RunOnThreadPool) ├─ Object.Destroy(tex) └─ return bytes ▼ EventBus.Publish(new PaperCapturedSignal(templateId)) │ ▼ IGalleryService.SaveToDeviceAsync(bytes, "Color Book") │ ├─ native plugin handles platform permissions ├─ writes PNG into the device's Photos album └─ (no return — fire and forget; throws on failure) ▼ EventBus.Publish(new PaperSavedSignal(templateId)) ``` Notes: - HUD never appears in capture because `CaptureCamera.cullingMask` excludes `HUDUI`. Layer mask, not coincidence — even if you accidentally parent a HUD element under `PaperPanel`, putting it on the wrong layer keeps it out. - Paper background is just an `Image` on `PaperUI`. Already in the right layer; no special compositing. - Saved PNGs are 2048×2048 on every device. `CaptureCamera` has fixed `orthographicSize` and aspect, independent of screen size. - `CaptureAsync` is safe to call repeatedly. The CaptureCamera's transform / projection are set once at scene start and never modified. - The temp RT is allocated via `RenderTexture.GetTemporary` so successive captures don't leak GPU memory. - `IGalleryService` and `ICaptureService` are independent — `IGalleryService` knows nothing about the camera; `ICaptureService` knows nothing about the native plugin. The chain is the `CaptureController`'s sole responsibility. --- ## 12b. Save System Everything the user does that affects their drawing state must end up persisted. `GameplayFlowController` is the **single owner** of all save calls — feature controllers expose getters; the flow controller assembles the `DrawingProgress` record and hands it to `IProgressionSystem`. ### What is saved | Field on `DrawingProgress` | Meaning | |---|---| | `templateId` | Which drawing this record is about | | `phase` | `ShapeBuilding` or `Coloring` — where to resume | | `snappedPieces` | Pieces locked into slots (relevant in ShapeBuilding) | | `regionColors` | Per-region color (relevant in Coloring) | | `hasThumbnail` | Whether a thumbnail PNG exists on disk for catalog display | | `hasBeenCompleted` | Flipped true on first Next; never flips back | | `completionCount` | Number of times Next was pressed (optional stats) | | `updatedUtcIso` / `firstCompletedUtcIso` | Timestamps (ISO 8601 strings for JsonUtility) | ### Save matrix | Trigger | Phase saved | Snapped pieces | Region colors | Thumbnail? | Native gallery? | |---|---|---|---|---|---| | **ShapeAssembledSignal** (Building → Coloring transition) | `Coloring` | all | empty | **yes** (bare-assembled paper) | no | | **Each paint** (debounced 500 ms) | `Coloring` | all | current | no | no | | **Save button** | `Coloring` | all | current | **yes** | **yes** | | **Next button** | `Coloring` | all | current | **yes** | **yes** | | **Back button (during Building)** | `ShapeBuilding` | current | empty | no | no | | **Back button (during Coloring)** | `Coloring` | all | current | **yes** | no | | **OnApplicationPause(true) / OnApplicationQuit** | current phase | current | current | no | no | Two design principles drive the matrix: - **Thumbnail capture is expensive** (render + ReadPixels + PNG encode). Skip it on partial-assembly saves and per-paint autosaves. Only generate a thumbnail when the user takes an explicit save-style action. - **Defensive saves never block UX.** App pause/quit saves whatever is in memory without capturing — fast path, no awaitable IO holding up shutdown. ### `Next` adds two extras - Flips `hasBeenCompleted = true` (preserves first `firstCompletedUtcIso`); increments `completionCount`. - Plays completion animation, then calls `AdvanceToNextDrawing` → `Catalog.NextUnseen(currentId)` → reload Gameplay scope for the new drawing. ### Storage layout | What | Where | |---|---| | `DrawingProgress` records + `lastOpened` | One JSON blob in `ProtectedPlayerPrefs[PlayerPrefsKeys.Progression]` (see `ProgressionRootDto`) | | Thumbnail PNGs | `Application.persistentDataPath/thumbs/{safeId}.png` (one file per template that has a thumbnail) | `safeId` replaces `/` and `\` with `_` so `animals/elephant` becomes `animals_elephant.png`. ### Resume / load decision On Gameplay scope startup: ```csharp var id = _progression.LastOpenedTemplateId; var progress = _progression.GetProgress(id); // null if untouched if (progress == null || progress.Value.phase == DrawingPhase.ShapeBuilding) { fsm.Go(Building); // spawn pieces in tray, pre-snap any in progress.snappedPieces } else { fsm.Go(Coloring); // skip ShapeBuilder; auto-snap pieces; spawn regions with progress.regionColors } ``` ### Catalog cells reflect saves automatically The Colorbook scene reloads on Back. Its presenter calls `_progression.GetCachedThumbnailAsync(id)` per cell → returns the most recent save's PNG. Drawings the user touched display their progress; untouched drawings fall back to `IDrawingTemplate.DefaultThumbnail`. No live-update plumbing needed — re-entry is the refresh. ### Files touching the save system | Path | Role | |---|---| | `Core/Contracts/Features/Progression/IProgressionSystem.cs` | Contract | | `Core/Data/Dynamic/Features/Progression/DrawingProgress.cs` | The struct | | `Core/Data/Dynamic/Features/Progression/ProgressionRootDto.cs` | JSON root (records + lastOpened) | | `Core/Data/Dynamic/Features/Progression/RegionColorEntry.cs` | Flattened color entry (JsonUtility-friendly) | | `Core/Enums/Features/Progression/DrawingPhase.cs` | Phase enum | | `Features/Progression/Systems/ProgressionSystem.cs` | In-memory cache + write serialization (SemaphoreSlim) | | `Features/Progression/Systems/ProgressionRepository.cs` | PlayerPrefs JSON + thumbnail file IO | | `Features/Progression/Installers/ProgressionFeatureModule.cs` | Registers `IProgressionSystem` as Singleton in Root scope | ### Single rule > Only `GameplayFlowController` calls `_progression.SaveProgressAsync(...)`. Feature controllers expose getters; they never touch the tracker themselves. This means there is exactly one place to audit when save behavior changes. --- ## 13. Communication Rules | Use case | Mechanism | |---|---| | Load template, return result | Direct DI call (`IDrawingTemplateCatalog.LoadAsync`). | | Capture → save chain | Direct DI calls, sequenced. | | Notify HUD that a region was painted | `IEventBus` signal. | | Notify Progression that a drawing was completed | `IEventBus` signal. | | Tell ColorBookFlow that pieces are assembled | `IEventBus` signal. | | Tell Coloring which color is currently selected | Direct DI on `ColoringStateRepository`. | **Never** use signals for request/response. If you need a return value or guaranteed single handler, define a Core interface. --- ## 14. UI (MVP — Passive View) Identical to Bus Game. - **Model** — controller / repository, fires C# events. - **View** — `MonoBehaviour`, only setters (`SetColors(IReadOnlyList)`). - **Presenter** — pure C#, subscribes to model events, calls view setters. ### Inspector bridge For palette icons, undo buttons, region prefabs: ```csharp [SerializeField, RequireInterface(typeof(IColorButtonView))] private MonoBehaviour[] _colorButtons; ``` --- ## 15. Toddler UX Constraints These shape several design decisions and are **non-negotiable**: - **No fail states.** Drawings cannot be "wrong". - **No timers.** Nothing decays or runs out. - **No tiny hitboxes.** Drag tolerance ≥ 40 canvas units; snap radius ≥ 80 canvas units for shape pieces. (Canvas reference resolution is 2048×2048 — these are anchored-position deltas, not screen px.) - **Auto-snap on near-miss.** If a piece is dropped within `1.5 × SnapRadius`, snap anyway and play a happy sound. - **No text-heavy UI.** Icons everywhere. Single-word labels max. - **Loud, immediate feedback.** Every tap plays a sound; every fill bursts a small particle effect. - **Undo cap = 20.** Toddlers will mash undo. Bound the memory. - **Long-press = quick menu off.** Avoid surprise modals. --- ## 16. Testing | Layer | Test type | Location | |---|---|---| | `Libs/CommandStack` | EditMode unit tests | `Libs/CommandStack/Tests/` | | `Core` DTOs | EditMode | rarely needed; mostly type-only checks. | | `Services/Gallery` | EditMode w/ temp directory | mocks `Application.persistentDataPath`. | | `Services/Capture` | PlayMode | requires a Camera in the test scene. | | `Features/*/Systems` | EditMode w/ DI test container | inject fakes for `IUndoStack`, signals captured by a fake `IEventBus`. | | Full flow | PlayMode smoke test | one drawing → assemble → color → capture → assert gallery has 1 file. | --- ## 17. "Where do I put this?" Checklist 1. **Is it a cross-assembly interface / enum / DTO?** → `Core/` 2. **Is it a generic, sellable utility?** → `Libs/` 3. **Is it infrastructure (input, audio, file IO, addressables, capture)?** → `Services/` 4. **Is it gameplay logic specific to coloring books?** → `Features/` 5. **Is it composition / scene wiring?** → `App/` When in doubt, ask: *would deleting this feature break Core?* If yes, the dependency is wrong. --- ## 18. Open Questions / Future Work - **Pencil/brush mode** — currently the design is tap-to-fill regions. A free-draw brush mode would need a `BrushStrokeCommand` and a dynamic texture per region; out of scope for v1. - **Multi-child profiles** — single-profile for v1; multi-profile would slot in behind `IProgressionService` and `IGalleryService` keyed by `profileId`. - **Cloud sync** — gallery sync would happen behind `IGalleryService` (decorator pattern); local-first stays the source of truth. - **Sticker / decoration layer** — additive sprite layer above coloring, also `ICommand`-driven so it integrates with undo/redo cleanly. --- ## 19. Quick Reference — Feature ↔ Signal Map | Feature | Subscribes to | Publishes | |---|---|---| | `MainMenu` | — | — (Play tap loads a scene directly) | | `DrawingCatalog` | — | `DrawingSelectedSignal` | | `ShapeBuilder` | `DrawingSelectedSignal` | `ShapeAssembledSignal` | | `Coloring` | `ShapeAssembledSignal` | `ColorAppliedSignal` | | `History` | `DrawingSelectedSignal` (to clear) | — | | `Capture` | — (button-driven) | `PaperCapturedSignal`, `PaperSavedSignal` | | `Progression` | `PaperSavedSignal` | — | | `ColorBookFlow` | `ShapeAssembledSignal`, `PaperSavedSignal` | — | --- Maintained alongside the [Darkmatter Architecture Guide](../Assets/Darkmatter_Architecture_Guide.md). Do not break the dependency arrows. --- ## 20. Assembly Definition Map Every Lib / Service / Feature is its own `.asmdef`. The `Darkmatter.` prefix is **only** on the App asmdef; everything else uses bare `.` names. References follow the layer rules. ### On disk today | Asmdef | Path | References | |---|---|---| | `Darkmatter.App` | `App/` | All Services, Libs, Core (Features when they land) | | `Core` | `Core/` | (none — `UniTask` allowed in async signatures) | | `Libs.FSM` | `Libs/FSM/` | `Core` | | `Libs.Installers` | `Libs/Installers/` | (VContainer only) | | `Libs.Observer` | `Libs/Observer/` | `Core` | | `Libs.PlayerPrefs` | `Libs/PlayerPrefs/Runtime/` | (standalone) | | `Libs.PlayerPrefs.Editor` | `Libs/PlayerPrefs/Editor/` | `Libs.PlayerPrefs` | | `Libs.UI` | `Libs/UI/` | `Core` | | `Services.Analytics` | `Services/Analytics/` | `Core`, `Libs.Installers` | | `Services.Assets` | `Services/Assets/` | `Core`, `Libs.Installers` | | `Services.Audio` | `Services/Audio/` | `Core`, `Libs.Installers` | | `Services.Camera` | `Services/Camera/` | `Core`, `Libs.Installers` | | `Services.Inputs` | `Services/Inputs/` | `Core`, `Libs.Installers` | | `Services.Scenes` | `Services/Scenes/` | `Core`, `Libs.Installers` | ### Planned (not on disk yet) | Asmdef | Path | References | |---|---|---| | `Services.Capture` | `Services/Capture/` | `Core`, `Libs.Installers` | | `Services.Gallery` | `Services/Gallery/` | `Core`, `Libs.Installers` | | `Features.MainMenu` | `Features/MainMenu/` | `Core`, `Libs.Installers` | | `Features.DrawingCatalog` | `Features/DrawingCatalog/` | `Core`, `Libs.Installers` | | `Features.ShapeBuilder` | `Features/ShapeBuilder/` | `Core`, `Libs.Installers` | | `Features.Coloring` | `Features/Coloring/` | `Core`, `Libs.Installers` | | `Features.Capture` | `Features/Capture/` | `Core`, `Libs.Installers` | | `Features.Progression` | `Features/Progression/` | `Core`, `Libs.Installers`, `Libs.PlayerPrefs` | | `Features.ColorBookFlow` | `Features/ColorBookFlow/` | `Core`, `Libs.Installers`, `Libs.FSM` | > `Libs.CommandStack`, `Features.ArtBook`, and `Features.Paper` were previously planned but cut. Undo lives inside `Features.History` (already on disk). Art-book is removed because captures save to phone Photos. Paper is just RectTransforms in the scene — no contract needed. `ICommand` / `IUndoStack` live in `Core`, so `Features.Coloring` reaches the undo stack without referencing `Features.History`. **Hard rules:** - No Service asmdef references any Feature asmdef. - No Feature asmdef references another Feature asmdef. - All Services and Features depend on `Libs.Installers` so they can implement `IServiceModule`. - The compiler enforces this — if a `using` won't resolve, the dependency is wrong. --- ## 21. LifetimeScope Concrete Sample All scopes use the same pattern: a serialized `MonoBehaviour[]` list of `IServiceModule` installers. Each installer is a MonoBehaviour on a child GameObject of the scope. Scope iterates and calls `Register`. **No hardcoded registrations in the scope itself.** This is exactly what [RootLifetimeScope.cs](Assets/Darkmatter/Code/App/LifetimeScopes/RootLifetimeScope.cs) already does today. ### `RootLifetimeScope` (Boot scene, persists forever) — actual code ```csharp using Darkmatter.Libs.Installers; using UnityEngine; using VContainer; using VContainer.Unity; public class RootLifetimeScope : LifetimeScope { [SerializeField] private MonoBehaviour[] serviceModules; protected override void Configure(IContainerBuilder builder) { foreach (var module in serviceModules) { if (module is IServiceModule serviceModule) serviceModule.Register(builder); } } } ``` The inspector lists the installer MonoBehaviours in `serviceModules[]`. Drag the children of the Boot scope GameObject (e.g. `AudioServiceModule`, `CameraServiceModule`, `InputServiceModule`, `AssetProviderServiceModule`, `AnalyticsServiceModule`, `SceneServiceModule`) into that slot. Each is a `MonoBehaviour, IServiceModule`. ### `ColorBookLifetimeScope` (per-scene, child of Root) — same pattern ```csharp namespace Darkmatter.App.LifetimeScopes; public sealed class ColorBookLifetimeScope : LifetimeScope { [SerializeField] private MonoBehaviour[] sceneModules; protected override void Configure(IContainerBuilder builder) { foreach (var module in sceneModules) { if (module is IServiceModule serviceModule) serviceModule.Register(builder); } } } ``` Drag the scene's installer MonoBehaviours into `sceneModules[]`: - `DrawingCatalogModule` - `ShapeBuilderModule` - `ColoringModule` - `HistoryModule` - `CaptureModule` - `ProgressionModule` - `ColorBookFlowModule` Each registers its own classes via `IServiceModule.Register(IContainerBuilder)`. > If a scope needs a non-installer reference (e.g. a `GameplaySceneRefs` MB holding `PaperRoot`), expose it as a separate `[SerializeField]` and `builder.RegisterComponent(...)` it inside the scope's `Configure`. Don't put scene refs inside an installer — keep installers stateless across scenes. --- ## 22. Installer Pattern — Concrete Coloring Sample Mirrors the existing [CameraServiceModule.cs](Assets/Darkmatter/Code/Services/Camera/Installers/CameraServiceModule.cs) and [InputServiceModule.cs](Assets/Darkmatter/Code/Services/Inputs/Installers/InputServiceModule.cs) — a `MonoBehaviour` implementing `IServiceModule.Register`. ```csharp namespace Darkmatter.Features.Coloring.Installers; public sealed class ColoringModule : MonoBehaviour, IServiceModule { [SerializeField] private ColoringConfig _config; public void Register(IContainerBuilder builder) { builder.RegisterInstance(_config); builder.Register(Lifetime.Scoped).AsSelf(); builder.Register(Lifetime.Scoped) .As() .AsSelf(); builder.Register(Lifetime.Scoped); builder.RegisterEntryPoint(); } } ``` Convention: - One `IServiceModule` per feature, named `Module` (matches `CameraServiceModule`, `InputServiceModule`, `AnalyticsServiceModule` already in the project). - `MonoBehaviour` lives on a GameObject under the scope's hierarchy; dragged into the scope's `serviceModules[]` / `sceneModules[]` inspector list. - Method name is `Register`, not `Install`. There is **no `IInstaller`** in this project — uses `IServiceModule` from [Libs.Installers](Assets/Darkmatter/Code/Libs/Installers/IServiceModule.cs). - Registers only its own types. Never touches another feature's types. - If the installer needs to wire scene-bound MonoBehaviours into DI, expose them as `[SerializeField]` fields on the installer itself and `builder.RegisterComponent(_foo)` them. `GameplaySceneRefs` (§32.13) is registered this way directly from the scope's serialized field. Per-feature holder views (e.g. `ShapeHolderView`, `ColorPaletteHolderView`) are registered the same way from each feature's `IServiceModule`. --- ## 23. Command Pattern — `PaintRegionCommand` ```csharp namespace Darkmatter.Features.Coloring.Commands; internal sealed class PaintRegionCommand : ICommand { private readonly ColorRegionView _view; private readonly Color _fromColor; private readonly Color _toColor; private readonly IEventBus _bus; public PaintRegionCommand(ColorRegionView view, Color from, Color to, IEventBus bus) { _view = view; _fromColor = from; _toColor = to; _bus = bus; } public void Execute() { _view.SetColor(_toColor); _bus.Publish(new ColorAppliedSignal(_view.RegionId, _toColor)); } public void Undo() { _view.SetColor(_fromColor); _bus.Publish(new ColorAppliedSignal(_view.RegionId, _fromColor)); } } ``` Usage in controller: ```csharp public void PaintRegion(ColorRegionView view) { var current = _state.CurrentColor; if (view.Color == current) return; // no-op var cmd = new PaintRegionCommand(view, view.Color, current, _bus); _undoStack.Push(cmd); // Push executes + records } ``` Same pattern applies to `SnapPieceCommand` if shape-builder steps should be undoable (optional for v1). --- ## 24. CommandStack — `Libs/CommandStack` ```csharp namespace Darkmatter.Lib.CommandStack; public sealed class BoundedUndoStack : IUndoStack { private readonly Deque _undo = new(); private readonly Stack _redo = new(); private readonly int _capacity; public BoundedUndoStack(int capacity = 20) => _capacity = capacity; public bool CanUndo => _undo.Count > 0; public bool CanRedo => _redo.Count > 0; public void Push(ICommand cmd) { cmd.Execute(); _undo.AddLast(cmd); if (_undo.Count > _capacity) _undo.RemoveFirst(); _redo.Clear(); } public void Undo() { if (!CanUndo) return; var cmd = _undo.Last; _undo.RemoveLast(); cmd.Undo(); _redo.Push(cmd); } public void Redo() { if (!CanRedo) return; var cmd = _redo.Pop(); cmd.Execute(); _undo.AddLast(cmd); } public void Clear() { _undo.Clear(); _redo.Clear(); } } ``` `Deque` keeps the oldest entry cheap to evict when the cap fires. --- ## 25. Self-contained Button + Factory — Color Palette The palette deliberately **skips the View/Presenter pair**. Each color is its own self-contained `ColorButton` MB, spawned by a factory that mirrors `ShapePieceFactory`. The button writes directly to `ColoringStateRepository` on click — no intermediary indexed event, no presenter wiring `_state.SelectedIndexChanged → _view.SetSelected`. Why: palette behavior is per-button (each one is a discrete tap target with its own selected/hover anim), and adding per-button variants later (locked colors, magic "rainbow" button, premium colors) is just a new MB type that the factory can branch to. A View+Presenter pair would force every variant through a single `SetColors(IReadOnlyList)` setter. ### Button (self-contained MB) ```csharp namespace Darkmatter.Features.Coloring.UI; public sealed class ColorButton : MonoBehaviour, IPointerClickHandler { [SerializeField] private Image swatch; [SerializeField] private GameObject selectedRing; // optional highlight private Color _color; private ColoringStateRepository _state; private ISfxPlayer _sfx; public Color Color => _color; public void Setup(Color color, ColoringStateRepository state, ISfxPlayer sfx) { _color = color; _state = state; _sfx = sfx; swatch.color = color; _state.SelectedColorChanged += OnSelectedChanged; OnSelectedChanged(_state.CurrentColor); } public void OnPointerClick(PointerEventData _) { _sfx.Play(SfxId.UiTap); _state.SelectColor(_color); } private void OnSelectedChanged(Color current) { if (selectedRing != null) selectedRing.SetActive(current == _color); } private void OnDestroy() { if (_state != null) _state.SelectedColorChanged -= OnSelectedChanged; } } ``` ### Factory (mirrors ShapePieceFactory) ```csharp namespace Darkmatter.Features.Coloring.Systems; public interface IColorButtonFactory { ColorButton Create(Color color); } public sealed class ColorButtonFactory : IColorButtonFactory { private readonly ColorPaletteHolderView _holder; private readonly GameObject _buttonPrefab; private readonly ColoringStateRepository _state; private readonly ISfxPlayer _sfx; public ColorButtonFactory( ColorPaletteHolderView holder, GameObject buttonPrefab, ColoringStateRepository state, ISfxPlayer sfx) { _holder = holder; _buttonPrefab = buttonPrefab; _state = state; _sfx = sfx; } public ColorButton Create(Color color) { var go = Object.Instantiate(_buttonPrefab, _holder.SpawnRoot); var btn = go.GetComponent(); btn.Setup(color, _state, _sfx); return btn; } } ``` ### Spawn loop (inside `ColoringController.InitializeAsync`) ```csharp foreach (var color in _palette.Colors) _buttonFactory.Create(color); ``` That's the whole feature wiring. No `IColorPaletteView`, no `OnColorButtonClicked` indexed event, no `SelectedIndex ↔ SetSelected` round-trip. ### When to use View/Presenter instead Other features (history buttons, drawing catalog) still use View/Presenter — that pair fits when a single canvas of fixed sub-widgets needs to react to one model. Use button+factory when each item is **spawned dynamically from data** and has independent click behavior. --- ## 26. ShapeBuilder — Snap Algorithm All math is in canvas-local space — `anchoredPosition`, `sizeDelta`, `localRotation`. No world coords. Behavior lives inline in `ShapePiece : MonoBehaviour` — no FSM, no factory, no state classes. Three behaviors expressed across three Unity drag handlers. ### `OnDrag` — reactive preview lerp ```csharp public void OnDrag(PointerEventData e) { if (_locked) return; var pointerLocal = ScreenToLocal(e.position) + _grabOffset; var slotPos = _slot.RectTransform.anchoredPosition; float dist = Vector2.Distance(pointerLocal, slotPos); if (dist <= _cfg.PreviewRadius) { if (!_inPreview) { _inPreview = true; _sfx.Play(SfxId.ShapeHover); } ApplyPreviewLerp(pointerLocal, dist); } else { _inPreview = false; RectTransform.anchoredPosition = pointerLocal; RectTransform.sizeDelta = _traySize; RectTransform.localRotation = Quaternion.identity; } } private void ApplyPreviewLerp(Vector2 pointerLocal, float dist) { float t = Mathf.Clamp01(1f - dist / _cfg.PreviewRadius); if (_cfg.PreviewCurve != null) t = _cfg.PreviewCurve.Evaluate(t); var slot = _slot.RectTransform; RectTransform.anchoredPosition = Vector2.Lerp(pointerLocal, slot.anchoredPosition, t); RectTransform.sizeDelta = Vector2.Lerp(_traySize, slot.sizeDelta, t); RectTransform.localRotation = Quaternion.Slerp(Quaternion.identity, slot.localRotation, t); } ``` ### `OnEndDrag` — snap or return ```csharp public void OnEndDrag(PointerEventData e) { if (_locked) return; float dist = Vector2.Distance( RectTransform.anchoredPosition, _slot.RectTransform.anchoredPosition); if (dist <= _cfg.SnapRadius) Snap(); else ReturnToTray(); } private void Snap() { Lock(); // reparent + raycast off + _locked = true var slot = _slot.RectTransform; Tween.UIAnchoredPosition(RectTransform, slot.anchoredPosition, _cfg.SnapDuration, Ease.OutBack); Tween.UISizeDelta (RectTransform, slot.sizeDelta, _cfg.SnapDuration, Ease.OutBack); Tween.LocalRotation (RectTransform, slot.localRotation, _cfg.SnapDuration, Ease.OutBack); _sfx.Play(SfxId.ShapeSnap); _bus.Publish(new PieceSnappedSignal(_shape.Id)); } ``` ### Four things worth noting 1. **Reparent on lock** — `Lock()` calls `RectTransform.SetParent(_slot.RectTransform.parent, false)`. The piece moves from the HUD-side tray to the per-drawing slot parent so it travels with the paper and gets included in the captured PNG. 2. **Three parallel PrimeTween calls** — position, size, rotation. Tweens start together so the piece visually snaps as one motion. Zero allocations per tween. 3. **`SnapRadius` is in canvas units** (from `ShapeBuilderConfig`, e.g. 80–120), not world units. Same `CanvasScaler` reference resolution across devices = same hit feel. 4. **Preview hover sound fires once per drag**, on the boundary cross into the preview radius. `_inPreview` flag resets on `OnBeginDrag`. ### Pre-snapped resume If the user closes the app mid-assembly (or after completing the drawing), the saved `DrawingProgress.snappedPieces` lists which pieces were locked. On resume, the controller passes `preSnapped: true` to `Setup` for those, and `ShapePiece.SnapInstantly()` puts them straight into their slots — no tween, no input. The user can keep snapping the remaining pieces. ```csharp private void SnapInstantly() { Lock(); var slot = _slot.RectTransform; RectTransform.anchoredPosition = slot.anchoredPosition; RectTransform.sizeDelta = slot.sizeDelta; RectTransform.localRotation = slot.localRotation; } ``` ### Spawn loop in `ShapeBuilderController.BuildAsync` The controller no longer parents pieces directly nor knows about `Setup` dependencies. It delegates to `IShapePieceFactory`, which owns the parent (`ShapeHolderView.SpawnRoot`) and the per-piece deps (cfg/sfx/bus/undo). Tray width is read from the holder, not from scene refs. ```csharp _bus.Publish(new ShapeBuilderStartedSignal(template.Id)); // ShapeHolderPresenter → view.Show() int count = template.Pieces.Count; float trayW = _holder.SpawnWidth; float pitch = trayW / (count + 1); for (int i = 0; i < count; i++) { var shape = template.Pieces[i]; var slot = FindSlotForShape(slots, shape); var trayPos = new Vector2(pitch * (i + 1) - trayW * 0.5f, 0f); var preSnapped = preSnappedIds != null && preSnappedIds.Contains(shape.Id); _factory.Create(_piecePrefab, shape, slot, trayPos, preSnapped); } ``` Controller listens for `PieceSnappedSignal`, counts against expected piece count, fires `ShapeAssembledSignal` when complete → `GameplayFlowController` captures bare-assembled thumbnail, transitions to Coloring (see §13). --- ## 27. Rendering Order & Sorting Canvas-only — order is sibling index inside `PaperPanel` (front-most is last in hierarchy). No URP 2D sorting layers. `PaperPanel` children (bottom → top): ``` PaperPanel ├─ BackgroundImage (paper texture) ├─ RegionsPanel (colorable region Images) ├─ SlotsPanel (slot outline Images — under pieces so snapped pieces hide them) ├─ PiecesPanel (draggable / snapped piece Images) └─ EffectsPanel (sparkle / particle UI for completion FX, optional) ``` `HUDCanvas` is a separate Canvas at a higher sorting order (or Screen Space - Overlay). It never sorts against `PaperCanvas` because they're different canvases. `CaptureCamera` renders only the `PaperUI` layer. The HUD physically cannot leak into the saved PNG because of the culling mask, regardless of sibling order. > If you ever need particles outside the canvas (e.g. confetti falling across the full screen on completion), use a separate Canvas above the HUD with its own sub-tree of UI particles. Don't add `ParticleSystem`s under PaperPanel — they don't render in UI canvases without `UIParticleSystem` or similar packages. --- ## 28. Native Gallery Integration `IGalleryService.SaveToDeviceAsync(byte[] png, string albumName)` is the only operation. Implementations wrap a native plugin — recommended packages: | Platform | Library | |---|---| | Cross-platform | [Native Gallery for Android & iOS](https://github.com/yasirkula/UnityNativeGallery) | | iOS-only fallback | `PHPhotoLibrary` direct bindings | | Android-only fallback | `MediaStore` direct bindings via `AndroidJavaClass` | Permission handling: - **iOS** — `NSPhotoLibraryAddUsageDescription` in `Info.plist`. iOS prompts on first save. - **Android 13+** — no permission required for writes that target a public collection via the plugin. - **Android 11–12** — `WRITE_EXTERNAL_STORAGE` declared but not requested at runtime; plugin uses scoped storage. - **Android ≤ 10** — `WRITE_EXTERNAL_STORAGE` runtime permission requested by the plugin. `NativeGallerySaveService` (the planned concrete) catches plugin permission denials and either silently no-ops (toddler app) or surfaces a child-friendly retry prompt via the HUD. No app-side data is persisted about saved drawings. Once `SaveToDeviceAsync` returns, the PNG is the OS's responsibility. --- ## 29. Boot & Error Handling > **Status: not implemented.** No `AppBoot` class exists. Today, [RootLifetimeScope.cs](Assets/Darkmatter/Code/App/LifetimeScopes/RootLifetimeScope.cs) only iterates installer MonoBehaviours and registers them — nothing runs after that. The block below is the *target* sequence when `AppBoot` is added as an `IAsyncStartable` entry point under `App/Boot/`. ``` AppBoot.StartAsync() (planned — Features/Boot/AppBoot.cs, registered via builder.RegisterEntryPoint()) ├─ try Addressables.InitializeAsync() │ fail → show "Tap to retry" splash ├─ try preload palette + UI sounds (Addressables labels) │ fail → log + continue (non-fatal) ├─ try _persistence.LoadAsync() │ fail → start with empty progression (don't crash) ├─ _scenes.LoadAsync("MainMenu") └─ done ``` Toddler-mode error UI: - One large smiling icon. - One big "tap" button. - No text, no error codes. - A small upper-right gear opens a parent-only diagnostic screen (long-press 3 s to unlock). --- ## 30. Setup Checklist (new dev, day one) 1. Open `Colorbook.sln` at the repo root. 2. Verify required Unity packages are installed (check `Packages/manifest.json`): VContainer, UniTask, Addressables, Input System, URP, **Spine-Unity runtime** (`com.esotericsoftware.spine.spine-unity`) for the main-menu mascot, DOTween (for snap/return tweens). 3. Open `Assets/Darkmatter/Scenes/Boot.unity` (currently the only scene wired). 4. Inspect the `RootLifetimeScope` GameObject — confirm its `serviceModules[]` list references the child installer MonoBehaviours (`AudioServiceModule`, `CameraServiceModule`, `InputServiceModule`, etc.). 5. Hit Play from `Boot.unity`. Other scenes (`MainMenu`, `ColorBook`) don't exist yet — they're listed in §6 / §4c as planned work. 6. When new scene scopes land, the same rule applies: never start a scene mid-flow, always enter from `Boot.unity` so the root scope exists. 7. When drawings are authored: duplicate the template folder under `Content/Gameplay/Drawings///`, edit `Template.asset` (pieces + regions), add to the appropriate Addressables group. 8. Run `Tests > EditMode` and `Tests > PlayMode` before pushing (test infra not set up yet — see §16). --- ## 31. Quick Reference — Class ↔ Layer ↔ Asmdef | Class | Layer | Asmdef | |---|---|---| | `IDrawingTemplate`, `IDrawingTemplateCatalog`, `IDrawingCatalogController` | Core | `Core` | | `ColorRegionDTO`, `PaintCommandDTO`, `ColorPaletteSO` | Core | `Core` | | `ShapeSO`, `ShapeBuilderConfig` (ScriptableObjects) | Core | `Core` | | `DrawingProgress`, `DrawingPhase`, `ProgressionRootDto`, `RegionColorEntry` | Core | `Core` | | `ICommand`, `IUndoStack`, `IProgressionSystem` | Core | `Core` | | `UndoStack`, `HistoryButtonsView`, `HistoryButtonsPresenter` | Features | `Features.History` | | `AddressableAssetProviderService` | Services | `Services.Assets` | | `NativeGallerySaveService` | Services | `Services.Gallery` | | `CaptureService` | Services | `Services.Capture` | | `ColoringController`, `ColoringStateRepository`, `ColorRegionView`, `ColorPaletteHolderView`, `ColorPaletteHolderPresenter`, `PaintRegionCommand` | Features | `Features.Coloring` | | `ShapePiece`, `SlotMarker`, `ShapeBuilderController`, `IShapePieceFactory`, `ShapePieceFactory`, `ShapeHolderView`, `ShapeHolderPresenter` | Features | `Features.ShapeBuilder` | | `AddressableDrawingTemplateCatalog` | Features | `Features.DrawingTemplate` | | `DrawingCatalogController`, `DrawingCatalogPresenter`, `DrawingCatalogView`, `CatalogItemVM` | Features | `Features.DrawingCatalog` | | `ColorbookFlowController` | Features | `Features.Colorbook` | | `GameplayFlowController` | Features | `Features.GameplayFlow` | | `ProgressionSystem`, `ProgressionRepository` | Features | `Features.Progression` | | `MenuMascotView`, `MenuMascotPresenter` | Features | `Features.MainMenu` | | `RootLifetimeScope`, `MainMenuLifetimeScope`, `ColorbookLifetimeScope`, `GameplayLifetimeScope`, `GameplaySceneRefs`, `AppBoot` | App | `Darkmatter.App` | If a class's natural home doesn't match its asmdef, the architecture is bent — fix the placement, don't add a reference. ### 31b. Scripts inventory by domain Comprehensive index — every script (existing or planned) grouped by its module. Use this as the single-page mental map. Status column: ✅ exists on disk, ⚠️ planned. #### Core | Module (path) | Scripts | Status | |---|---|---| | `Core/Compatibility/` | `IsExternalInit` | ✅ | | `Core/Contracts/Services/Assets/` | `IAssetProviderService` | ✅ | | `Core/Contracts/Services/Audio/` | `IAudioService`, `ISfxPlayer` | ✅ | | `Core/Contracts/Services/Camera/` | `ICameraService` | ✅ | | `Core/Contracts/Services/Inputs/` | `IInputReader` | ✅ | | `Core/Contracts/Services/Scenes/` | `ISceneService` | ✅ | | `Core/Contracts/Services/Capture/` | `ICaptureService` | ⚠️ | | `Core/Contracts/Services/Gallery/` | `IGalleryService` | ⚠️ | | `Core/Contracts/Features/DrawingCatalog/` | `IDrawingCatalogController`, `IDrawingTemplate`, `IDrawingTemplateCatalog` | ✅ | | `Core/Contracts/Features/Coloring/` | `IColorPalette` | ✅ | | `Core/Contracts/Features/History/` | `ICommand`, `IUndoStack` | ✅ | | `Core/Contracts/Features/Progression/` | `IProgressionSystem` | ✅ | | `Core/Data/Dynamic/Services/Audio/` | `AudioHandle`, `AudioRequest` | ✅ | | `Core/Data/Static/Services/Audio/` | `SfxCatalogSO` | ✅ | | `Core/Data/Static/Features/DrawingTemplate/` | `DrawingTemplateSO` | ✅ | | `Core/Data/Static/Features/ShapeBuilder/` | `ShapeSO`, `ShapeBuilderConfig` | ✅ | | `Core/Data/Static/Features/Coloring/` | `ColorPaletteSO` | ✅ | | `Core/Data/Dynamic/Features/Coloring/` | `ColorRegionDTO`, `PaintCommandDTO` | ✅ | | `Core/Data/Dynamic/Features/Progression/` | `DrawingProgress`, `ProgressionRootDto`, `RegionColorEntry` | ✅ | | `Core/Data/Signals/Features/DrawingCatalog/` | `DrawingSelectedSignal` | ✅ | | `Core/Data/Signals/Features/ShapeBuilder/` | `ShapeAssembledSignal`, `PieceSnappedSignal` | ✅ | | `Core/Data/Signals/Features/Coloring/` | `ColorAppliedSignal` | ⚠️ | | `Core/Data/Signals/Features/Capture/` | `PaperCapturedSignal`, `PaperSavedSignal` | ⚠️ | | `Core/Enums/Features/Progression/` | `DrawingPhase` (`ShapeBuilding`, `Coloring`) | ✅ | | `Core/Enums/Services/Audio/` | `SfxId` (`None`, `ShapeHover`, `ShapeSnap`, `ShapeNiceTry`, `ShapeReturn`) | ✅ | | `Core/Enums/Services/Camera/` | `CameraType` (add `CaptureCamera` value) | ✅ | | `Core/Enums/Services/Scenes/` | `GameScene` | ✅ | #### Libs | Module (path) | Scripts | Status | |---|---|---| | `Libs/FSM/` | `IState`, `State`, `StateMachine` (abstract) | ✅ | | `Libs/Installers/` | `IModule` | ✅ | | `Libs/Observer/` | `IEventBus`, `EventBus` | ✅ | | `Libs/PlayerPrefs/Runtime/` | `ProtectedPlayerPrefs`, `ProtectedPlayerPrefsSettings`, `PlayerPrefsKeys`, `PlayerPrefsKeyRegistry`, `LocalWriteTracker`, `PendingWriteResync` | ✅ | | `Libs/PlayerPrefs/Editor/` | `PlayerPrefsEditorWindow`, `ProtectedPlayerPrefsGettingStartedWindow`, `ProtectedPlayerPrefsSettingsUtility`, `ProtectedPlayerPrefsSetupBootstrap` | ✅ | | `Libs/UI/` | `ToggleButton`, `ToggleButtonGroup` | ✅ | #### Services | Module (path) | Scripts | Status | |---|---|---| | `Services/Analytics/Installers/` | `AnalyticsModule` | ✅ | | `Services/Analytics/Systems/` | `FirebaseAnalyticsSystem` | ✅ | | `Services/Assets/` | `AddressableAssetProviderService`, `AddressableLoadHandleTracker` | ✅ | | `Services/Audio/` | `AudioService`, `SfxPlayer` | ✅ | | `Services/Camera/Service/` | `CameraService` | ✅ | | `Services/Camera/Installers/` | `CameraModule` | ✅ | | `Services/Inputs/Generated/` | `GameInputs` (Input System codegen) | ✅ | | `Services/Inputs/` | (Inputs feature partial — Reader + Installer location TBD) | ⚠️ | | `Services/Scenes/` | `SceneService` | ✅ | | `Services/Capture/Systems/` | `CaptureService` | ✅ (stub) | | `Services/Capture/Installers/` | `CaptureModule` | ✅ | | `Services/Gallery/Core/` | `GalleryService` | ✅ (stub — needs native plugin wiring) | | `Services/Gallery/Installers/` | `GalleryModule` | ✅ | #### Features | Module (path) | Scripts | Status | |---|---|---| | `Features/History/Stack/` | `UndoStack` | ✅ | | `Features/History/Installers/` | `HistoryFeatureModule` | ✅ | | `Features/History/UI/` | `HistoryButtonsView`, `HistoryButtonsPresenter` | ✅ | | `Features/MainMenu/Installers/` | `MainMenuFeatureModule` | ⚠️ | | `Features/MainMenu/Systems/` | `MainMenuModel`, `MenuMascotPresenter` | ⚠️ | | `Features/MainMenu/UI/` | `MenuMascotView`, `IMenuMascotView` | ⚠️ | | `Features/DrawingCatalog/Systems/` | `DrawingCatalogController` | ✅ | | `Features/DrawingCatalog/UI/` | `DrawingCatalogPresenter`, `DrawingCatalogView`, `DrawingCatalogButton`, `CatalogItemVM` | ✅ | | `Features/DrawingCatalog/Installers/` | `DrawingCatalogFeatureModule` | ✅ | | `Features/DrawingTemplate/Systems/` | `AddressableDrawingTemplateCatalog` | ✅ | | `Features/DrawingTemplate/Installers/` | `DrawingTemplateFeatureModule` | ✅ | | `Features/ShapeBuilder/UI/` | `ShapePiece`, `SlotMarker`, `ShapeHolderView`, `ShapeHolderPresenter` | ✅ | | `Features/ShapeBuilder/Systems/` | `ShapeBuilderController`, `IShapePieceFactory`, `ShapePieceFactory` | ✅ | | `Features/ShapeBuilder/Installers/` | `ShapeBuilderFeatureModule` | ✅ | | `Features/Coloring/Systems/` | `ColoringController`, `ColoringStateRepository` | ⚠️ planned | | `Features/Coloring/UI/` | `ColorRegionView` ✅, `ColorPaletteHolderView` ✅, `ColorPaletteHolderPresenter` ✅, `ColorButton` ⚠️ planned | | | `Features/Coloring/Systems/` | `IColorButtonFactory`, `ColorButtonFactory` | ⚠️ planned | | `Features/Coloring/Commands/` | `PaintRegionCommand` | ⚠️ | | `Features/Coloring/Installers/` | `ColoringFeatureModule` | ⚠️ | | `Features/Capture/Systems/` | `CaptureController` (light wrapper around `ICaptureService`) | ⚠️ | | `Features/Capture/UI/` | `CaptureButtonPresenter`, `SaveToastView` | ⚠️ | | `Features/Capture/Installers/` | `CaptureFeatureModule` | ⚠️ | | `Features/Progression/Systems/` | `ProgressionSystem`, `ProgressionRepository` | ✅ (stubs) | | `Features/Progression/Installers/` | `ProgressionFeatureModule` | ✅ | | `Features/ColorbookFlow/System/` | `ColorbookFlowController` | ✅ (needs constructor injection) | | `Features/ColorbookFlow/Installers/` | `ColorbookFlowFeatureModule` | ✅ | | `Features/GameplayFlow/Systems/` | `GameplayFlowController` (single owner of all saves — §13) | ⚠️ | | `Features/GameplayFlow/Installers/` | `GameplayFlowFeatureModule` | ⚠️ | #### App | Module (path) | Scripts | Status | |---|---|---| | `App/LifetimeScopes/` | `BaseLifetimeScope` (abstract), `RootLifetimeScope` | ✅ | | `App/LifetimeScopes/` | `GameLifetimeScope` (placeholder, empty), `GameplayLifetimescope` (typo — needs rename to `GameplayLifetimeScope`) | ⚠️ | | `App/LifetimeScopes/` | `MainMenuLifetimeScope`, `ColorbookLifetimeScope`, `GameplayLifetimeScope` (final) | ⚠️ planned | | `App/Boot/` | `AppBoot` | ⚠️ planned | | `Features/GameplayFlow/SceneRefs/` | `GameplaySceneRefs` (PaperRoot only — per-feature containers moved to holder views) | ✅ | --- ## 32. Class Reference (Detailed) > **Status: target spec, mostly unimplemented.** Existing on disk: `AddressableAssetProviderService`, `AudioService` / `SfxPlayer`, `CameraService`, `SceneService`, `InputReaderSO`, `FirebaseAnalyticsSystem`, plus the History feature (`UndoStack`, `HistoryServiceModule`, `ICommand`, `IUndoStack`). Everything else (MainMenu, DrawingCatalog, ShapeBuilder, Coloring, Capture, Gallery, Progression, ColorBookFlow, AppBoot, scene scopes) is the target shape for when those classes are written. Treat this section as a contract for new code, not documentation of current state. Canonical breakdown of every concrete class and interface. For each: **purpose**, **public surface** (signatures), **injected dependencies**, and **collaborators** (signals or interfaces it talks to). > Convention used below > - `// fields:` = constructor-injected dependencies > - `// pub:` = events / signals fired > - `// sub:` = events / signals consumed > - All async returns are `UniTask` unless noted. > - Folder labels follow the actual nesting pattern: `Core/Contracts/Features//`, `Core/Contracts/Services//`, `Core/Data/Dynamic/Features//`, `Features///`, `Services///`. --- ### 32.1 Core Contracts Pure interfaces and DTOs. Zero logic. #### `IDrawingTemplate` *(Core/Contracts/Features/Drawing — planned)* Immutable view of a single drawing's authored data. ```csharp public interface IDrawingTemplate { string Id { get; } // e.g. "animals/elephant" 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; } // shapes spawned in tray (reusable across drawings) IReadOnlyList Regions { get; } // for Coloring } ``` 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 always uses `DefaultThumbnail`. The user's captured drawings live in the phone's Photos app, not the catalog cell. #### `IDrawingTemplateCatalog` *(Core/Contracts/Features/Drawing — planned)* Authority on which drawings exist, completion state, and "next" selection. ```csharp public interface IDrawingTemplateCatalog { UniTask InitializeAsync(); // pulls manifest from Addressables IReadOnlyList AllTemplateIds { get; } UniTask GetThumbnailAsync(string id); // cheap; for grid UniTask LoadAsync(string id); // expensive; full template void Release(string id); // Addressables ref count down string NextUnseen(string currentId); // progression hint } ``` #### `IColorPalette` *(Core/Contracts/Features/Coloring — planned)* Set of colors offered to the child. Authored as `ColorPaletteSO`. ```csharp public interface IColorPalette { string Id { get; } IReadOnlyList Colors { get; } // 6–10 entries typical } ``` #### `ICommand` & `IUndoStack` *(Core/Contracts/Features/History — planned)* Already shown in section 8. Each undoable user action is one `ICommand`; the stack is bounded. #### `IGalleryService` *(Core/Contracts/Services/Gallery — planned)* Thin shim over a native gallery plugin. Saves PNG bytes into the phone's Photos app. **Does not** track files, thumbnails, or sidecar metadata — the OS owns the file once it's saved. ```csharp public interface IGalleryService { UniTask SaveToDeviceAsync(byte[] png, string albumName = "Color Book"); } ``` - No list / load / delete operations. The user uses the phone's Photos app for those. - Implementation (`NativeGallerySaveService`) wraps a third-party native plugin and handles platform permission prompts. #### `ICaptureService` *(Core/Contracts/Services/Capture — planned)* Snapshots the paper area to a PNG blob. No arguments — implementation owns the disabled `CaptureCamera` reference. ```csharp public interface ICaptureService { UniTask CaptureAsync(); } ``` - Independent of `IGalleryService`. Returns raw PNG bytes; what happens next is the caller's call (save, share, discard). #### Removed contracts - `IPaperRig`, `IArtInputBridge`, `IPaperSurface` — paper is just RectTransforms in the scene now, exposed via `GameplaySceneRefs.PaperRoot`. No contract. - `SavedArtworkDTO`, `IGalleryService.ListAsync/LoadFullAsync/LoadThumbnailAsync/DeleteAsync/GetLatestThumbnailAsync` — no app-side gallery store. - `ArtworkCapturedSignal`, `ArtworkSavedSignal` — replaced by `PaperCapturedSignal` / `PaperSavedSignal` (templateId only, no DTO). #### `IProgressionService` *(Core/Contracts/Features/Progression — planned)* Tracks which templates the child has completed and what they last opened. ```csharp public interface IProgressionService { UniTask LoadAsync(); UniTask SaveAsync(); IReadOnlyCollection CompletedTemplateIds { get; } string LastOpenedTemplateId { get; } void MarkCompleted(string templateId); void SetLastOpened(string templateId); } ``` #### `IAssetProviderService` *(Core/Contracts/Services/Assets — ✅ exists)* Addressables wrapper. Hides handle bookkeeping from features. ```csharp public interface IAssetProviderService { UniTask InitializeAsync(); UniTask LoadAsync(string address) where T : UnityEngine.Object; UniTask> LoadByLabelAsync(string label) where T : UnityEngine.Object; void Release(string address); void ReleaseAll(); } ``` #### `IEventBus` *(Libs/Observer — ✅ exists; note the folder is `Observer`, not `EventBus`)* ```csharp public interface IEventBus { void Publish(T signal) where T : struct; IDisposable Subscribe(Action handler) where T : struct; } ``` Signals are structs to avoid GC. Disposable subscription so presenters can unsubscribe in `Dispose()`. --- ### 32.2 Services Layer Concrete infrastructure. One implementation each. All singletons in `RootLifetimeScope`, registered via per-service `MonoBehaviour, IServiceModule` installers. #### `AddressableAssetProviderService` *(Services/Assets — ✅ exists)* Implements `IAssetProviderService`. - **Responsibility:** Wrap `Addressables.LoadAssetAsync` and ref-count handles by address. - **State:** `Dictionary` keyed by address. - **Notes:** `Release(address)` decrements; `ReleaseAll()` for scene teardown. Initialization must complete before any other service may load. #### `NativeGallerySaveService` *(Services/Gallery — planned)* Implements `IGalleryService` as a thin wrapper around a native gallery plugin. ```csharp // fields: // INativeGalleryPlugin _plugin (third-party native bridge) // IEventBus _bus (optional — for failure surfacing) public sealed class NativeGallerySaveService : IGalleryService { public async UniTask SaveToDeviceAsync(byte[] png, string albumName = "Color Book") { var permission = await _plugin.RequestWritePermissionAsync(); if (permission != Permission.Granted) { // toddler app — silently skip; or publish a failure signal for HUD retry prompt return; } await _plugin.SaveImageToAlbumAsync(png, albumName, fileName: $"colorbook_{DateTime.UtcNow:yyyyMMdd_HHmmss}.png"); } } ``` - **No file IO**, no thumbnails, no sidecars. The native plugin owns everything past the call. - **Permission flow** runs once per session; the plugin caches the grant. - **Failure handling**: a toddler app shouldn't crash or block on denial — silently skip and let the user keep playing. #### `RenderTextureCaptureService` *(Services/Capture — planned)* Implements `ICaptureService`. Drives the scene's disabled `CaptureCamera` once per capture. ```csharp // fields: // Camera _captureCam (scene-bound, registered via CaptureServiceModule) // int _surfaceSize = 2048 // IPathProvider _paths (only if you want to expose paths — usually not needed here) ``` - **Steps:** `RenderTexture.GetTemporary(size, size, 0, ARGB32)` → set `_captureCam.targetTexture = rt` → `_captureCam.Render()` → `ReadPixels` into a `Texture2D` → null out the target texture → `RenderTexture.ReleaseTemporary(rt)` → `EncodeToPNG` → return bytes. - **Threading:** PNG encode happens on `UniTask.RunOnThreadPool` to avoid hitching the main thread on tablets. - **Camera setup:** `_captureCam` has `cullingMask = PaperUI`, `clearFlags = SolidColor` (white or paper color), `orthographicSize` and `aspect` cloned from `UICamera` once at scene start. Stays disabled — `Render()` is the only call site. - **Sizing:** default 2048², overridable. Capped at device max texture size. #### Persistence (no dedicated service) There is no `IPersistenceService` / JSON file writer. `Libs/PlayerPrefs` (`ProtectedPlayerPrefs`) is the only persistent storage in the app. `IProgressionService` consumes it directly. - **Backing store:** `PlayerPrefs` via the encrypted wrapper. - **Keys:** namespaced strings registered in `PlayerPrefsKeyRegistry`. - **Format per value:** JSON-encoded primitives (`ScriptableObject.CreateInstance` not needed at this layer). - **Atomicity:** `PlayerPrefs` is already atomic per key on iOS/Android. #### `SceneService` *(Services/Scenes — ✅ exists)* Implements `ISceneService`. Wraps `SceneManager.LoadSceneAsync` with `UniTask` plus a fade-curtain. ```csharp public interface ISceneService { UniTask LoadAsync(string sceneName, LoadSceneMode mode = LoadSceneMode.Single); UniTask UnloadAsync(string sceneName); } ``` #### `AudioService` *(Services/Audio — ✅ exists; see also `SfxPlayer`)* Implements `IAudioService`. Plays SFX clips loaded by address, mixes via Unity AudioMixer groups. ```csharp public interface IAudioService { UniTask PreloadAsync(string label); // e.g. "sfx,ui" void PlayOneShot(string clipId, float volume = 1f); void SetCategoryVolume(AudioCategory cat, float v01); } ``` Holds an internal `Dictionary` populated at preload. #### `InputReaderSO` *(Services/Inputs/Readers — ✅ exists)* ScriptableObject wrapping the new Input System; exposes events. ```csharp public interface IInputReader { event Action PointerDown; // world pos event Action PointerDrag; event Action PointerUp; } ``` - **Why an SO:** assignable in inspector and survives scene loads, but still resolvable via DI (`builder.RegisterInstance(_inputReader).As()`). --- ### 32.3 Libs Generic, project-agnostic utilities. #### `BoundedUndoStack` *(Libs/CommandStack — planned)* Implements `IUndoStack`. Source already in section 24. - **Capacity:** default 20. - **Invariant:** `_redo` cleared on any new `Push`. - **Edge cases:** `Undo`/`Redo` on empty stack is a no-op (never throws). #### `EventBus` *(Libs/Observer — ✅ exists)* Implements `IEventBus` with a `Dictionary` of `Action` per signal type. - **Subscribe** returns an `IDisposable` that removes the handler on `Dispose`. - **Publish** snapshots the invocation list before iterating (so handlers may safely unsubscribe during dispatch). #### `StateMachine` / `IState` / `State` *(Libs/FSM — ✅ exists)* Generic state machine. Current shape on disk uses `IState` / `State` / `StateMachine` (see [Libs/FSM/](Assets/Darkmatter/Code/Libs/FSM/)). `ColorBookFlowController` (planned) will use this. The generic sketch below is the target shape if you decide to make it strongly-typed via an enum — verify against actual API before consuming. ```csharp public sealed class Fsm where TState : struct, Enum { public TState Current { get; } public event Action Transitioned; public void Bind(TState state, IFsmState handler); public void Go(TState next); // calls Exit on old, Enter on new } public interface IFsmState { void Enter(); void Exit(); } ``` --- ### 32.3b Feature — `MainMenu` *(planned)* Lives in `MainMenu.unity`. Hosts the **Play** / **Art Book** entry buttons plus the **Spine character mascot**. #### `IMenuMascotView` *(UI contract)* Setter-only view interface. Hides Spine-Unity's API behind a tiny surface. ```csharp public interface IMenuMascotView { event Action AnimationComplete; // fires when a non-looping anim ends void Play(string animName, bool loop); void SetSkin(string skinName); // optional — character variants } ``` #### `MenuMascotView : MonoBehaviour, IMenuMascotView` *(UI)* Concrete view. Wraps a `SkeletonGraphic` component (Spine-Unity's Canvas-compatible renderer). ```csharp public sealed class MenuMascotView : MonoBehaviour, IMenuMascotView { [SerializeField] private SkeletonGraphic _skeleton; // Spine UI component public event Action AnimationComplete; public void Play(string animName, bool loop) { var track = _skeleton.AnimationState.SetAnimation(0, animName, loop); track.Complete += _ => AnimationComplete?.Invoke(animName); } public void SetSkin(string skinName) { _skeleton.Skeleton.SetSkin(skinName); _skeleton.Skeleton.SetSlotsToSetupPose(); _skeleton.AnimationState.Apply(_skeleton.Skeleton); } } ``` - `SkeletonGraphic` lives on a child of `MainMenuCanvas`. It's a `Graphic`, so it interacts with `CanvasRenderer` just like an `Image`. - The Spine asset (`SkeletonDataAsset`) is loaded via Addressables, assigned at scene setup, and released on scene exit. #### `MenuMascotPresenter` *(UI)* — `IStartable, IDisposable` Pure C#. Subscribes to button events + idle timer, drives the view. ```csharp // fields: IMenuMascotView _view, MainMenuModel _model, IInputReader _input public sealed class MenuMascotPresenter : IStartable, IDisposable { public void Start() { _view.Play("idle", loop: true); _model.PlayButtonHovered += () => _view.Play("hover_play", loop: false); _model.PlayButtonPressed += () => _view.Play("wave", loop: false); _view.AnimationComplete += OnAnimationComplete; } private void OnAnimationComplete(string anim) { if (anim != "idle") _view.Play("idle", loop: true); // always return to idle } } ``` - Mascot reactions are pure presenter logic — the view never decides what to play. - If you want randomized idle variants, add an idle timer in the model + a list of clip names. #### `MainMenuModel` *(Repository)* Holds menu state — current selected skin, fires hover/click events from button presenters. #### `MainMenuModule : MonoBehaviour, IServiceModule` *(Installers)* Registers the view (`RegisterInstance(_view)`), the presenter as a startable entry point, and the model. > **Package dependency:** [Spine-Unity runtime](http://esotericsoftware.com/spine-unity) (`com.esotericsoftware.spine.spine-unity`). Add to `Packages/manifest.json`. The `SkeletonGraphic` component lives in `Spine.Unity` namespace. --- ### 32.4 Feature — `DrawingCatalog` *(planned)* #### `DrawingCatalogController` *(Systems)* Headless logic. Owns the list of template IDs visible in the grid. ```csharp // fields: IDrawingTemplateCatalog _catalog, IEventBus _bus public sealed class DrawingCatalogController : IAsyncStartable { public IReadOnlyList VisibleIds { get; } public event Action ListChanged; public UniTask StartAsync(CancellationToken ct); // pulls catalog, refreshes list public void OnTemplateSelected(string id); // bus.Publish(new DrawingSelectedSignal(id)) } // pub: DrawingSelectedSignal ``` #### `DrawingCatalogPresenter` *(UI)* Bridges controller ↔ view. ```csharp // fields: IDrawingCatalogView _view, DrawingCatalogController _ctrl, IDrawingTemplateCatalog _catalog public sealed class DrawingCatalogPresenter : IStartable, IDisposable { public void Start(); // wires view.OnItemClicked → _ctrl.OnTemplateSelected public void Dispose(); } ``` - **Thumbnail load:** `_catalog.GetThumbnailAsync(id)` per visible cell, with placeholder while loading. #### `DrawingCatalogView : MonoBehaviour` *(UI)* Implements `IDrawingCatalogView`. Pure setters + click event. ```csharp public interface IDrawingCatalogView { event Action OnItemClicked; void SetItems(IReadOnlyList items); // vm = id + Sprite thumbnail } ``` --- ### 32.5 Feature — `ShapeBuilder` *(planned controller, MBs exist)* The simplified post-FSM design. **No state machine per piece, no factory class.** Three roles: piece MB, slot MB, controller. Plus a tunables SO. #### `ShapePiece : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler` *(UI — ✅ exists)* The single MonoBehaviour that handles drag, reactive preview lerp, snap (PrimeTween), and return-to-tray. Spawned by the controller; `Setup` binds dependencies and starting pose. ```csharp public sealed class ShapePiece : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler { [SerializeField] private Image image; private ShapeSO _shape; // bound by Setup private SlotMarker _slot; private ShapeBuilderConfig _cfg; private ISfxPlayer _sfx; private IEventBus _bus; private Vector2 _trayPos; private Vector2 _traySize; private bool _locked; public ShapeSO Shape => _shape; public string PieceId => _shape != null ? _shape.Id : null; public bool IsLocked => _locked; public RectTransform RectTransform { get; } public void Setup( ShapeSO shape, SlotMarker slot, ShapeBuilderConfig cfg, ISfxPlayer sfx, IEventBus bus, Vector2 trayPos, bool preSnapped); } ``` - Drag handlers run inline in this MB — no separate FSM class. - `Snap()` is a PrimeTween triple (`Tween.UIAnchoredPosition` / `UISizeDelta` / `LocalRotation`); `SnapInstantly()` is the resume path that puts a pre-snapped piece directly into its slot pose without animation. - `ReturnToTray()` builds a PrimeTween `Sequence` of three parallel tweens. - See §26 for the snap algorithm walkthrough. #### `SlotMarker : MonoBehaviour` *(UI — ✅ exists)* Authored per drawing — designer places one in the per-drawing prefab at each slot location with the `RectTransform` set to the target pose and `_shape` field assigned to the matching `ShapeSO`. The `RectTransform` itself **is** the target pose. ```csharp public sealed class SlotMarker : MonoBehaviour { [SerializeField] private ShapeSO shape; [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; public void SetOutlineVisible(bool visible); } ``` **Matching is by `ShapeSO` reference equality** — the controller pairs each `ShapePiece.Shape` with the `SlotMarker.Shape` of the same SO asset. #### `ShapeBuilderConfig : ScriptableObject` *(Static data — ✅ exists)* ```csharp [CreateAssetMenu(menuName = "Darkmatter/ShapeBuilder/Config")] public sealed class ShapeBuilderConfig : ScriptableObject { public float SnapRadius; // 80–120 canvas units public float SnapGraceMultiplier; // (currently unused — grace zone removed) public float PreviewRadius; // ~2× SnapRadius public float SnapDuration; // 0.25s public float ReturnDuration; // 0.25s public AnimationCurve PreviewCurve; // easing for the reactive lerp public Vector2 DragSizeDelta(ShapeSO shape); } ``` #### `ShapeBuilderController` *(Systems — ✅ exists)* Orchestrates per-template build: loads piece prefab, instantiates drawing layout under `PaperRoot`, computes tray positions from `ShapeHolderView.SpawnWidth`, publishes `ShapeBuilderStartedSignal`, delegates per-piece spawn to `IShapePieceFactory`. Subscribes to `PieceSnappedSignal` and fires `ShapeAssembledSignal` when count matches expected. ```csharp // fields: IEventBus _bus, IAssetProviderService _assetProviderService, // IShapePieceFactory _factory, ShapeHolderView _holder, IGameplaySceneRefs _refs public sealed class ShapeBuilderController : IShapeBuilderController, IDisposable { private GameObject _piecePrefab; public async UniTask InitializeAsync(CancellationToken ct); // load prefab + subscribe public IReadOnlyCollection GetSnappedPieceIds(); // for save records public async UniTask BuildAsync( IDrawingTemplate template, IReadOnlyCollection preSnappedIds, CancellationToken ct = default); public void Clear(); // destroys current drawing instance + resets counters } // sub: PieceSnappedSignal // pub: ShapeBuilderStartedSignal (before spawn), ShapeAssembledSignal (when all locked) ``` - **Slot discovery:** after the per-drawing prefab is instantiated, `GetComponentsInChildren(includeInactive: true)` finds all slots. Each slot's `_shape` tells which `ShapeSO` it expects. - **Pre-snap on resume:** if `preSnappedIds.Contains(shape.Id)`, the factory is called with `preSnapped: true` → `ShapePiece.SnapInstantly()` lands it in the slot at scope start. - **Tray width source:** read from `_holder.SpawnWidth` — the controller never touches `_refs` for HUD geometry. #### `IShapePieceFactory` / `ShapePieceFactory` *(Systems — ✅ exists)* Encapsulates piece instantiation. Owns the parent (from `ShapeHolderView.SpawnRoot`) and the per-piece deps so the controller stays focused on flow. ```csharp public interface IShapePieceFactory { ShapePiece Create(GameObject prefab, ShapeSO shape, SlotMarker slot, Vector2 trayPos, bool preSnapped); } // fields: ShapeHolderView _holder, ShapeBuilderConfig _cfg, ISfxPlayer _sfx, IEventBus _bus, IUndoStack _undo public sealed class ShapePieceFactory : IShapePieceFactory { public ShapePiece Create(GameObject prefab, ShapeSO shape, SlotMarker slot, Vector2 trayPos, bool preSnapped) { var go = Object.Instantiate(prefab, _holder.SpawnRoot); var piece = go.GetComponent(); piece.Setup(shape, slot, _cfg, _sfx, _bus, _undo, trayPos, preSnapped); return piece; } } ``` #### `ShapeHolderView` / `ShapeHolderPresenter` *(UI — ✅ exists)* View owns the tray container `RectTransform` (`SpawnRoot`) and a PrimeTween `Sequence` driving slide + alpha on a `CanvasGroup`. Default hide direction is `+X = 1500` (slide right off-screen), tunable per inspector. Public: `SpawnRoot`, `SpawnWidth`, `Show()`, `Hide()`, `HideInstant()`. Presenter is an `IStartable, IDisposable`: - On `Start`: `view.HideInstant()`, subscribe `ShapeBuilderStartedSignal` → `Show`, subscribe `ShapeAssembledSignal` → `Hide`. - On `Dispose`: dispose subscriptions. #### Removed / not needed - **`TrayLayout`** — was a stateless tray-position helper. Tray positions are now computed inline from `holder.SpawnWidth` (~3 lines). - **`ShapePieceFsm`** — was a per-piece state machine. Replaced by inline drag handlers + a single `_locked` bool on `ShapePiece`. - **Five state classes** (`InTray`, `Dragging`, `Preview`, `Snapped`, `Returning`) — gone. Their behavior maps to: `_locked = false` (idle/dragging/preview all share the same handlers), `_inPreview` flag (preview boundary detection), `Snap()` method, `ReturnToTray()` method. - **`ShapeBuilderInputBinder`** — never needed; UI handlers on the piece are sufficient. --- ### 32.6 Feature — `Coloring` *(planned)* #### `ColoringStateRepository` *(Repository)* In-memory model. Owns "currently selected color" and the palette in use. ```csharp public sealed class ColoringStateRepository { public IColorPalette Palette { get; private set; } public Color CurrentColor { get; private set; } public event Action SelectedColorChanged; public void SetPalette(IColorPalette palette); // resets CurrentColor to Colors[0] public void SelectColor(Color color); // called directly by ColorButton on click } ``` - **Why a repository:** `ColorButton` (palette taps) and `ColoringController` (paint command builder) both need to read/write the current color; an event-emitting POCO is simpler than wiring two signals. - **Color-keyed, not index-keyed:** with `ColorButton + Factory` (§25), each button owns its `Color` directly. Index-based addressing is unnecessary — the button passes its color straight to `SelectColor(color)`. #### `ColoringController` *(Systems — planned)* — implements `IColoringController` Wires pre-authored `ColorRegionView` children, applies saved colors on resume, builds and pushes `PaintRegionCommand` instances on click. ```csharp // fields: IUndoStack _undo, ColoringStateRepository _state, IGameplaySceneRefs _refs, IEventBus _bus public interface IColoringController { // Initialize regions on the paper after the drawing prefab is instantiated. // Pass non-null savedColors to restore colors from a DrawingProgress record; // null = use ColorRegionDTO.InitialColor. UniTask InitializeRegionsAsync( IDrawingTemplate template, IReadOnlyDictionary savedColors = null); void PaintRegion(ColorRegionView view); // builds command, pushes to undo stack // Snapshot current paint state for save records (see §13). IReadOnlyDictionary GetCurrentColors(); void Clear(); // detach handlers } // sub: ShapeAssembledSignal (via flow controller, not direct) // pub: ColorAppliedSignal (via PaintRegionCommand) ``` Regions are pre-authored as children of the drawing prefab (parented under `PaperRoot`) — the controller does not spawn them. Each `ColorRegionView` sets its own `Image.alphaHitTestMinimumThreshold` in `Awake`; the controller subscribes to each view's click event and routes to `PaintRegion`. No `Physics2D`. **Autosave integration:** after each successful `PaintRegion`, the controller calls a debounced `GameplayFlowController.ScheduleAutosave()` so the flow can write the new color state to `IProgressionSystem` 500 ms later (no thumbnail, cheap). The flow controller cancels and resets the timer on each paint — only the last paint in a burst triggers the write. #### `ColorRegionView : MonoBehaviour, IPointerClickHandler` *(UI — ✅ exists)* UI Image with alpha-based hit detection. Tap routes through Unity's EventSystem directly to `OnPointerClick`. ```csharp [RequireComponent(typeof(Image))] public sealed class ColorRegionView : MonoBehaviour, IPointerClickHandler { [field: SerializeField] public string RegionId { get; private set; } [SerializeField, Range(0f, 1f)] private float alphaHitThreshold = 0.01f; public Color Color => _image.color; private void Awake() { _image = GetComponent(); _image.alphaHitTestMinimumThreshold = alphaHitThreshold; // pass-through transparent pixels } public void Initialize(string id, Color color); public void SetColor(Color color); public void OnPointerClick(PointerEventData e); // routes to ColoringController.PaintRegion(this) } ``` - **Required sprite import:** **Read/Write Enabled = on**, **Mesh Type = Full Rect**, keep alpha channel (RGBA32 or Automatic-with-alpha). Without these, Unity throws `UnityException: Texture is not readable` on first hover/click. - **Threshold tuning:** default `0.01f` is very permissive (any non-fully-transparent pixel counts). Raise toward `0.5f` for tighter hits when regions overlap visually. - **Sibling order matters** for stacked regions — top sibling gets first crack at the click; with alpha hit-test, transparent areas defer correctly to siblings below. No `ColoringInputBinder` class needed. Unity's EventSystem fires `OnPointerClick` on the topmost UI element under the pointer whose `Image.alphaHitTestMinimumThreshold` is met. #### `PaintRegionCommand` *(Commands)* Source in section 23. Holds `view`, `fromColor`, `toColor`, `bus`. Symmetrical execute/undo. #### `ColorButton` + `IColorButtonFactory` / `ColorButtonFactory` *(UI + Systems — planned)* Replaces the View/Presenter pair for the palette. Each color is its own self-contained MB with click handler; factory instantiates the prefab under `ColorPaletteHolderView.SpawnRoot` and runs `Setup(color, state, sfx)`. Button writes `_state.SelectColor(_color)` on click. See §25 for the full pattern and rationale. The stale `ColorPaletteView.cs` / `ColorPalettePresenter.cs` stubs should be deleted when this lands. #### `ColorPaletteHolderView` / `ColorPaletteHolderPresenter` *(UI — ✅ exists)* View owns the palette container `RectTransform` (`SpawnRoot`) + slide/fade show-hide animation (same `Tween.UIAnchoredPosition` + `Tween.Alpha` pattern as `ShapeHolderView`, default hide off-screen right). Presenter subscribes `ShapeAssembledSignal` → `view.Show()`. Hide is invoked externally (scope tear-down or future phase-exit signal). #### Removed / not needed - **`ColorRegionFactory`** — regions are pre-authored as children of the drawing prefab. The controller wires existing views; it doesn't spawn anything. - **`ColorPaletteView` + `ColorPalettePresenter`** — replaced by self-contained `ColorButton` + `ColorButtonFactory`. The stub files at `Features/Coloring/UI/ColorPaletteView.cs` and `ColorPalettePresenter.cs` should be deleted; see §25 for rationale. --- ### 32.7 Feature — `History` *(planned)* #### `HistoryController` *(Systems)* — `IStartable, IDisposable` Owns the per-session `IUndoStack` (registered scoped, so a new ColorBook scene = new stack). ```csharp // fields: IUndoStack _stack, IEventBus _bus public sealed class HistoryController : IStartable, IDisposable { public bool CanUndo => _stack.CanUndo; public bool CanRedo => _stack.CanRedo; public event Action StateChanged; public void Undo(); // _stack.Undo() + StateChanged public void Redo(); // sub: DrawingSelectedSignal → _stack.Clear() } ``` #### `HistoryButtonsView : MonoBehaviour` *(UI)* Two big arrow buttons. Setters only. ```csharp public interface IHistoryButtonsView { event Action UndoClicked; event Action RedoClicked; void SetUndoEnabled(bool enabled); void SetRedoEnabled(bool enabled); } ``` #### `HistoryPresenter` *(UI)* Wires controller `StateChanged` ↔ view enable/disable; view click events → controller. --- ### 32.8 Feature — `Capture` *(planned)* #### `CaptureController` *(Systems)* The orchestrator behind the "Save" button. Owns the capture-then-save chain. Stateless other than guarding against concurrent captures. ```csharp // fields: ICaptureService _capture, IGalleryService _gallery, IEventBus _bus public sealed class CaptureController { public bool IsBusy { get; } public UniTask SaveAsync(string templateId); } // pub: PaperCapturedSignal (mid-flow), PaperSavedSignal (after native save) ``` - **Flow:** `_capture.CaptureAsync()` → publish `PaperCapturedSignal` → `_gallery.SaveToDeviceAsync(bytes)` → publish `PaperSavedSignal`. - **Concurrency:** sets `IsBusy = true` on entry; UI binds button enabled to `!IsBusy` to prevent double-tap. - **No camera args** — `ICaptureService` owns the `CaptureCamera` reference. - **No file-IO awareness** — `IGalleryService` handles the native plugin handoff. #### `CaptureButtonPresenter` *(UI)* Wires button click → `CaptureController.SaveAsync(currentTemplateId)`. Disables button while `IsBusy`. Shows a "Saved to Photos" toast on `PaperSavedSignal`. --- ### 32.9 Feature — `Progression` *(planned)* #### `ProgressionService` *(Systems)* — implements `IProgressionService` The only place that knows what "completed" means. - **Persistence:** delegates to `ProtectedPlayerPrefs` (`Libs.PlayerPrefs`) under key `"progression"`. - **Load order:** `AppBoot` calls `LoadAsync()` early. - **Save trigger:** after `MarkCompleted`, debounced 500 ms to coalesce a burst of "Next" presses. #### `ProgressionRepository` *(Repository)* Pure in-memory holder used by the service. Separated so tests can inspect state without going through file IO. --- ### 32.10 Feature — `ColorBookFlow` *(planned)* #### `ColorBookFlowController` *(Systems)* — `IStartable, IDisposable` **The only orchestrator inside the ColorBook scene.** Drives the panel FSM: `Catalog → Building → Coloring → Done`. ```csharp // fields: // IEventBus _bus // IDrawingTemplateCatalog _catalog // ShapeBuilderController _builder // IColoringController _coloring // CaptureController _capture // IProgressionService _progression // IGameplaySceneRefs _refs (PaperRoot only — panel show/hide owned by holder views) // Fsm _fsm ``` - **State table:** | State | On enter | Triggers exit | |------------|-----------------------------------------------|--------------------------------| | `Catalog` | Show catalog panel | `DrawingSelectedSignal` | | `Building` | `_builder.BuildAsync(id)` | `ShapeAssembledSignal` | | `Coloring` | `_coloring.SpawnRegionsAsync(template)` | "Next" or "Capture" pressed | | `Done` | Run autosave capture, mark completed, `Go(Catalog)` for next | always advances | - **"Next" sequence:** `_capture.CaptureCurrentAsync` → `_progression.MarkCompleted` → `_catalog.Release(current)` → `_catalog.LoadAsync(_catalog.NextUnseen(current))` → re-enter `Building`. --- ### 32.12 App Layer #### `AppBoot` *(App/Boot — planned; folder doesn't exist yet)* — `IAsyncStartable` Single entry point. Steps in section 29. ```csharp // fields: IAssetProviderService _assets, IProgressionService _progress, // IAudioService _audio, ISceneService _scenes, BootConfig _cfg public sealed class AppBoot : IAsyncStartable { public UniTask StartAsync(CancellationToken ct); } ``` #### LifetimeScopes - `RootLifetimeScope` — ✅ exists ([source](Assets/Darkmatter/Code/App/LifetimeScopes/RootLifetimeScope.cs)). Iterates a serialized `MonoBehaviour[] serviceModules` and calls `Register` on each `IServiceModule`. Persists for app lifetime. - `MainMenuLifetimeScope` — planned. Same pattern as Root (serialized installer list, no hardcoded registrations). - `GameplayLifetimeScope` — planned. Same pattern; installer list includes feature installers + the flow controller installer. Also has a `[SerializeField] GameplaySceneRefs _sceneRefs;` and registers it via `builder.RegisterComponent(_sceneRefs)`. All scope classes are thin: a serialized installer-MonoBehaviour list (+ optional scene refs as separate fields) and a `Configure(IContainerBuilder)` that iterates and calls `Register`. --- ### 32.13 Cross-cutting types #### `GameplaySceneRefs : MonoBehaviour, IGameplaySceneRefs` *(Features/GameplayFlow/SceneRefs — ✅ exists)* Single source of the one cross-feature scene anchor (`PaperRoot`). Per-feature UI containers (tray, palette) live on their own holder views — see §32.X. Registered in `GameplayLifetimeScope` via `builder.RegisterComponent(_sceneRefs)`. ```csharp public sealed class GameplaySceneRefs : MonoBehaviour, IGameplaySceneRefs { [SerializeField] private RectTransform paperRoot; public RectTransform PaperRoot => paperRoot; } ``` Replaces the dropped `IPaperSurface` contract. The drawing prefab (with `SlotMarker` + `ColorRegionView` children) is instantiated under `PaperRoot`; the `CaptureCamera` will live on its own scene MB owned by the Capture feature. #### `ShapeHolderView : MonoBehaviour` + `ShapeHolderPresenter` *(Features/ShapeBuilder/UI — ✅ exists)* Owns the tray container `RectTransform` (`SpawnRoot`) and the slide/fade show/hide animation (PrimeTween — `Tween.UIAnchoredPosition` + `Tween.Alpha` on `CanvasGroup`). Default hide offset is `+1500` on X (slide right off-screen), tunable per inspector. Exposes `SpawnRoot`, `SpawnWidth`, `Show()`, `Hide()`, `HideInstant()`. Presenter is an `IStartable` that subscribes: - `ShapeBuilderStartedSignal` → `view.Show()` - `ShapeAssembledSignal` → `view.Hide()` Registered in `ShapeBuilderFeatureModule` via `builder.RegisterComponent(holderView)` + `builder.RegisterEntryPoint().WithParameter(holderView)`. #### `ColorPaletteHolderView : MonoBehaviour` + `ColorPaletteHolderPresenter` *(Features/Coloring/UI — ✅ exists)* Same pattern as `ShapeHolderView` — owns the palette container `RectTransform` (`SpawnRoot`) and slide/fade animation. Presenter subscribes to `ShapeAssembledSignal` → `view.Show()`. Hide is invoked externally (scope tear-down or future phase-exit signal). Registered analogously in `ColoringFeatureModule`. #### `IShapePieceFactory` / `ShapePieceFactory` *(Features/ShapeBuilder/Systems — ✅ exists)* Encapsulates piece instantiation. Owns the per-piece dependencies and the spawn parent so the controller stays focused on flow. ```csharp public interface IShapePieceFactory { ShapePiece Create(GameObject prefab, ShapeSO shape, SlotMarker slot, Vector2 trayPos, bool preSnapped); } // fields: ShapeHolderView _holder, ShapeBuilderConfig _cfg, ISfxPlayer _sfx, IEventBus _bus, IUndoStack _undo public sealed class ShapePieceFactory : IShapePieceFactory { /* Instantiate under _holder.SpawnRoot, run piece.Setup(...) */ } ``` #### `IServiceModule` *(Libs/Installers — ✅ exists)* ```csharp public interface IServiceModule { void Register(IContainerBuilder builder); } ``` Implemented as `MonoBehaviour` per feature/service so scopes can drag them in the inspector ([CameraServiceModule.cs](Assets/Darkmatter/Code/Services/Camera/Installers/CameraServiceModule.cs) shows the pattern). The method is `Register`, not `Install` — there is no `IInstaller` in this project. --- ### 32.14 Class summary table | Class | Layer | Role | Key dependencies | |---|---|---|---| | `AppBoot` | App | Startup sequencer | assets, progression, audio, scenes | | `RootLifetimeScope` | App | Root DI | service modules | | `MainMenuLifetimeScope` | App | Menu scene DI | feature modules | | `ColorbookLifetimeScope` | App | Catalog scene DI | feature modules | | `GameplayLifetimeScope` | App | Active drawing scene DI | scene refs, feature modules | | `GameplaySceneRefs` | App | Scene-bound RectTransform + CaptureCamera holder | — | | `MenuMascotView` | Feature.MainMenu | Spine mascot UI (`SkeletonGraphic` wrapper) | — | | `MenuMascotPresenter` | Feature.MainMenu | Drives mascot animations from model events | view, model | | `DrawingCatalogController` | Feature.DrawingCatalog | Visible-ID list + selection signal | catalog, progression, bus | | `DrawingCatalogPresenter` | Feature.DrawingCatalog | UI bridge | view, controller, catalog, progression | | `DrawingCatalogView` | Feature.DrawingCatalog | UI; renders cells | — | | `CatalogItemVM` | Feature.DrawingCatalog | View-model per cell | — | | `AddressableDrawingTemplateCatalog` | Feature.DrawingTemplate | Loads `DrawingTemplateSO`s, exposes `NextUnseen` | assets, progression | | `ShapeSO` | Core asset | Authored shape (id + sprite + DefaultSizeDelta) | — | | `ShapeBuilderConfig` | Core asset | Tunables (radii, durations, curve) | — | | `ShapePiece` | Feature.ShapeBuilder | Draggable piece MB (drag + preview lerp + snap + return) | shape, slot, cfg, sfx, bus | | `SlotMarker` | Feature.ShapeBuilder | Slot anchor MB; `RectTransform` == target pose | — | | `ShapeBuilderController` | Feature.ShapeBuilder | Orchestrates per-template build, delegates spawn to factory, tracks snap count | bus, assets, factory, holder, refs | | `IShapePieceFactory` / `ShapePieceFactory` | Feature.ShapeBuilder | Instantiates a `ShapePiece` under `ShapeHolderView.SpawnRoot` + runs `Setup` | holder, cfg, sfx, bus, undo | | `ShapeHolderView` / `ShapeHolderPresenter` | Feature.ShapeBuilder | Tray container + slide/fade show/hide; presenter reacts to `ShapeBuilderStartedSignal` / `ShapeAssembledSignal` | bus | | `ColoringStateRepository` | Feature.Coloring | Current color model | — | | `ColoringController` | Feature.Coloring | Wires pre-authored regions + paint cmd + autosave hook | undo, state, refs, flow, bus | | `ColorPaletteHolderView` / `ColorPaletteHolderPresenter` | Feature.Coloring | Palette container + slide/fade show/hide; presenter reacts to `ShapeAssembledSignal` | bus | | `ColorButton` | Feature.Coloring | Self-contained palette swatch MB; click writes to repository (no view/presenter pair) | state, sfx | | `IColorButtonFactory` / `ColorButtonFactory` | Feature.Coloring | Instantiates a `ColorButton` under `ColorPaletteHolderView.SpawnRoot` + runs `Setup` | holder, prefab, state, sfx | | `ColorRegionView` | Feature.Coloring | Region UI Image + `IPointerClickHandler` | controller | | `PaintRegionCommand` | Feature.Coloring | Undoable paint (sets `Image.color`) | view, bus | | `HistoryController` | Feature.History | Undo/redo facade | undo stack, bus | | `UndoStack` | Feature.History | Bounded undo store | — | | `CaptureController` | Feature.Capture | (light wrapper) calls `ICaptureService.CaptureAsync` | capture svc | | `ColorbookFlowController` | Feature.ColorbookFlow | Catalog scene orchestrator (init + selection→scene-load) | catalog, progression, scenes, bus | | `GameplayFlowController` | Feature.GameplayFlow | Active drawing FSM + **single owner of all saves** (see §13) | catalog, builder, coloring, capture, gallery, progression, scenes, bus | | `ProgressionSystem` | Feature.Progression | Per-template state + completed view | repository | | `ProgressionRepository` | Feature.Progression | PlayerPrefs JSON + thumbnail file IO | — | | `EventBus` | Lib.Observer | Pub/sub | — | | `StateMachine` (abstract) + `State` | Lib.FSM | Generic FSM base (Enter/Tick/Exit, ChangeState) | — | | `IModule` | Lib.Installers | DI installer interface | — | | `ProtectedPlayerPrefs` | Lib.PlayerPrefs | Encrypted PlayerPrefs wrapper | — | | `AddressableAssetProviderService` | Service.Assets | Addressables wrapper | — | | `CaptureService` | Service.Capture | One-shot PNG render via `CaptureCamera` | refs | | `NativeGallerySaveService` | Service.Gallery | Native gallery save (thin plugin shim) | — | | `SceneService` | Service.Scenes | Async scene loads | — | | `AudioService`, `SfxPlayer` | Service.Audio | SFX playback | assets | | `CameraService` | Service.Camera | Camera registry | — | | `InputReaderSO` | Service.Inputs | New Input System reader | — | | `FirebaseAnalyticsSystem` | Service.Analytics | Analytics events | — | If you add a class not in this table, add it here in the same PR. This table is the cheap mental-model index — keep it honest. See §31b for the full path-by-path inventory. > **What's real on disk today (2026-05):** All Service classes (`AddressableAssetProviderService`, `AudioService`/`SfxPlayer`, `CameraService`, `SceneService`, `InputReaderSO`, `FirebaseAnalyticsSystem`, stub `CaptureService`, stub `GalleryService`), all Lib classes, `ShapePiece` + `SlotMarker` + `ShapeBuilderConfig`, `UndoStack` + `HistoryServiceModule`, `ProgressionSystem` + `ProgressionRepository` (stubs), `AddressableDrawingTemplateCatalog` + module, `DrawingCatalogController` + presenter + view, `ColorbookFlowController` (partial — needs constructor injection wired). Empty / planned: `MainMenu` feature, `GameplayFlow` feature, `Coloring` feature, `MainMenu`/`Colorbook`/`Gameplay` scene scopes, all scenes except `Boot.unity`.