# 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 **hybrid Sprites + Canvas** 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 ``` App launch └─ Boot scene (RootLifetimeScope) └─ MainMenu scene ├─ Press "Play" → ColorBook scene │ ├─ Drawing catalog (grid of templates) │ ├─ Select drawing │ ├─ Shape Builder panel (drag pieces → snap to slots) │ ├─ ↓ on assembly complete │ ├─ Color panel (tap color → tap region) │ ├─ Undo / Redo any time │ ├─ "Capture" → save to Gallery with paper background │ └─ "Next" → auto-save + load next drawing └─ Press "Art Book" → ArtBook scene (gallery viewer) ├─ Grid of saved artworks ├─ View / share / delete └─ Save to device camera roll ``` --- ## 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 ``` Assets/Darkmatter/Code/ ├── App/ │ ├── Boot/ │ │ └── AppBoot.cs │ └── LifetimeScopes/ │ ├── RootLifetimeScope.cs │ ├── MainMenuLifetimeScope.cs │ ├── ColorBookLifetimeScope.cs │ └── ArtBookLifetimeScope.cs │ ├── Core/ │ ├── Drawing/ │ │ ├── IDrawingTemplate.cs │ │ ├── IDrawingTemplateCatalog.cs │ │ ├── ShapePieceDTO.cs │ │ └── ColorRegionDTO.cs │ ├── Coloring/ │ │ ├── IColorPalette.cs │ │ └── PaintCommandDTO.cs │ ├── Paper/ (shared art rig — RT-as-paper) │ │ ├── IPaperRig.cs │ │ └── IArtInputBridge.cs │ ├── History/ │ │ ├── ICommand.cs │ │ └── IUndoStack.cs │ ├── Gallery/ │ │ ├── IGalleryService.cs │ │ └── SavedArtworkDTO.cs │ ├── Capture/ │ │ └── ICaptureService.cs │ ├── Progression/ │ │ └── IProgressionService.cs │ └── Signals/ │ ├── DrawingSelectedSignal.cs │ ├── ShapeAssembledSignal.cs │ ├── ColorAppliedSignal.cs │ ├── ArtworkCapturedSignal.cs │ └── ArtworkSavedSignal.cs │ ├── Libs/ │ ├── CommandStack/ (generic bounded undo/redo) │ ├── EventBus/ (shared with bus game if monorepo) │ └── FSM/ (optional, for ColorBookFlow) │ ├── Services/ │ ├── Audio/ │ ├── Inputs/ │ ├── Assets/ (Addressables wrapper — IAssetProviderService) │ ├── Scenes/ │ ├── Persistence/ (JSON / PlayerPrefs for non-image state) │ ├── Gallery/ (file IO — PNG + sidecar JSON) │ └── Capture/ (RenderTexture → PNG, paper bg compositing) │ └── Features/ ├── MainMenu/ ├── DrawingCatalog/ ├── Paper/ (RT paper rig — ArtCamera + RenderTexture + input bridge) ├── ShapeBuilder/ ├── Coloring/ ├── History/ ├── Capture/ ├── Progression/ ├── ColorBookFlow/ (orchestrates panel swap, next, capture chain) └── ArtBook/ ``` ### Per-feature folder layout Every feature follows the same internal shape: ``` Features/[Name]/ ├── Installers/ IInstaller — VContainer registrations ├── Systems/ Controllers, services (pure C#) ├── Repository/ In-memory state holders ├── Commands/ ICommand implementations (if feature mutates undoable state) ├── UI/ │ ├── *Presenter.cs Pure C#, listens to model, drives view │ └── *View.cs MonoBehaviour, setters only ├── Views/ World-space MonoBehaviours (sprites, colliders) └── Docs/ Feature-specific markdown ``` ### Asset folder parallel ``` Assets/Darkmatter/Contents/ ├── Drawings/ │ ├── Animals// │ │ ├── Template.asset (DrawingTemplateSO) │ │ ├── Pieces/*.png │ │ ├── Regions/*.png │ │ └── PaperBackground.png │ └── Vehicles/... ├── Palettes/*.asset (ColorPaletteSO) ├── Audio/ │ ├── UI/ (tap, swipe, button) │ └── Coloring/ (fill, complete, sparkle) └── UI/ (HUD prefabs, fonts, icons) ``` --- ## 5. Namespaces `Darkmatter.[Layer].[Module]` - `Darkmatter.Features.Coloring` - `Darkmatter.Features.ShapeBuilder` - `Darkmatter.Services.Gallery` - `Darkmatter.Services.Capture` - `Darkmatter.Core.Drawing` - `Darkmatter.Lib.CommandStack` Each maps 1:1 to a `.asmdef`. --- ## 6. Scenes & Lifetime Scopes | Scene | Scope | Contents | |---|---|---| | `Boot.unity` | `RootLifetimeScope` | All Services + `IEventBus`. Persists forever. | | `MainMenu.unity` | `MainMenuLifetimeScope` | Menu presenter, art book entry. | | `ColorBook.unity` | `ColorBookLifetimeScope` | DrawingCatalog, ShapeBuilder, Coloring, History, Capture, ColorBookFlow. | | `ArtBook.unity` | `ArtBookLifetimeScope` | Gallery presenter, viewer, share. | Scopes nest: `Root → (MainMenu | ColorBook | ArtBook)`. Services resolved from the root parent. Scene scopes only register their own features. ### Boot chain `AppBoot` runs once, in order: 1. Initialize `IAssetProviderService` (Addressables init). 2. Preload essential bundles (palettes, UI sounds). 3. Load `IProgressionService` from disk. 4. Load `MainMenu` scene. Failures show a child-friendly retry screen; never crash. --- ## 7. Rendering Strategy **RT-as-paper.** ArtCamera renders the drawing world to an offscreen `RenderTexture`. A Canvas `RawImage` displays that RT. HUD lives on the same Canvas, above the RawImage. The RT *is* the paper — same fixed coordinate system on every device. ``` ┌──────────────────────────────────────────────────────┐ │ UICanvas (Screen-Space - Camera, UICamera) │ │ │ │ ┌────────────────────────────────────┐ │ │ │ RawImage (AspectRatioFitter 1:1) │ [HUD] │ │ │ └─ texture = PaperRig.Surface │ palette │ │ │ │ undo etc │ │ │ ArtCamera renders → here │ │ │ └────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────┘ ▲ │ rendered offscreen │ ArtCamera (orthographicSize fixed, aspect = 1f) culling mask: Artwork, PaperBackground, Effects target texture: PaperRig.Surface (2048×2048 ARGB32) ``` ### Cameras | Camera | Type | Culling Mask | Render Target | Purpose | |---|---|---|---|---| | `ArtCamera` | Orthographic, **fixed ortho size**, aspect = 1 | `Artwork`, `PaperBackground`, `Effects` | `PaperRig.Surface` (offscreen RT) | Renders the drawing world. Never sees the screen. | | `UICamera` | Camera (Screen-Space – Camera) | `UI` | Screen | Displays the paper RawImage + HUD. | ### Layers | Layer | Used by | |---|---| | `Artwork` | Drawing region sprites, shape pieces, paper bg, all in ArtCamera world | | `Effects` | Particle bursts, sparkles — also in ArtCamera world (so they're captured into the PNG) | | `UI` | All Canvas elements (RawImage paper + HUD) | ### Why RT-as-paper | Need | Choice | Why | |---|---|---| | Per-region tap-to-fill | Sprites + `PolygonCollider2D` in ArtCamera world; tapped via `IArtInputBridge` | Coordinate system is fixed (RT space). One `Physics2D.OverlapPoint` call after screen→art-world conversion. | | Drag/drop shape pieces | Sprites + Physics2D in art world | Same fixed bounds on every device — no per-aspect tray layout. | | Capture to PNG | `RT → Texture2D → PNG` | The RT *is* the saved image. No camera state override, no compositing pass, no determinism worries. | | Multi-resolution support | `AspectRatioFitter (1:1, FitInParent)` on the RawImage | The "fit camera" problem reduces to a single Canvas property. Letterbox/pillarbox = whatever the Canvas around the RawImage looks like. | | Color palette, buttons | Canvas above the RawImage | Anchors handle aspect ratios. Buttons + ScrollRect free. | | Drawing catalog grid | Canvas | `GridLayoutGroup` + ScrollRect, async thumbnail loader. | ### Multi-resolution rule The artwork world is **screen-size-independent by construction.** Author every drawing in a fixed 2048×2048 design rect (or 20×20 world units at PPU=100). Pieces, regions, snap radii, slot positions — all expressed in this space and never scaled at runtime. Different screen sizes only change how the *RawImage* is laid out on the Canvas; the contents of the RT stay identical. If you need a backdrop (wood/cloth behind the paper), it's a sibling Canvas Image *outside* the RawImage, sized to fill the screen. The RT itself has a transparent or paper-colored background. --- ## 8. Core Contracts All Core types are pure data or interfaces. ### Drawing ```csharp namespace Darkmatter.Core.Drawing; public interface IDrawingTemplate { string Id { get; } string DisplayName { get; } Sprite Thumbnail { get; } Sprite PaperBackground { get; } IReadOnlyList Pieces { get; } IReadOnlyList Regions { get; } } public readonly struct ShapePieceDTO { public string PieceId { get; } public Sprite Sprite { get; } public Vector2 SlotPosition { get; } public float SlotRotation { get; } public float SnapRadius { get; } // generous for toddlers } public readonly struct ColorRegionDTO { public string RegionId { get; } public Sprite Sprite { get; } // sprite renderer source public Vector2[] ColliderPath { get; } // polygon collider points public Color InitialColor { get; } // usually white } ``` ### Coloring ```csharp namespace Darkmatter.Core.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; } } ``` ### Paper (RT rig + input bridge) ```csharp namespace Darkmatter.Core.Paper; public interface IPaperRig { Camera ArtCamera { get; } // offscreen, targetTexture = Surface RenderTexture Surface { get; } // 2048×2048 ARGB32; the paper itself Transform PaperRoot { get; } // parent of regions/pieces/paper bg Vector2 DesignSize { get; } // world units, e.g. (20, 20) Rect DesignRect { get; } // centered on origin, DesignSize wide } public interface IArtInputBridge { // Converts a screen-space pointer (Input System) to art-world coords // inside the RT. Returns false if the pointer is outside the RawImage. bool TryScreenToArtWorld(Vector2 screenPos, out Vector2 artWorldPos); } ``` - `IPaperRig` is implemented by `PaperRig : MonoBehaviour` in the ColorBook scene. - `IArtInputBridge` does the screen → RawImage local → UV → `ArtCamera.ViewportToWorldPoint` chain. - All consumers (Coloring, ShapeBuilder, Capture, particle effects) read these from DI; they never touch `Screen.width/height` directly. ### History ```csharp namespace Darkmatter.Core.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(); } ``` ### Gallery & Capture ```csharp namespace Darkmatter.Core.Gallery; public readonly struct SavedArtworkDTO { public string Id { get; } public string TemplateId { get; } public DateTime CreatedUtc { get; } public string ImagePath { get; } // persistentDataPath PNG public string ThumbnailPath { get; } } public interface IGalleryService { UniTask SaveAsync(byte[] png, string templateId); UniTask> ListAsync(); UniTask LoadFullAsync(string artworkId); UniTask DeleteAsync(string artworkId); } namespace Darkmatter.Core.Capture; public interface ICaptureService { // No camera or paperBg args — capture reads directly from IPaperRig.Surface. // Dimensions inherited from the RT; no resize, no compositing. UniTask CaptureAsync(); } ``` `ICaptureService` resolves `IPaperRig` via DI and reads `Surface` directly. The paper background is already baked into the RT because it sits in `PaperRoot` under the ArtCamera. No special compositing pass is ever needed. ### Signals ```csharp namespace Darkmatter.Core.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 ArtworkCapturedSignal { public string ArtworkId { get; } } public readonly struct ArtworkSavedSignal { public SavedArtworkDTO Artwork { get; } } ``` --- ## 9. Feature Responsibilities ### `DrawingCatalog` - Loads the catalog manifest (list of available template IDs + thumbnail addresses). - Presents a scrollable grid of thumbnails (Canvas). - On select → fires `DrawingSelectedSignal(templateId)` and unloads the catalog UI. ### `Paper` - Scene-scoped infrastructure. Lives in `ColorBook.unity` only. - Owns `PaperRig` (MonoBehaviour) — exposes `ArtCamera`, the `RenderTexture Surface`, `PaperRoot` transform, and the design rect. - Owns `ArtInputBridge` — converts pointer screen positions to art-world coords inside the RT. - Registered in `ColorBookLifetimeScope` via `PaperRigModule`. All other features in the scene resolve `IPaperRig` / `IArtInputBridge` from DI. - Lifetime is scene-scoped: created on scene load, destroyed on scene unload. RT is allocated in `Awake`, released in `OnDestroy`. ### `ShapeBuilder` - Listens to `DrawingSelectedSignal`. - Loads template via `IDrawingTemplateLoader`, parents shape pieces under `IPaperRig.PaperRoot` at off-slot positions inside the design rect. - Per piece: drag with `ShapePieceView` (sprite + collider). Pointer events go through `IArtInputBridge.TryScreenToArtWorld`. On drop, check distance to `SlotPosition` against `SnapRadius`; if within, snap and lock. - Fires `ShapeAssembledSignal` when all pieces locked. ### `Coloring` - Listens to `ShapeAssembledSignal`. - Spawns one `ColorRegionView` per `ColorRegionDTO` under `IPaperRig.PaperRoot` (sprite + polygon collider on `Artwork` layer). - Listens to palette selection (current color held in `ColoringStateRepository`). - On pointer down: `IArtInputBridge.TryScreenToArtWorld(screenPos, out var artPos)` → `Physics2D.OverlapPoint(artPos, artworkMask)` → if hit, build `PaintRegionCommand(regionId, oldColor, newColor)`, push to `IUndoStack`. - Command sets `SpriteRenderer.color` on undo/redo. - Fires `ColorAppliedSignal` for SFX / sparkle effects. ### `History` - Owns the singleton `IUndoStack` for the current ColorBook session. - Cleared on `DrawingSelectedSignal` (new drawing = fresh history). - Capped at ~20 entries (memory + cognitive simplicity). - UI: two big arrow buttons; disabled state when `CanUndo / CanRedo` is false. ### `Capture` - Bound to the "Capture" button. - Calls `ICaptureService.CaptureAsync(artCamera, template.PaperBackground)` → PNG bytes. - Hands bytes to `IGalleryService.SaveAsync(...)`. - Fires `ArtworkCapturedSignal` then `ArtworkSavedSignal`. - Shows a quick "saved!" toast with a thumbnail of the new entry. ### `Progression` - Tracks completed template IDs and the in-progress draft. - On "Next" button: silently runs Capture pipeline (auto-save), marks current as completed, calls `IDrawingTemplateCatalog.NextUnseen()`. - Persists JSON via `IPersistenceService`. ### `ColorBookFlow` - The only orchestrator inside ColorBook scope. - Subscribes to flow-relevant signals and toggles UI panels (catalog → builder → coloring). - Coordinates "Next" sequence: `IProgressionService.MarkCompleted` → `ICaptureService` autosave → `IDrawingTemplateLoader.Release(currentId)` → load next. - Built as a small FSM (`Catalog → Building → Coloring → Done`). ### `ArtBook` - Separate scene. - `GalleryPresenter` calls `IGalleryService.ListAsync()` → grid of thumbnails. - Tap → fullscreen view, share-sheet button, delete. - Saved-to-device-camera-roll uses an optional platform plugin behind `IExternalShareService` (Core contract). --- ## 10. Addressables Strategy Mirror the Bus Game pattern via `IAssetProviderService`. ### What ships through Addressables | Asset | Why | |---|---| | `DrawingTemplate` ScriptableObject (per drawing) | Many; load on demand. | | Shape piece sprites | Only needed when active. | | Region sprites + polygon paths | 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. | ### 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) Palettes (label: palette) Audio_UI (label: sfx, ui) Audio_Coloring (label: sfx, coloring) ``` ### 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 Two distinct stores, each behind its own Core contract. ### `IPersistenceService` (JSON / PlayerPrefs) Holds: - Completed template IDs. - Last opened drawing. - Audio volume, simple settings. Path: `Application.persistentDataPath/save.json`. ### `IGalleryService` (file IO) Holds user artworks: ``` persistentDataPath/Gallery/ ├── {guid}.png full-res render (~2048×2048) ├── {guid}.thumb.png 256×256 for grid └── {guid}.json SavedArtworkDTO sidecar ``` - Writes are atomic (`.tmp` → rename). - `ListAsync` enumerates sidecar JSONs sorted by `CreatedUtc desc`. - Thumbnail generation happens once at save time on a worker thread. --- ## 12. Capture Pipeline ``` [Capture button or Next button] │ ▼ ICaptureService.CaptureAsync(artCamera, paperBg) │ ├─ Allocate RenderTexture (2048×2048, ARGB32) ├─ artCamera.targetTexture = rt ├─ Force render (artCamera.Render()) ├─ ReadPixels into Texture2D ├─ Composite paperBg underneath (single shader pass or CPU blend) ├─ Encode PNG (Texture2D.EncodeToPNG) ├─ Release RT + temp texture └─ return byte[] ▼ IGalleryService.SaveAsync(bytes, templateId) │ ├─ Write .png atomically ├─ Generate + write thumbnail ├─ Write sidecar JSON └─ return SavedArtworkDTO ▼ EventBus.Publish(new ArtworkSavedSignal(dto)) ``` Notes: - HUD never appears in capture because `ArtCamera` only renders the `Artwork` layer. - Paper background can either be already present in the scene (cheap) or composited at capture time (lets the same drawing be saved with different papers). --- ## 13. Communication Rules | Use case | Mechanism | |---|---| | Load template, return result | Direct DI call (`IDrawingTemplateLoader.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 px; snap radius ≥ 60 px for shape pieces. - **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, but for `SavedArtworkDTO` serialization, yes. | | `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 | |---|---|---| | `DrawingCatalog` | — | `DrawingSelectedSignal` | | `ShapeBuilder` | `DrawingSelectedSignal` | `ShapeAssembledSignal` | | `Coloring` | `ShapeAssembledSignal` | `ColorAppliedSignal` | | `History` | `DrawingSelectedSignal` (to clear) | — | | `Capture` | — (button-driven) | `ArtworkCapturedSignal`, `ArtworkSavedSignal` | | `Progression` | `ArtworkSavedSignal` | — | | `ColorBookFlow` | `ShapeAssembledSignal`, `ArtworkSavedSignal` | — | | `ArtBook (Gallery)` | `ArtworkSavedSignal` (if open) | — | --- Maintained alongside the [Darkmatter Architecture Guide](../Assets/Darkmatter_Architecture_Guide.md). Do not break the dependency arrows. --- ## 20. Assembly Definition Map Every folder under `Code/` is its own `.asmdef`. References follow the layer rules exactly. | Asmdef | Path | References | |---|---|---| | `Darkmatter.App` | `App/` | All Features, all Services, Core, Libs | | `Darkmatter.Core` | `Core/` | (none — `UniTask` allowed in async signatures) | | `Darkmatter.Lib.CommandStack` | `Libs/CommandStack/` | `Darkmatter.Core` | | `Darkmatter.Lib.EventBus` | `Libs/EventBus/` | `Darkmatter.Core` | | `Darkmatter.Lib.FSM` | `Libs/FSM/` | `Darkmatter.Core` | | `Darkmatter.Services.Audio` | `Services/Audio/` | `Darkmatter.Core` | | `Darkmatter.Services.Inputs` | `Services/Inputs/` | `Darkmatter.Core` | | `Darkmatter.Services.Assets` | `Services/Assets/` | `Darkmatter.Core` | | `Darkmatter.Services.Scenes` | `Services/Scenes/` | `Darkmatter.Core` | | `Darkmatter.Services.Persistence` | `Services/Persistence/` | `Darkmatter.Core` | | `Darkmatter.Services.Gallery` | `Services/Gallery/` | `Darkmatter.Core` | | `Darkmatter.Services.Capture` | `Services/Capture/` | `Darkmatter.Core` | | `Darkmatter.Features.MainMenu` | `Features/MainMenu/` | `Darkmatter.Core`, Libs | | `Darkmatter.Features.DrawingCatalog` | `Features/DrawingCatalog/` | `Darkmatter.Core`, Libs | | `Darkmatter.Features.ShapeBuilder` | `Features/ShapeBuilder/` | `Darkmatter.Core`, Libs | | `Darkmatter.Features.Coloring` | `Features/Coloring/` | `Darkmatter.Core`, `Lib.CommandStack` | | `Darkmatter.Features.History` | `Features/History/` | `Darkmatter.Core`, `Lib.CommandStack` | | `Darkmatter.Features.Capture` | `Features/Capture/` | `Darkmatter.Core` | | `Darkmatter.Features.Progression` | `Features/Progression/` | `Darkmatter.Core` | | `Darkmatter.Features.ColorBookFlow` | `Features/ColorBookFlow/` | `Darkmatter.Core`, `Lib.FSM` | | `Darkmatter.Features.ArtBook` | `Features/ArtBook/` | `Darkmatter.Core` | **Hard rule:** No Service asmdef references any Feature asmdef. No Feature asmdef references another Feature asmdef. Compiler enforces the architecture. --- ## 21. LifetimeScope Concrete Sample ### `RootLifetimeScope` (Boot scene, persists forever) ```csharp namespace Darkmatter.App.LifetimeScopes; public sealed class RootLifetimeScope : LifetimeScope { [SerializeField] private AudioServiceConfig _audioConfig; [SerializeField] private InputReaderSO _inputReader; protected override void Configure(IContainerBuilder builder) { // EventBus builder.Register(Lifetime.Singleton); // Services builder.RegisterInstance(_inputReader).As(); builder.Register(Lifetime.Singleton) .WithParameter(_audioConfig); builder.Register(Lifetime.Singleton); builder.Register(Lifetime.Singleton); builder.Register(Lifetime.Singleton); builder.Register(Lifetime.Singleton); builder.Register(Lifetime.Singleton); // App entry builder.RegisterEntryPoint(); } } ``` ### `ColorBookLifetimeScope` (per-scene, child of Root) ```csharp namespace Darkmatter.App.LifetimeScopes; public sealed class ColorBookLifetimeScope : LifetimeScope { [SerializeField] private ColorBookSceneRefs _sceneRefs; // ArtCamera, panel roots, prefabs [SerializeField] private IInstaller[] _installers; // assigned in inspector protected override void Configure(IContainerBuilder builder) { builder.RegisterInstance(_sceneRefs); // Each feature ships an IInstaller foreach (var installer in _installers) installer.Install(builder); // Scene-scoped orchestrator builder.RegisterEntryPoint(); } } ``` Drag these installers in the inspector: - `DrawingCatalogServiceModule` - `ShapeBuilderServiceModule` - `ColoringServiceModule` - `HistoryServiceModule` - `CaptureFeatureModule` - `ProgressionServiceModule` --- ## 22. Installer Pattern — Concrete Coloring Sample ```csharp namespace Darkmatter.Features.Coloring.Installers; [CreateAssetMenu(menuName = "Darkmatter/Installers/Coloring")] public sealed class ColoringServiceModule : ScriptableObject, IInstaller { [SerializeField] private ColoringConfig _config; public void Install(IContainerBuilder builder) { builder.RegisterInstance(_config); builder.Register(Lifetime.Scoped).AsSelf(); builder.Register(Lifetime.Scoped) .As() .AsSelf(); builder.Register(Lifetime.Scoped).AsSelf(); builder.RegisterEntryPoint(); } } ``` Convention: - One `IInstaller` per feature. - `ScriptableObject` so it can be referenced by scene scope inspector. - Registers only its own types. Never touches another feature's types. --- ## 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. View / Presenter Pair — Color Palette ### View (MonoBehaviour, setters only) ```csharp namespace Darkmatter.Features.Coloring.UI; public sealed class ColorPaletteView : MonoBehaviour, IColorPaletteView { [SerializeField, RequireInterface(typeof(IColorButtonView))] private MonoBehaviour[] _buttonsRaw; private IColorButtonView[] _buttons; public event Action OnColorButtonClicked; private void Awake() { _buttons = _buttonsRaw.Cast().ToArray(); for (var i = 0; i < _buttons.Length; i++) { var idx = i; _buttons[i].OnClicked += () => OnColorButtonClicked?.Invoke(idx); } } public void SetColors(IReadOnlyList colors) { for (var i = 0; i < _buttons.Length; i++) _buttons[i].SetVisible(i < colors.Count); for (var i = 0; i < colors.Count; i++) _buttons[i].SetColor(colors[i]); } public void SetSelected(int index) { for (var i = 0; i < _buttons.Length; i++) _buttons[i].SetSelected(i == index); } } ``` ### Presenter (pure C#) ```csharp namespace Darkmatter.Features.Coloring.UI; public sealed class ColorPalettePresenter : IStartable, IDisposable { private readonly IColorPaletteView _view; private readonly ColoringStateRepository _state; public ColorPalettePresenter(IColorPaletteView view, ColoringStateRepository state) { _view = view; _state = state; } public void Start() { _view.SetColors(_state.Palette.Colors); _view.SetSelected(_state.SelectedIndex); _view.OnColorButtonClicked += OnClicked; _state.SelectedIndexChanged += OnIndexChanged; } private void OnClicked(int index) => _state.SelectColor(index); private void OnIndexChanged(int index) => _view.SetSelected(index); public void Dispose() { _view.OnColorButtonClicked -= OnClicked; _state.SelectedIndexChanged -= OnIndexChanged; } } ``` Same shape repeats for every feature's UI. --- ## 26. ShapeBuilder — Snap Algorithm ```csharp // In ShapePieceView.OnPointerUp: public void OnDragEnd(Vector2 worldPos) { var slot = transform.position; // assigned target slot var d = Vector2.Distance(worldPos, slot); if (d <= _piece.SnapRadius) { SnapToSlot(); } else if (d <= _piece.SnapRadius * 1.5f) { // Toddler grace zone — snap anyway, play happy sound SnapToSlot(); _audio.PlayOneShot(_clips.NiceTry); } else { ReturnToTrayAnimated(); } } private void SnapToSlot() { _locked = true; transform.DOMove(_piece.SlotPosition, 0.25f).SetEase(Ease.OutBack); _audio.PlayOneShot(_clips.Snap); _bus.Publish(new PieceSnappedSignal(_piece.PieceId)); } ``` Controller listens for `PieceSnappedSignal`, counts against expected piece count, fires `ShapeAssembledSignal` when complete. --- ## 27. Rendering Order & Sorting URP 2D with a single `ArtCamera` ortho cam. | Sorting Layer | Order | Contents | |---|---|---| | `PaperBackground` | 0 | Paper bg sprite (under everything) | | `ArtworkRegions` | 100 | `ColorRegionView` sprites (the colorable shapes) | | `ArtworkPieces` | 200 | `ShapePieceView` sprites (during build) | | `Effects` | 300 | Particle bursts, sparkles | | `UIWorld` | 400 | World-space prompts (rare; mostly Canvas) | Canvas HUD lives on `UICamera` (Overlay), never sorts against `ArtCamera`. Capture renders only `ArtCamera`'s layers → HUD physically cannot leak into saved PNG. --- ## 28. SavedArtwork JSON Schema ```json { "id": "f3a8e2d4-...", "templateId": "animals/elephant", "createdUtc": "2026-05-26T16:42:11Z", "imagePath": "Gallery/f3a8e2d4-....png", "thumbnailPath": "Gallery/f3a8e2d4-....thumb.png", "regions": [ { "regionId": "body", "color": "#FFB347" }, { "regionId": "ears", "color": "#FF6961" } ] } ``` `regions[]` lets the gallery reopen an artwork for further edits in a future version (out of scope v1, but the schema reserves the field now to avoid migration later). Paths are **relative** to `persistentDataPath`. Never store absolute paths — they change between OS updates on some platforms. --- ## 29. Boot & Error Handling ``` AppBoot.StartAsync() ├─ 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 `Bus Game.sln` (color book lives in same repo / Unity project per plan). 2. Verify Addressables groups exist: `Drawings_*`, `Palettes`, `Audio_*`. 3. Open `Boot.unity` → confirm `RootLifetimeScope` references the right configs. 4. Open `ColorBook.unity` → confirm `ColorBookLifetimeScope._installers[]` is fully populated. 5. Hit Play from `Boot.unity` (entry scene). Never start mid-flow — DI parent scope must exist. 6. To author a new drawing: duplicate `Animals/elephant/`, edit `Template.asset` (pieces + regions), add to the appropriate Addressables group. 7. Run `Tests > EditMode` and `Tests > PlayMode` before pushing. --- ## 31. Quick Reference — Class ↔ Layer ↔ Asmdef | Class | Layer | Asmdef | |---|---|---| | `IDrawingTemplate`, `ShapePieceDTO`, `ColorRegionDTO` | Core | `Darkmatter.Core` | | `ICommand`, `IUndoStack` | Core | `Darkmatter.Core` | | `BoundedUndoStack` | Libs | `Darkmatter.Lib.CommandStack` | | `AddressableAssetProviderService` | Services | `Darkmatter.Services.Assets` | | `FileGalleryService` | Services | `Darkmatter.Services.Gallery` | | `RenderTextureCaptureService` | Services | `Darkmatter.Services.Capture` | | `ColoringController`, `PaintRegionCommand` | Features | `Darkmatter.Features.Coloring` | | `ShapeBuilderController`, `ShapePieceView` | Features | `Darkmatter.Features.ShapeBuilder` | | `HistoryController` | Features | `Darkmatter.Features.History` | | `ColorBookFlowController` | Features | `Darkmatter.Features.ColorBookFlow` | | `GalleryPresenter`, `GalleryGridView` | Features | `Darkmatter.Features.ArtBook` | | `ColorBookLifetimeScope`, `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. --- ## 32. Class Reference (Detailed) 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. --- ### 32.1 Core Contracts Pure interfaces and DTOs. Zero logic. #### `IDrawingTemplate` *(Core/Drawing)* 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 Thumbnail { get; } // 256×256 for catalog grid Sprite PaperBackground { get; } // composited under artwork IReadOnlyList Pieces { get; } // for ShapeBuilder IReadOnlyList Regions { get; } // for Coloring } ``` Implemented by `DrawingTemplateSO` (ScriptableObject) loaded via Addressables. #### `IDrawingTemplateCatalog` *(Core/Drawing)* 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/Coloring)* 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/History)* Already shown in section 8. Each undoable user action is one `ICommand`; the stack is bounded. #### `IGalleryService` *(Core/Gallery)* Persistent store of saved artwork PNGs. ```csharp public interface IGalleryService { UniTask SaveAsync(byte[] png, string templateId); UniTask> ListAsync(); // sorted newest first UniTask LoadFullAsync(string artworkId); // for fullscreen view UniTask LoadThumbnailAsync(string artworkId); UniTask DeleteAsync(string artworkId); } ``` #### `ICaptureService` *(Core/Capture)* Snapshots the artwork camera to a PNG blob. ```csharp public interface ICaptureService { UniTask CaptureAsync( Camera artCamera, Sprite paperBackground, int width = 2048, int height = 2048); } ``` #### `IProgressionService` *(Core/Progression)* 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/Assets)* 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/EventBus, also referenced from Core)* ```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`. #### `AddressableAssetProviderService` *(Services/Assets)* 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. #### `FileGalleryService` *(Services/Gallery)* Implements `IGalleryService`. ```csharp // fields: // IPathProvider _paths (wraps Application.persistentDataPath for tests) // IThumbnailGenerator _thumb (downscale + encode) // IEventBus _bus // pub: ArtworkSavedSignal, ArtworkDeletedSignal ``` - **Save flow:** write `{guid}.png.tmp` → fsync → rename; generate thumbnail on a worker; write sidecar JSON last (so partial saves are detectable by absence of JSON). - **List flow:** enumerate `*.json` in `Gallery/`, deserialize, sort by `CreatedUtc desc`. - **Delete flow:** delete png + thumb + json; missing files ignored (idempotent). #### `RenderTextureCaptureService` *(Services/Capture)* Implements `ICaptureService`. - **Steps:** allocate `RenderTexture(width, height, 0, ARGB32)` → bind to `artCamera.targetTexture` → `artCamera.Render()` → `ReadPixels` into `Texture2D` → composite `paperBackground` underneath (single shader blit) → `EncodeToPNG` → release RT + textures. - **Threading:** PNG encode happens on a `UniTask.RunOnThreadPool` to avoid hitching the main thread on tablets. - **Sizing:** default 2048², overridable. Capped at device max texture size. #### `JsonPersistenceService` *(Services/Persistence)* Implements `IPersistenceService` (small JSON blob; not the gallery). ```csharp public interface IPersistenceService { UniTask LoadAsync(string key) where T : class, new(); UniTask SaveAsync(string key, T value); } ``` - **Path:** `Application.persistentDataPath/save.json`. - **Format:** single JSON object keyed by `key` so multiple services can share one file. - **Atomicity:** write to `save.json.tmp` → rename. #### `SceneService` *(Services/Scenes)* 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)* 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)* 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)* 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/EventBus)* 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). #### `Fsm` *(Libs/FSM)* Generic state machine used by `ColorBookFlowController`. ```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.4 Feature — `DrawingCatalog` #### `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` #### `ShapeBuilderController` *(Systems)* Spawns shape pieces for the selected template, tracks snap progress, fires `ShapeAssembledSignal` when complete. ```csharp // fields: IDrawingTemplateCatalog _catalog, ShapePieceFactory _factory, IEventBus _bus, ShapeBuilderConfig _cfg public sealed class ShapeBuilderController : IDisposable { public IReadOnlyList Active { get; } public UniTask BuildAsync(string templateId); // load template, spawn pieces in tray public void Reset(); // clear, unsubscribe } // sub: DrawingSelectedSignal // pub: ShapeAssembledSignal ``` - **Internal:** counts `PieceSnappedSignal` against expected piece count. #### `ShapePieceView : MonoBehaviour` *(Views)* World-space draggable sprite with collider. Source for snap-or-return logic shown in section 26. ```csharp public sealed class ShapePieceView : MonoBehaviour { public string PieceId { get; } public bool IsLocked { get; } public event Action Snapped; // raised when piece locks into slot public void Initialize(ShapePieceDTO dto, IInputReader input, IAudioService audio); } ``` - **No public mutators** for position once locked — controller treats `IsLocked` as the source of truth. #### `ShapePieceFactory` *(Systems)* Instantiates `ShapePieceView` prefabs from a pool. Avoids re-instantiating across "Next" cycles on the same template family. ```csharp public sealed class ShapePieceFactory { public ShapePieceView Spawn(ShapePieceDTO dto, Transform parent); public void Despawn(ShapePieceView view); } ``` --- ### 32.6 Feature — `Coloring` #### `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 int SelectedIndex { get; private set; } public Color CurrentColor => Palette.Colors[SelectedIndex]; public event Action SelectedIndexChanged; public void SetPalette(IColorPalette palette); // resets SelectedIndex to 0 public void SelectColor(int index); } ``` - **Why a repository:** presenter and controller both need to read/write current color; an event-emitting POCO is simpler than wiring two signals. #### `ColoringController` *(Systems)* — implements `IColoringController` Builds and pushes `PaintRegionCommand` instances; spawns `ColorRegionView` per region. ```csharp // fields: IUndoStack _undo, ColoringStateRepository _state, ColorRegionFactory _factory, IEventBus _bus public interface IColoringController { UniTask SpawnRegionsAsync(IDrawingTemplate template); void PaintRegion(ColorRegionView view); // builds command, pushes to undo stack void Clear(); } // sub: ShapeAssembledSignal (via flow controller, not direct) // pub: ColorAppliedSignal (via PaintRegionCommand) ``` #### `ColorRegionView : MonoBehaviour` *(Views)* Sprite + `PolygonCollider2D`, on `Artwork` layer. Tapped via `Physics2D.OverlapPoint` from `ColoringInputBinder`. ```csharp public sealed class ColorRegionView : MonoBehaviour { public string RegionId { get; } public Color Color { get; } // current paint public void Initialize(ColorRegionDTO dto); public void SetColor(Color c); // setter only; no logic } ``` #### `ColoringInputBinder` *(Systems)* — `IStartable, IDisposable` Subscribes to `IInputReader.PointerDown`, raycasts on the `Artwork` layer mask, calls `ColoringController.PaintRegion(view)` on hit. #### `PaintRegionCommand` *(Commands)* Source in section 23. Holds `view`, `fromColor`, `toColor`, `bus`. Symmetrical execute/undo. #### `ColorPaletteView`, `ColorPalettePresenter` *(UI)* Sources in section 25. Presenter binds `ColoringStateRepository.SelectedIndexChanged` ↔ `IColorPaletteView`. #### `ColorRegionFactory` *(Systems)* Mirror of `ShapePieceFactory` for regions. Pool-friendly. --- ### 32.7 Feature — `History` #### `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` #### `CaptureController` *(Systems)* The orchestrator behind the "Capture" button. Stateless other than guarding against concurrent captures. ```csharp // fields: ICaptureService _capture, IGalleryService _gallery, IEventBus _bus, ColorBookSceneRefs _refs public sealed class CaptureController { public bool IsCapturing { get; } public UniTask CaptureCurrentAsync(string templateId, Sprite paperBg); } // pub: ArtworkCapturedSignal (mid-flow), ArtworkSavedSignal (post-save) ``` - **Concurrency:** sets `IsCapturing = true` on entry; UI binds button enabled to `!IsCapturing` to prevent double-tap. #### `CaptureButtonPresenter` *(UI)* Wires button click → `CaptureController.CaptureCurrentAsync`. Disables button while in progress. Shows toast on `ArtworkSavedSignal`. --- ### 32.9 Feature — `Progression` #### `ProgressionService` *(Systems)* — implements `IProgressionService` The only place that knows what "completed" means. - **Persistence:** delegates to `IPersistenceService` 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` #### `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 // ColorBookSceneRefs _refs (panel roots to enable/disable) // 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.11 Feature — `ArtBook` #### `GalleryPresenter` *(UI)* — `IAsyncStartable, IDisposable` Lists artworks, opens fullscreen view, deletes, shares. ```csharp // fields: IGalleryService _gallery, IGalleryView _view, IExternalShareService _share, IEventBus _bus ``` - **Start:** `_gallery.ListAsync()` → `_view.SetItems(...)`. - **Subscribes** to `ArtworkSavedSignal` to live-refresh if the user pops back in. #### `IGalleryView` *(UI)* ```csharp public interface IGalleryView { event Action OnArtworkTapped; event Action OnDeleteRequested; event Action OnShareRequested; void SetItems(IReadOnlyList items); void ShowFullscreen(Texture2D full); void HideFullscreen(); } ``` #### `IExternalShareService` *(Core)* Platform plugin shim (iOS Photos / Android MediaStore). ```csharp public interface IExternalShareService { UniTask SaveToCameraRollAsync(byte[] png); UniTask ShareAsync(byte[] png, string subject); } ``` --- ### 32.12 App Layer #### `AppBoot` *(App/Boot)* — `IAsyncStartable` Single entry point. Steps in section 29. ```csharp // fields: IAssetProviderService _assets, IPersistenceService _persist, IProgressionService _progress, // IAudioService _audio, ISceneService _scenes, BootConfig _cfg public sealed class AppBoot : IAsyncStartable { public UniTask StartAsync(CancellationToken ct); } ``` #### LifetimeScopes - `RootLifetimeScope` — section 21. Registers all services + `IEventBus`. Persists for app lifetime. - `MainMenuLifetimeScope` — registers `MainMenuPresenter` and view. - `ColorBookLifetimeScope` — section 21. Registers feature installers + `ColorBookFlowController` as entry point. - `ArtBookLifetimeScope` — registers `GalleryPresenter` + view + `IExternalShareService`. All scope classes are thin: serialized fields for scene refs, `Configure(IContainerBuilder)` only. --- ### 32.13 Cross-cutting types #### `ColorBookSceneRefs : MonoBehaviour` *(App)* Aggregates all scene-bound Unity references that features need: `Camera artCamera`, `Transform catalogRoot`, `Transform builderRoot`, `Transform coloringRoot`, `RectTransform hudRoot`, `ColorPaletteView paletteView`, `HistoryButtonsView historyView`. Registered as a singleton in `ColorBookLifetimeScope` so features don't `Find` things. #### `IInstaller` *(App)* ```csharp public interface IInstaller { void Install(IContainerBuilder builder); } ``` Implemented as `ScriptableObject` per feature so scopes can drag them in the inspector (section 22). --- ### 32.14 Class summary table | Class | Layer | Role | Key dependencies | |---|---|---|---| | `AppBoot` | App | Startup sequencer | assets, persist, progression, scenes | | `RootLifetimeScope` | App | Root DI | configs | | `ColorBookLifetimeScope` | App | Scene DI | scene refs, installers | | `DrawingCatalogController` | Feature | Grid logic | catalog, bus | | `DrawingCatalogPresenter` | Feature | UI bridge | view, controller, catalog | | `ShapeBuilderController` | Feature | Piece spawn + snap tracking | catalog, factory, bus, cfg | | `ShapePieceView` | Feature | Draggable piece MB | input, audio | | `ColoringStateRepository` | Feature | Current color model | — | | `ColoringController` | Feature | Region spawn + paint cmd | undo, state, factory, bus | | `ColorRegionView` | Feature | Region sprite MB | — | | `PaintRegionCommand` | Feature | Undoable paint | view, bus | | `HistoryController` | Feature | Undo/redo facade | undo stack, bus | | `CaptureController` | Feature | Capture+save orchestration | capture svc, gallery, bus, refs | | `ColorBookFlowController` | Feature | Scene FSM | bus, catalog, builder, coloring, capture, progression | | `GalleryPresenter` | Feature | Art book listing | gallery, share, view, bus | | `BoundedUndoStack` | Lib | Capped undo store | — | | `EventBus` | Lib | Pub/sub | — | | `Fsm` | Lib | Generic FSM | — | | `AddressableAssetProviderService` | Service | Addressables wrapper | — | | `FileGalleryService` | Service | Gallery file IO | paths, thumb gen, bus | | `RenderTextureCaptureService` | Service | PNG render | — | | `JsonPersistenceService` | Service | Settings/progression IO | — | | `SceneService` | Service | Async scene loads | — | | `AudioService` | Service | SFX playback | assets | | `ProgressionService` | Service | Completion tracking | persistence | 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.