diff options
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.md | 201 |
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. |
