# 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.