diff options
Diffstat (limited to 'docs/superpowers/specs')
| -rw-r--r-- | docs/superpowers/specs/2026-04-05-demos-design.md | 256 | ||||
| -rw-r--r-- | docs/superpowers/specs/2026-04-05-milestone-8-game-object-lifecycle-design.md | 234 |
2 files changed, 0 insertions, 490 deletions
diff --git a/docs/superpowers/specs/2026-04-05-demos-design.md b/docs/superpowers/specs/2026-04-05-demos-design.md deleted file mode 100644 index 78ed3f4..0000000 --- a/docs/superpowers/specs/2026-04-05-demos-design.md +++ /dev/null @@ -1,256 +0,0 @@ -# Downstroke Demo Games Design - -**Date:** 2026-04-05 -**Status:** Approved -**Scope:** `demo/` folder, 5 demo games, Makefile `make demos` target, CLAUDE.md update - ---- - -## Goal - -Provide 5 small self-contained demo games in `demo/` that collectively exercise every engine system. Each demo compiles to its own executable (`bin/demo-*`). They replace the macroknight integration test for Milestone 8 and serve as living documentation of the engine API. - ---- - -## File Layout - -``` -demo/ - assets/ ← copied from macroknight/assets (not symlinked) - monochrome-transparent.png ← tileset spritesheet - monochrome_transparent.tsx ← tileset metadata (TSX) - level-0.tmx ← level used by platformer, topdown, sandbox - DejaVuSans.ttf ← font for audio demo text - jump.wav ← sound effect (platformer jump, shmup shoot) - theme.ogg ← music (audio demo) - platformer.scm - shmup.scm - topdown.scm - audio.scm - sandbox.scm -``` - -**Omitted from copy:** `prefabs.scm`, `macroknight.tiled-project`, `macroknight.tiled-session` — macroknight-specific files not needed by any demo. - -**Audio modules:** `mixer.scm` and `sound.scm` are copied from `macroknight/` into the downstroke repo root and added to the build. They are engine-level modules and are required by any demo that uses audio. - -When copying `sound.scm`, add one function that macroknight's version omits: -```scheme -(define (stop-music!) (mix-halt-music)) -``` -The audio demo uses `stop-music!` to toggle music off. - ---- - -## Build - -### Makefile additions - -```makefile -DEMO_NAMES := platformer shmup topdown audio sandbox -DEMO_BINS := $(patsubst %,bin/demo-%,$(DEMO_NAMES)) - -demos: engine $(DEMO_BINS) - -bin/demo-%: demo/%.scm $(OBJECT_FILES) | bin - csc demo/$*.scm $(OBJECT_FILES) -o bin/demo-$* -I bin -``` - -- `make` — builds engine modules only (unchanged) -- `make demos` — builds all 5 demo executables; depends on engine being built first -- Demos are compiled as programs (not units), linked against all engine `.o` files -- `$(OBJECT_FILES)` includes `mixer` and `sound` once those modules are added to `MODULE_NAMES` - -### `render-scene!` nil guards - -`render-scene!` in `renderer.scm` is updated so that entity drawing is **nested inside** the tilemap guard: - -- Tilemap drawing only fires if `(scene-tilemap scene)` is not `#f` -- Entity drawing only fires if **both** `(scene-tilemap scene)` AND `(scene-tileset-texture scene)` are not `#f` - -```scheme -(when tilemap - (draw-tilemap renderer camera tileset-texture tilemap) - (when tileset-texture - (let ((tileset (tilemap-tileset tilemap))) - (draw-entities renderer camera tileset tileset-texture entities)))) -``` - -**Consequence for shmup:** since shmup has no tilemap, the engine will not draw its entities. Shmup draws its player, bullets, and enemies as **colored SDL rectangles** in its `render:` hook — matching the original "colored rects" intent. This is acceptable because shmup entities are simple geometric shapes, not sprites. - -Audio (no scene at all) works because `engine.scm` guards `render-scene!` with `(when (game-scene game) ...)`. - -### CLAUDE.md update - -Add to the Build & Test section: - -> `make demos` must always succeed. A demo that fails to compile is a build failure. Run `make && make demos` to verify both engine and demos build cleanly. - ---- - -## Demo Code Pattern - -Every demo follows this ~30-line structure: - -```scheme -(import (prefix sdl2 "sdl2:") - (prefix sdl2-ttf "ttf:") - (prefix sdl2-image "img:") - downstroke/engine - downstroke/world - downstroke/tilemap - downstroke/renderer - downstroke/input - downstroke/physics - downstroke/assets) - -(define *game* - (make-game - title: "Demo: <Name>" width: 600 height: 400 - preload: (lambda (game) ...) ;; load tilemap, tileset texture, sounds - create: (lambda (game) ...) ;; build scene, place entities - update: (lambda (game dt) ...) ;; input dispatch, physics calls - render: (lambda (game) ...))) ;; HUD overlay (optional) - -(game-run! *game*) -``` - -Tile IDs in entity plists are placeholder values — to be adjusted visually after first run. - ---- - -## The 5 Demos - -### 1. `demo/platformer.scm` — Platformer - -**Systems exercised:** `input`, `physics` (gravity + tile collision), `renderer` (tilemap + entities), `world`/scene, camera follow, audio (sound effect) - -**Mechanics:** -- Player entity with gravity, left/right movement, jump -- Tile collision via `apply-physics` from `physics.scm` -- Camera follows player horizontally -- Jump sound via `(play-sound 'jump)` (loaded in preload: as `'(jump . "demo/assets/jump.wav")`) -- Level: `demo/assets/level-0.tmx` -- Tile IDs: placeholder (user adjusts) - -**Key entity plist:** -```scheme -(list #:type 'player - #:x 100 #:y 50 - #:width 16 #:height 16 - #:vx 0 #:vy 0 - #:gravity? #t - #:on-ground? #f - #:tile-id 1) -``` - -**Update logic:** read input → set `#:vx` from left/right → jump sets `#:vy` → call physics step → update camera x to follow player x. - ---- - -### 2. `demo/shmup.scm` — Shoot-em-up - -**Systems exercised:** `entity` (spawning/removal), manual AABB entity-entity collision (removal-based, not `physics.scm`), `input`, `renderer` (SDL colored rects), `world`/scene (no tilemap) - -**Mechanics:** -- Player ship at bottom, moves left/right -- Space bar fires bullet upward (new entity added to scene) -- Enemies spawn from top at random x positions every N frames, move downward -- Bullet-enemy collision: both entities removed from scene -- No tilemap — plain background (black/SDL clear) -- No gravity on any entity -- `jump.wav` plays on shoot - -**Key entity plists:** (no `#:tile-id` — entities are drawn as colored rects) -```scheme -;; player -(list #:type 'player #:x 280 #:y 360 #:width 16 #:height 16 #:vx 0 #:vy 0) -;; bullet -(list #:type 'bullet #:x px #:y 340 #:width 4 #:height 8 #:vx 0 #:vy -5) -;; enemy -(list #:type 'enemy #:x rx #:y 0 #:width 16 #:height 16 #:vx 0 #:vy 2) -``` - -**Rendering:** shmup has no tilemap, so `render-scene!` draws nothing for this scene. Shmup implements its own `render:` hook using `sdl2:render-fill-rect!` with distinct colors (player = white, bullet = yellow, enemy = red). The camera is at (0,0) so screen coords == world coords. - -**Update logic:** move entities by vx/vy each frame → manual AABB collision check between bullets and enemies (not `resolve-entity-collisions` — shmup uses removal, not push-apart) → filter removed entities from scene → spawn new enemy every 60 frames → read input for player movement and shoot. - ---- - -### 3. `demo/topdown.scm` — Top-down explorer - -**Systems exercised:** `input` (8-directional), `renderer` (tilemap + entity), `world`/scene, camera follow (both axes), `physics` (no gravity) - -**Mechanics:** -- Player entity moves in 8 directions (WASD or arrows) -- No gravity (`#:gravity? #f`) -- Camera follows player on both x and y axes -- Level: `demo/assets/level-0.tmx` (same tilemap, different movement feel) -- Tileset texture loaded in preload: — required for entity sprite rendering (tilemap + tileset-texture both present so render-scene! draws entities via tileset) -- No audio - -**Update logic:** read input → set `#:vx` and `#:vy` from direction keys → apply tile collision (no gravity component) → update camera to center on player. - ---- - -### 4. `demo/audio.scm` — Audio showcase - -**Systems exercised:** audio (sound effects + music), `renderer` (text via `draw-ui-text`), `input`, `assets` - -**Mechanics:** -- Static screen with text instructions rendered via `draw-ui-text` -- Press **J** → play `jump.wav` -- Press **M** → toggle `theme.ogg` music on/off -- Press **Escape** → quit -- No tilemap, no physics, no moving entities -- Uses `DejaVuSans.ttf` for text -- Audio calls via `sound.scm` functions: `load-sounds!`, `play-sound`, `load-music!`, `play-music!`, `stop-music!` (added when copying sound.scm — see File Layout) - -**Display:** colored rectangle background + text labels for each key binding. - ---- - -### 5. `demo/sandbox.scm` — Physics sandbox - -**Systems exercised:** `physics` (gravity + tile collision + entity-entity collision), `renderer`, `world`/scene, no player input - -**Mechanics:** -- 10 entities spawned at random x positions near the top of the screen -- All have `#:gravity? #t` and `#:solid? #t` (required for `resolve-entity-collisions` to participate) -- Physics step runs each frame (gravity accelerates, tile collision stops them) -- Entities rest on floor tiles or bounce (depending on physics.scm behavior) -- No player — pure observation of physics pipeline -- Level: `demo/assets/level-0.tmx` -- After all entities settle (or after 10 seconds), loop: despawn all, respawn at new random positions - ---- - -## Systems Coverage Matrix - -| System / Module | platformer | shmup | topdown | audio | sandbox | -|---|---|---|---|---|---| -| `engine` (make-game, game-run!) | ✓ | ✓ | ✓ | ✓ | ✓ | -| `input` | ✓ | ✓ | ✓ | ✓ | — | -| `physics` (gravity) | ✓ | — | — | — | ✓ | -| `physics` (tile collision) | ✓ | — | ✓ | — | ✓ | -| `physics` (entity collision) | — | — | — | — | ✓ | -| manual AABB (removal) | — | ✓ | — | — | — | -| `renderer` (tilemap) | ✓ | — | ✓ | — | ✓ | -| `renderer` (entities) | ✓ | — | ✓ | — | ✓ | -| `renderer` (SDL colored rects) | — | ✓ | — | — | — | -| `renderer` (text) | — | — | — | ✓ | — | -| `world` / scene | ✓ | ✓ | ✓ | — | ✓ | -| `assets` registry | ✓ | ✓ | ✓ | ✓ | ✓ | -| audio (sound) | ✓ | ✓ | — | ✓ | — | -| audio (music) | — | — | — | ✓ | — | -| camera follow | ✓ (x) | — | ✓ (xy) | — | — | - ---- - -## Out of Scope - -- Animation state machine (`animation.scm`) — not yet extracted to downstroke -- AI (`ai.scm`) — not yet extracted -- Prefab system — not yet extracted -- Scene transitions — Milestone 9 -- Asset-type-specific load helpers (`game-load-tilemap!` etc.) — Milestone 6 diff --git a/docs/superpowers/specs/2026-04-05-milestone-8-game-object-lifecycle-design.md b/docs/superpowers/specs/2026-04-05-milestone-8-game-object-lifecycle-design.md deleted file mode 100644 index d6e71be..0000000 --- a/docs/superpowers/specs/2026-04-05-milestone-8-game-object-lifecycle-design.md +++ /dev/null @@ -1,234 +0,0 @@ -# Milestone 8 — The Game Object and Lifecycle API - -**Date:** 2026-04-05 -**Status:** Approved -**Scope:** `engine.scm`, `assets.scm`, macroknight port -**Milestone numbering:** Follows `downstroke.org` / `TODO-engine.org` (Milestone 8). CLAUDE.md refers to the same work as "Milestone 7". - ---- - -## Goal - -Introduce `make-game` and `game-run!` as the public entry point for downstroke. A minimal game becomes ~20 lines of Scheme. This is the "Phaser moment" — the API stabilises around a game struct with lifecycle hooks. - ---- - -## Architecture - -Two new files: - -- **`engine.scm`** — `make-game` struct, `game-run!`, the frame loop, and all public accessors. -- **`assets.scm`** — minimal key→value asset registry. No asset-type-specific logic; that is Milestone 6's domain. - -`engine.scm` imports: `renderer.scm`, `input.scm`, `world.scm`, `assets.scm`, the sdl2 egg (prefix `sdl2:`), `sdl2-ttf` (prefix `ttf:`), and `sdl2-image` (prefix `img:`). - -**Audio / mixer is not initialised by the engine in this milestone.** `mixer.scm` and `sound.scm` remain in macroknight and are not yet extracted. macroknight's `preload:` hook calls `init-audio!` directly from `sound.scm` until those modules are extracted. - -`physics.scm` is **not** imported by the engine. Physics is a library; the user requires it if needed and calls physics functions from their `update:` hook. - -### New function in `renderer.scm` - -`render-scene!` does not yet exist. It must be added to `renderer.scm` as part of this milestone. The module uses `export *` so no explicit export update is needed. - -Existing signatures in `renderer.scm` that `render-scene!` calls: -- `(draw-tilemap renderer camera tileset-texture tilemap)` — 4 args -- `(draw-entities renderer camera tileset tileset-texture entities)` — 5 args -- `tilemap-tileset` is a field accessor on the `tilemap` struct (defined in `tilemap.scm`) - -```scheme -(define (render-scene! renderer scene) - (let ((camera (scene-camera scene)) - (tilemap (scene-tilemap scene)) - (tileset-texture (scene-tileset-texture scene)) - (tileset (tilemap-tileset (scene-tilemap scene))) - (entities (scene-entities scene))) - (draw-tilemap renderer camera tileset-texture tilemap) - (draw-entities renderer camera tileset tileset-texture entities))) -``` - -### Build order (Makefile) - -Only modules that currently exist in downstroke are listed. Modules still in macroknight (`animation`, `ai`, `prefabs`, `mixer`, `sound`) are not prerequisites for this milestone. - -``` -entity → tilemap → world → input → renderer → assets → engine -``` - -`physics` is not a link dependency of `engine` and is omitted from this chain. It is a separate build target. - ---- - -## Data Structures - -### `game` struct (`engine.scm`) - -`defstruct` is called with `constructor: make-game*` so the generated raw constructor does not collide with the public keyword constructor `make-game`. - -```scheme -(defstruct (game constructor: make-game*) - title width height - window renderer - input ;; input-state record (accessor: game-input) - input-config ;; input-config record (accessor: game-input-config) - assets ;; asset registry hash table (from assets.scm) - frame-delay - preload-hook ;; (lambda (game) ...) - create-hook ;; (lambda (game) ...) - update-hook ;; (lambda (game dt) ...) - render-hook ;; (lambda (game) ...) — post-render overlay - scene) ;; current scene struct (accessor: game-scene) -``` - -**Note:** There is no separate `camera` field. The camera lives inside the scene (`scene-camera`). `game-camera` is a convenience accessor (see Accessors section). - -### `make-game` keyword constructor - -```scheme -(define (make-game #!key - (title "Downstroke Game") - (width 640) (height 480) - (frame-delay 16) - (input-config *default-input-config*) - (preload #f) (create #f) (update #f) (render #f)) - (make-game* - title: title width: width height: height - window: #f renderer: #f scene: #f - input: (create-input-state input-config) ;; create-input-state takes one arg: config - input-config: input-config - assets: (make-asset-registry) - frame-delay: frame-delay - preload-hook: preload - create-hook: create - update-hook: update - render-hook: render)) -``` - -### Asset registry (`assets.scm`) - -```scheme -(define (make-asset-registry) (make-hash-table)) -(define (asset-set! reg key val) (hash-table-set! reg key val)) -(define (asset-ref reg key) (hash-table-ref reg key (lambda () #f))) -``` - ---- - -## Lifecycle: `game-run!` - -```scheme -(define (game-run! game) - ;; 1. SDL2 init (no audio — mixer not yet extracted, user calls init-audio! in preload:) - (sdl2:init! '(video joystick game-controller)) - (ttf:init!) - (img:init! '(png)) - - ;; 2. Create window + renderer - (game-window-set! game - (sdl2:create-window! (game-title game) 'centered 'centered - (game-width game) (game-height game) '())) - (game-renderer-set! game - (sdl2:create-renderer! (game-window game) -1 '(accelerated vsync))) - - ;; 3. preload hook - (when (game-preload-hook game) ((game-preload-hook game) game)) - - ;; 4. create hook - (when (game-create-hook game) ((game-create-hook game) game)) - - ;; 5. Frame loop - (let loop ((last-ticks (sdl2:get-ticks))) - (let* ((now (sdl2:get-ticks)) - (dt (- now last-ticks)) - (events (sdl2:collect-events!)) - (input (input-state-update (game-input game) events (game-input-config game)))) - (game-input-set! game input) - (unless (input-held? input 'quit) - (when (game-update-hook game) ((game-update-hook game) game dt)) - (sdl2:render-clear! (game-renderer game)) - (when (game-scene game) - (render-scene! (game-renderer game) (game-scene game))) - (when (game-render-hook game) ((game-render-hook game) game)) - (sdl2:render-present! (game-renderer game)) - (sdl2:delay! (game-frame-delay game)) - (loop now)))) - - ;; 6. Cleanup - (sdl2:destroy-window! (game-window game)) - (sdl2:quit!)) -``` - -**Notes:** -- `sdl2:collect-events!` pumps the SDL2 event queue and returns a list of events (from the sdl2 egg). -- `input-state-update` is the existing function in `input.scm`: `(input-state-update state events config)`. It is purely functional — it returns a new `input-state` record and does not mutate the existing one. -- `create-input-state` in `input.scm` takes one argument: `(create-input-state config)` — it initialises all actions to `#f`. -- Quit is detected via `(input-held? input 'quit)` — the `'quit` action is in `*default-input-config*` mapped to `escape` key: `(escape . quit)` in `keyboard-map`. -- `sdl2:render-clear!` and `sdl2:render-present!` are called directly from the sdl2 egg (no wrapper needed). -- The loop exits when `input-held?` returns true for `'quit`. No user-settable `game-running?` flag in this milestone. - ---- - -## `render:` Hook Semantics - -Following Phaser 2.x: the engine draws the full scene first via `render-scene!` (tilemap layers + entities), then calls the user's `render:` hook. The hook is for post-render overlays only — debug info, HUD elements, custom visuals. It does not replace or suppress the engine's render pass. - ---- - -## Physics - -Physics (`physics.scm`) is a library, not a pipeline. `game-run!` does not call any physics function automatically. Users who want physics `(require-extension downstroke/physics)` and call functions from their `update:` hook. - ---- - -## Public Accessors - -All `game-*` readers and `game-*-set!` mutators are generated by `defstruct` and exported. Additional convenience accessors: - -| Accessor | Description | -|---|---| -| `game-renderer` | SDL2 renderer — available from `preload:` onward | -| `game-window` | SDL2 window | -| `game-scene` / `game-scene-set!` | Current scene struct; user sets this in `create:` | -| `game-camera` | Convenience: `(scene-camera (game-scene game))` — derived, not a struct field. Only valid after `create:` sets a scene; calling before that is an error. | -| `game-assets` | Full asset registry hash table | -| `game-input` | Current `input-state` record | -| `game-input-config` | Current `input-config` record | -| `(game-asset game key)` | Convenience: `(asset-ref (game-assets game) key)` | -| `(game-asset-set! game key val)` | Convenience: `(asset-set! (game-assets game) key val)` | - -`game-camera` is not a struct field — it is a derived accessor: -```scheme -(define (game-camera game) - (scene-camera (game-scene game))) -``` - ---- - -## Scene-Level Preloading - -Out of scope for this milestone. A `preload:` slot will be reserved in the scene struct when `scene-loader.scm` is implemented (Milestone 7), and the two-level asset registry (global + per-scene) will be designed then. - ---- - -## macroknight Port - -macroknight's `game.scm` must be ported to use `make-game` + `game-run!` as part of this milestone. It is the primary integration test. - -**What moves to the engine:** SDL2 init block (lines 87–137), the main loop (line 659+), and all frame-level rendering. - -**What stays in macroknight `game.scm`:** Only the three lifecycle hooks and `game-run!`: -- `preload:` — load fonts, sounds, tilemaps (currently scattered across lines 87–137) -- `create:` — build initial scene (currently `make-initial-game-state`, line 196) -- `update:` — game logic, input dispatch, physics calls (currently the per-frame update functions) - -**Acceptance criterion:** macroknight compiles (`make` succeeds), the game runs and plays correctly (title screen → stage select → gameplay), and `game.scm` is ≤50 lines with no SDL2 init or frame-loop boilerplate. - ---- - -## Out of Scope - -- Scene-level preloading (Milestone 7) -- Asset-type-specific load functions (`game-load-tilemap!`, `game-load-font!`, etc.) — Milestone 6 -- `game-running?` quit flag -- Scene state machine (Milestone 9) -- AI tag lookup (Milestone 10) -- Extraction of `animation`, `ai`, `prefabs`, `mixer`, `sound` modules into downstroke |
