aboutsummaryrefslogtreecommitdiff
path: root/docs/superpowers/plans/2026-04-05-milestone-7-scene-loader.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers/plans/2026-04-05-milestone-7-scene-loader.md')
-rw-r--r--docs/superpowers/plans/2026-04-05-milestone-7-scene-loader.md201
1 files changed, 201 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-04-05-milestone-7-scene-loader.md b/docs/superpowers/plans/2026-04-05-milestone-7-scene-loader.md
new file mode 100644
index 0000000..2b7f86f
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-05-milestone-7-scene-loader.md
@@ -0,0 +1,201 @@
+# Scene Loader Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Create `scene-loader.scm` -- a module that encapsulates the repeated tilemap-load / texture-create / scene-build pattern currently duplicated across platformer, topdown, and sandbox demos.
+
+**Architecture:** A new `downstroke/scene-loader` module provides `game-load-scene!` as the single entry point for the common pattern (load TMX, create texture, build scene, set on game). Two pure helpers (`tilemap-objects->entities` and `make-prefab-registry` / `instantiate-prefab`) handle object-layer entity instantiation with a simple hash-table registry. The module sits between `assets`/`world`/`tilemap` and `engine` in the dependency graph; demos import it and replace ~10 lines of boilerplate with a single call.
+
+**Tech Stack:** Chicken Scheme 5, `defstruct`, `srfi-1` (filter-map), `srfi-69` (hash-tables), `sdl2` (texture creation), existing downstroke modules (`world`, `tilemap`, `assets`).
+
+**Rejected alternatives:**
+- Putting scene-loading logic directly into `engine.scm` -- rejected because engine should stay lifecycle-only; scene loading is an opt-in convenience.
+- Making `game-load-scene!` accept an entity list parameter -- rejected because callers should add entities after via `scene-add-entity` (which already exists and mutates in place); keeps the loader focused on the tilemap/texture concern.
+
+---
+
+### Task 1: Tests for pure helpers (TDD -- red phase)
+
+**Files:**
+- Create: `tests/scene-loader-test.scm`
+
+- [ ] **Step 1:** Create `tests/scene-loader-test.scm` with `(import srfi-64)` and mock modules. Mock `downstroke/tilemap` inline (same pattern as `tests/renderer-test.scm` lines 10-18) defining the `object` defstruct (`object-type`, `object-x`, `object-y`, `object-width`, `object-height`) and `tilemap-objects` accessor. Mock `downstroke/world` with the `scene`, `camera` defstructs. Mock `sdl2` with a stub `create-texture-from-surface`. Mock `downstroke/assets` with stubs for `asset-set!` and `asset-ref`.
+- [ ] **Step 2:** Write tests for `make-prefab-registry` + `instantiate-prefab`:
+ - `make-prefab-registry` with two type/constructor pairs returns a registry.
+ - `instantiate-prefab` with a known type calls the constructor and returns the entity plist.
+ - `instantiate-prefab` with an unknown type returns `#f`.
+- [ ] **Step 3:** Write tests for `tilemap-objects->entities`:
+ - Given a mock tilemap whose `tilemap-objects` returns a list of 3 fake objects (with `object-type`, `object-x`, `object-y`, `object-width`, `object-height`), and an `instantiate-fn` that returns a plist for type `"player"` but `#f` for type `"decoration"`, verify the result is a list containing only the matched entity (i.e., `#f` results are filtered out).
+ - Given a tilemap with zero objects, verify the result is `'()`.
+- [ ] **Step 4:** Add a placeholder `(include "scene-loader.scm")` and `(import downstroke/scene-loader)` after the mocks. Run `csi -s tests/scene-loader-test.scm` and confirm it fails (module not found). This is the red phase.
+
+---
+
+### Task 2: Implement `scene-loader.scm` (green phase)
+
+**Files:**
+- Create: `scene-loader.scm`
+
+- [ ] **Step 1:** Create `scene-loader.scm` with the module declaration:
+ ```scheme
+ (module downstroke/scene-loader *
+ (import scheme
+ (chicken base)
+ (only srfi-1 filter-map)
+ (srfi 69)
+ (prefix sdl2 "sdl2:")
+ defstruct
+ downstroke/world
+ downstroke/tilemap
+ downstroke/assets)
+ ...)
+ ```
+- [ ] **Step 2:** Implement `make-prefab-registry` -- takes alternating `symbol constructor` pairs, returns an `srfi-69` hash-table mapping each symbol to its constructor lambda:
+ ```scheme
+ (define (make-prefab-registry . pairs)
+ (let ((ht (make-hash-table)))
+ (let loop ((p pairs))
+ (if (null? p) ht
+ (begin
+ (hash-table-set! ht (car p) (cadr p))
+ (loop (cddr p)))))))
+ ```
+- [ ] **Step 3:** Implement `instantiate-prefab` -- looks up `type` (a symbol) in registry, calls the constructor with `(x y w h)`, returns entity plist or `#f`:
+ ```scheme
+ (define (instantiate-prefab registry type x y w h)
+ (let ((ctor (hash-table-ref/default registry type #f)))
+ (and ctor (ctor x y w h))))
+ ```
+- [ ] **Step 4:** Implement `tilemap-objects->entities`:
+ ```scheme
+ (define (tilemap-objects->entities tilemap instantiate-fn)
+ (filter-map
+ (lambda (obj)
+ (instantiate-fn (string->symbol (object-type obj))
+ (object-x obj) (object-y obj)
+ (object-width obj) (object-height obj)))
+ (tilemap-objects tilemap)))
+ ```
+ Note: `object-type` returns a string (from TMX XML); convert to symbol so callers work with symbols.
+- [ ] **Step 5:** Implement `create-tileset-texture`:
+ ```scheme
+ (define (create-tileset-texture renderer tilemap)
+ (sdl2:create-texture-from-surface
+ renderer
+ (tileset-image (tilemap-tileset tilemap))))
+ ```
+- [ ] **Step 6:** Implement `game-load-scene!`:
+ ```scheme
+ (define (game-load-scene! game filename)
+ (let* ((tm (load-tilemap filename))
+ (tex (create-tileset-texture (game-renderer game) tm))
+ (scene (make-scene entities: '()
+ tilemap: tm
+ camera: (make-camera x: 0 y: 0)
+ tileset-texture: tex)))
+ (game-asset-set! game 'tilemap tm)
+ (game-scene-set! game scene)
+ scene))
+ ```
+ Note: `game-load-scene!` needs `game-renderer`, `game-asset-set!`, and `game-scene-set!` from `downstroke/engine`. However, importing engine from scene-loader would create a circular dependency (engine depends on world, scene-loader depends on world+tilemap, engine should not depend on scene-loader). **Resolution:** `game-load-scene!` must accept `renderer` explicitly, or scene-loader must also import `downstroke/engine`. Check: engine exports `game-renderer`, `game-asset-set!`, `game-scene-set!`. Scene-loader depends on engine; engine does NOT need to depend on scene-loader. This is safe -- add `downstroke/engine` to imports.
+- [ ] **Step 7:** Run `csi -s tests/scene-loader-test.scm`. All tests should pass (green phase).
+
+---
+
+### Task 3: Build system integration
+
+**Files:**
+- Modify: `/home/gene/src/downstroke/Makefile`
+- Modify: `/home/gene/src/downstroke/downstroke.egg`
+
+- [ ] **Step 1:** In `Makefile`, add `scene-loader` to `MODULE_NAMES` after `engine` (it depends on engine, world, tilemap, assets):
+ ```
+ MODULE_NAMES := entity tilemap world input physics renderer assets engine mixer sound animation ai scene-loader
+ ```
+- [ ] **Step 2:** Add explicit dependency line:
+ ```makefile
+ bin/scene-loader.o: bin/world.o bin/tilemap.o bin/assets.o bin/engine.o
+ ```
+- [ ] **Step 3:** Add the test to the `test:` target:
+ ```makefile
+ @csi -s tests/scene-loader-test.scm
+ ```
+- [ ] **Step 4:** In `downstroke.egg`, add the extension after the `downstroke/engine` entry:
+ ```scheme
+ (extension downstroke/scene-loader
+ (source "scene-loader.scm")
+ (component-dependencies downstroke/world downstroke/tilemap downstroke/assets downstroke/engine))
+ ```
+- [ ] **Step 5:** Run `make clean && make && make test` to verify engine compiles and all tests pass.
+
+---
+
+### Task 4: Update demos to use scene-loader
+
+**Files:**
+- Modify: `/home/gene/src/downstroke/demo/platformer.scm`
+- Modify: `/home/gene/src/downstroke/demo/topdown.scm`
+- Modify: `/home/gene/src/downstroke/demo/sandbox.scm`
+
+- [ ] **Step 1:** In `demo/platformer.scm`:
+ - Add `downstroke/scene-loader` to the imports.
+ - In `preload:`, replace the `game-asset-set!` call for tilemap with nothing (remove it; `game-load-scene!` handles this). Keep the `init-audio!` and `load-sounds!` calls.
+ - In `create:`, replace the entire `let*` body with:
+ ```scheme
+ (let* ((scene (game-load-scene! game "demo/assets/level-0.tmx"))
+ (player (list #:type 'player
+ #:x 100 #:y 50
+ #:width 16 #:height 16
+ #:vx 0 #:vy 0
+ #:gravity? #t
+ #:on-ground? #f
+ #:tile-id 1)))
+ (scene-add-entity scene player))
+ ```
+ Note: `scene-add-entity` (from `world.scm` line 41) mutates and returns the scene, which is already set on the game by `game-load-scene!`, so no further `game-scene-set!` needed.
+
+- [ ] **Step 2:** In `demo/topdown.scm`:
+ - Add `downstroke/scene-loader` to imports.
+ - Remove the `preload:` tilemap loading entirely (the `preload:` lambda can just be `(lambda (game) #f)` or removed if `make-game` allows it -- check engine.scm; if preload is optional, omit it).
+ - In `create:`, replace with:
+ ```scheme
+ (let* ((scene (game-load-scene! game "demo/assets/level-0.tmx"))
+ (player (list #:type 'player
+ #:x 100 #:y 100
+ #:width 16 #:height 16
+ #:vx 0 #:vy 0
+ #:gravity? #f
+ #:tile-id 1)))
+ (scene-add-entity scene player))
+ ```
+
+- [ ] **Step 3:** In `demo/sandbox.scm`:
+ - Add `downstroke/scene-loader` to imports.
+ - Remove the `preload:` tilemap loading.
+ - In `create:`, replace with:
+ ```scheme
+ (let ((scene (game-load-scene! game "demo/assets/level-0.tmx")))
+ (scene-entities-set! scene (spawn-entities)))
+ ```
+ Note: sandbox replaces the entire entity list rather than adding one, so use `scene-entities-set!` directly.
+
+- [ ] **Step 4:** Run `make clean && make && make demos` to verify all demos compile.
+
+---
+
+### Task 5: Verify and clean up
+
+**Files:**
+- All modified files
+
+- [ ] **Step 1:** Run the full build and test suite: `make clean && make && make demos && make test`. All must pass.
+- [ ] **Step 2:** Review that no demo still imports `(prefix sdl2 "sdl2:")` solely for `create-texture-from-surface`. If that was the only sdl2 usage in the demo's `create:` hook, the `sdl2` import can remain (it may be needed for other SDL2 calls in `update:` or elsewhere) -- do not remove imports that are still used.
+- [ ] **Step 3:** Verify the three demos no longer contain the duplicated tilemap-load/texture-create/scene-build pattern. Each demo's `create:` hook should be noticeably shorter.
+
+---
+
+## Gaps -- follow-up investigation needed
+
+- **`game-load-scene!` importing engine:** The plan assumes `scene-loader.scm` can import `downstroke/engine` without creating a circular dependency. Verify by checking that `engine.scm` does NOT import `scene-loader`. From the Makefile deps (`bin/engine.o: bin/renderer.o bin/world.o bin/input.o bin/assets.o`), this is confirmed safe.
+- **`preload:` optionality:** The plan suggests removing `preload:` from topdown and sandbox demos. Need to verify whether `make-game` requires the `preload:` keyword or treats it as optional. If required, keep it as `(lambda (game) #f)`.
+- **`object-type` return type:** The plan assumes `object-type` returns a string (from TMX XML parsing). If it already returns a symbol, remove the `string->symbol` call in `tilemap-objects->entities`. Check the TMX parser in `tilemap.scm` to confirm.