1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
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.
|