From 365e3a7cc4f1665601a4a6f79f0da5cf1c80cec2 Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Thu, 7 May 2026 09:53:09 +0200 Subject: [PATCH] chore(todo): add acceptance criteria and test hints to open issues Flesh out the 14 still-open issues (GAL-34/35/36/37, 40/41, 43, 46/47/48/49, 52/53/55) with explicit acceptance criteria and concrete integration test hints that reference existing types and headless tooling, so future work on these tickets has a clear definition of done. Co-Authored-By: Claude Opus 4.7 (1M context) --- TODO/GAL-34.md | 11 +++++++++++ TODO/GAL-35.md | 12 ++++++++++++ TODO/GAL-36.md | 12 ++++++++++++ TODO/GAL-37.md | 15 +++++++++++++++ TODO/GAL-40.md | 14 ++++++++++++++ TODO/GAL-41.md | 14 ++++++++++++++ TODO/GAL-43.md | 14 ++++++++++++++ TODO/GAL-46.md | 11 +++++++++++ TODO/GAL-47.md | 13 +++++++++++++ TODO/GAL-48.md | 14 ++++++++++++++ TODO/GAL-49.md | 14 ++++++++++++++ TODO/GAL-52.md | 14 ++++++++++++++ TODO/GAL-53.md | 15 +++++++++++++++ TODO/GAL-55.md | 15 +++++++++++++++ 14 files changed, 188 insertions(+) diff --git a/TODO/GAL-34.md b/TODO/GAL-34.md index 9f9a6c1..04ef2f5 100644 --- a/TODO/GAL-34.md +++ b/TODO/GAL-34.md @@ -15,3 +15,14 @@ Allow the player to rescue a captured ship and operate in dual-fighter mode. - [ ] [GAL-35](GAL-35.md) — Allow the player to shoot a Boss that is holding a captured ship. - [ ] [GAL-36](GAL-36.md) — Implement the logic to free the captured ship upon shooting the Boss. - [ ] [GAL-37](GAL-37.md) — Implement the dual fighter mode (controlling two ships, firing two bullets). + +## Acceptance criteria + +- [ ] End-to-end Galaga rescue flow works: capture → shoot Boss in `ReturningWithCaptive` → freed ship attaches → dual fighter active. +- [ ] Losing the side ship of a dual fighter does **not** subtract a life and does **not** trigger respawn. +- [ ] Score reflects rescue bonus distinct from a normal Boss kill. + +## Integration test hints + +- Scripted scenario test (`tests/dual_fighter.rs`): build a minimal `App`, advance through capture → rescue → side-fighter-loss; assert state and resource values at each step. +- Audit no regressions in `tests/special_stage.rs` style: keep tests deterministic by injecting `Time` and `fastrand` seeds. diff --git a/TODO/GAL-35.md b/TODO/GAL-35.md index 9323e40..a9b0e59 100644 --- a/TODO/GAL-35.md +++ b/TODO/GAL-35.md @@ -9,3 +9,15 @@ labels: [gameplay, advanced-mechanics] # GAL-35: Allow the player to shoot a Boss that is holding a captured ship Allow the player to shoot a Boss that is holding a captured ship. + +## Acceptance criteria + +- [ ] Player bullets register a hit on a Boss whose `EnemyState::ReturningWithCaptive` is active (today they may pass through or be ignored — verify and fix). +- [ ] Bullet despawns on hit like any other collision. +- [ ] Hit awards a distinct rescue-bonus point value (Galaga: 1500/3000 depending on timing) — not the regular Boss 300. +- [ ] No double-counting if multiple bullets connect on the same frame. + +## Integration test hints + +- Spawn a Boss with `EnemyState::ReturningWithCaptive` and a `Captured` player attached; insert a bullet at boss position; tick world one frame; assert: `Score` increased by rescue bonus, bullet entity gone, boss entity gone. +- Negative test: same setup but with boss in `EnemyState::InFormation` — confirm only normal hit/score path runs (no rescue bonus). diff --git a/TODO/GAL-36.md b/TODO/GAL-36.md index af50d2e..996434a 100644 --- a/TODO/GAL-36.md +++ b/TODO/GAL-36.md @@ -9,3 +9,15 @@ labels: [gameplay, advanced-mechanics] # GAL-36: Implement the logic to free the captured ship upon shooting the Boss Implement the logic to free the captured ship upon shooting the Boss. + +## Acceptance criteria + +- [ ] When a Boss in `ReturningWithCaptive` dies, the held player has its `Captured` component removed. +- [ ] Freed ship is positioned next to the active player and tagged for dual-fighter mode (handoff to GAL-37). +- [ ] If the boss is destroyed while still off-screen / in transit, the freed ship is still recovered cleanly (no orphaned `Captured` component, no stuck `TractorBeam`). +- [ ] Existing `handle_captured_player` boss-despawn path still runs (cf. `player.rs:handle_captured_player` — release-and-penalize fallback). + +## Integration test hints + +- Set up boss + captured player (both entities, with `Captured { boss_entity, … }`); fire bullet; tick to collision; assert player still alive, no `Captured` component, `PlayerLives` unchanged. +- Edge case: destroy the boss off-screen — confirm fallback path still works without entering the rescue/dual-fighter branch. diff --git a/TODO/GAL-37.md b/TODO/GAL-37.md index c972530..1430caf 100644 --- a/TODO/GAL-37.md +++ b/TODO/GAL-37.md @@ -9,3 +9,18 @@ labels: [gameplay, advanced-mechanics] # GAL-37: Implement the dual fighter mode (controlling two ships, firing two bullets) Implement the dual fighter mode (controlling two ships, firing two bullets). + +## Acceptance criteria + +- [ ] After a successful rescue, player and side ship move as one rigid body (single horizontal input drives both). +- [ ] Shoot input spawns **two** bullet entities per shot, one from each ship. +- [ ] Side ship has its own collider; an enemy or enemy bullet hitting only the side ship despawns it without losing a life. +- [ ] Hitting the main ship still loses a life as today. +- [ ] When the side ship is gone, behaviour reverts to single-fighter (one bullet per shot). +- [ ] Tractor beam re-capturing the player while in dual mode behaves sensibly (define: side ship lost, main captured). + +## Integration test hints + +- World fixture with `Player` + a `SideFighter` component at +offset; trigger a `Shoot` input; assert exactly two `Bullet` entities exist this frame at expected x-offsets. +- Spawn an `EnemyBullet` at the side-fighter position; tick collision; assert only the side ship despawned, `PlayerLives` unchanged, two-bullet shooting now reverts to one. +- Movement test: drive `move_player` left/right; assert side ship transform tracks main with constant offset. diff --git a/TODO/GAL-40.md b/TODO/GAL-40.md index f820d05..e48bc30 100644 --- a/TODO/GAL-40.md +++ b/TODO/GAL-40.md @@ -9,3 +9,17 @@ labels: [gameplay, advanced-mechanics] # GAL-40: Design and implement intricate flight patterns for enemies that do not shoot Design and implement intricate flight patterns for enemies that do not shoot. + +## Acceptance criteria + +- [ ] On a special stage (`is_special_stage(stage_number) == true`), enemies follow scripted curved paths (e.g. spline / Bezier / Lissajous), not the formation flow. +- [ ] These enemies do **not** call `enemy_shoot` and never spawn `EnemyBullet`. +- [ ] Enemies despawn when their path reaches its end off-screen. +- [ ] At least 2 distinct path shapes used in a single special stage for visual variety. +- [ ] Difficulty / pattern parameters live in `StageConfigurations` (or a new resource) so future stages can compose them. + +## Integration test hints + +- Build a headless `App` with `CurrentStage = 3` (first special stage); tick for N seconds; assert: zero `EnemyBullet` entities ever spawned, all special-stage enemies eventually despawned (count returns to 0). +- Snapshot the path: at fixed intervals record enemy `Transform.translation`; assert curvature (e.g. y is non-monotonic to prove it isn't a straight line). +- Visual smoke test via `nix run .#take-screenshots -- ./target/debug/bglga 3 6 1 ./shots` after triggering a special stage; eyeball that paths look intentional. diff --git a/TODO/GAL-41.md b/TODO/GAL-41.md index 3ee559b..a5bd218 100644 --- a/TODO/GAL-41.md +++ b/TODO/GAL-41.md @@ -9,3 +9,17 @@ labels: [gameplay, advanced-mechanics] # GAL-41: Award bonus points for destroying all enemies in the stage Award bonus points for destroying all enemies in the stage. + +## Acceptance criteria + +- [ ] Bonus is awarded **only** when every enemy spawned during the special stage is destroyed before the stage ends. +- [ ] Partial clears award nothing (no proportional credit). +- [ ] Bonus value is configurable per stage via `StageConfig` (or equivalent), defaulting to a Galaga-style 10000. +- [ ] Bonus applies to `Score` resource on stage transition, not on the last kill (so final HUD reads cleanly). +- [ ] Visible feedback (text overlay or window title) so player knows they earned the bonus. + +## Integration test hints + +- World where `is_special_stage(current) == true` with N enemies; despawn all N via simulated bullet hits; advance to next stage; assert `Score` increased by exactly the configured bonus. +- Negative case: leave 1 enemy alive; advance stage; assert `Score` unchanged by the bonus mechanism. +- Property-style: vary N in {1, 5, 20}; assert bonus paid once, regardless of N. diff --git a/TODO/GAL-43.md b/TODO/GAL-43.md index 07e7a9a..6b0fdfb 100644 --- a/TODO/GAL-43.md +++ b/TODO/GAL-43.md @@ -9,3 +9,17 @@ labels: [polish, visuals] # GAL-43: Replace placeholder geometric shapes with actual sprites Replace placeholder geometric shapes with actual sprites. + +## Acceptance criteria + +- [ ] `Player`, `Bullet`, `EnemyBullet`, and both `EnemyType::Grunt` / `EnemyType::Boss` are rendered via `Sprite` components loaded from `assets/`, not coloured meshes. +- [ ] Asset paths centralised (e.g. constants in `constants.rs`) so swapping art is a one-line change. +- [ ] Hitbox sizes either preserved or recalibrated; existing collision tests still pass. +- [ ] Z-ordering preserved (player above starfield, bullets above enemies, beam below player). +- [ ] Falls back gracefully if an asset is missing (warn, do not panic at runtime). + +## Integration test hints + +- Unit-test `spawn_player` (and equivalents) inserts a `Sprite` component referencing the expected `Handle`. +- Run `nix run .#take-screenshots -- ./target/debug/bglga 3 6 1 ./shots` before and after; visually compare frames for regressions in placement. +- Existing collision tests: ensure they still pass with the new sprite-derived hitbox sizes (or update constants in `constants.rs` and the tests in lockstep). diff --git a/TODO/GAL-46.md b/TODO/GAL-46.md index a4c46a6..e2bfe56 100644 --- a/TODO/GAL-46.md +++ b/TODO/GAL-46.md @@ -15,3 +15,14 @@ Wire up audio plugin, sound effects, and background music. - [ ] [GAL-47](GAL-47.md) — Integrate `bevy_audio`. - [ ] [GAL-48](GAL-48.md) — Add sound effects (shooting, explosions, player death, tractor beam, etc.). - [ ] [GAL-49](GAL-49.md) — Add background music. + +## Acceptance criteria + +- [ ] Audio plugin integrated and SFX wired to ECS events (see GAL-47, GAL-48). +- [ ] Background music tied to `GameState` transitions (see GAL-49). +- [ ] Audio path is *non-fatal* in headless / lavapipe environments — `nix run .#take-screenshots` still works. + +## Integration test hints + +- Run the existing screenshot smoke test with the audio plugin enabled in headless mode; assert no panic and PNGs are produced. +- Tick a representative game scenario (spawn → shoot → kill → die → game over) and assert non-empty stream of audio events without panics. diff --git a/TODO/GAL-47.md b/TODO/GAL-47.md index 9f1c0c6..6e425b4 100644 --- a/TODO/GAL-47.md +++ b/TODO/GAL-47.md @@ -9,3 +9,16 @@ labels: [polish, audio] # GAL-47: Integrate `bevy_audio` Integrate `bevy_audio`. + +## Acceptance criteria + +- [ ] `AudioPlugin` (or feature-equivalent) registered in `App` setup in `lib.rs`. +- [ ] `cargo build` and `cargo run` succeed under `nix develop`. +- [ ] Headless run (no audio device) does **not** panic — required so `take-screenshots` continues to work. +- [ ] One canary sound plays at startup or on a known event to prove the wiring. + +## Integration test hints + +- `cargo test`-level: build the `App` with the audio plugin, run `Startup` schedule, assert no panic. +- Manual: `cargo run` on a dev host with audio, verify audible canary. +- CI: `nix run .#take-screenshots -- ./target/debug/bglga 1 6 1 ./shots` still produces a PNG with audio plugin enabled. diff --git a/TODO/GAL-48.md b/TODO/GAL-48.md index 3a62353..c12312e 100644 --- a/TODO/GAL-48.md +++ b/TODO/GAL-48.md @@ -9,3 +9,17 @@ labels: [polish, audio] # GAL-48: Add sound effects (shooting, explosions, player death, tractor beam, etc.) Add sound effects (shooting, explosions, player death, tractor beam, etc.). + +## Acceptance criteria + +- [ ] Distinct SFX for: player shoot, enemy shoot, enemy explosion, player death, tractor beam start, stage clear. +- [ ] SFX triggered via Bevy events (e.g. `EventWriter`), not direct calls scattered through gameplay systems. +- [ ] No duplicate playback in a single frame for a single source (e.g. one shot = one click). +- [ ] SFX volume independent from music channel (see GAL-49). +- [ ] Asset paths centralised so re-skinning is local to one file. + +## Integration test hints + +- Tick world: simulate shoot input → assert exactly one `PlaySfx(Shoot)` event emitted that frame. +- Despawn an enemy via a bullet collision → assert exactly one `PlaySfx(Explosion)` event. +- Activate a Boss `CaptureBeam` (insert `TractorBeam` component) → assert one `PlaySfx(BeamStart)` event per beam, not per frame the beam exists. diff --git a/TODO/GAL-49.md b/TODO/GAL-49.md index d6b4534..0200c13 100644 --- a/TODO/GAL-49.md +++ b/TODO/GAL-49.md @@ -9,3 +9,17 @@ labels: [polish, audio] # GAL-49: Add background music Add background music. + +## Acceptance criteria + +- [ ] Music begins on entering `GameState::Playing` and stops/pauses on `GameOver` and `StartMenu`. +- [ ] Re-entering `Playing` (after restart) starts exactly one music instance — no overlapping tracks. +- [ ] Music volume control independent from SFX channel. +- [ ] Looping is seamless (no audible gap on loop). +- [ ] Optional: per-stage track override defined in `StageConfig`. + +## Integration test hints + +- State-transition test: `OnEnter(GameState::Playing)` → assert one `BgMusic`-tagged audio entity exists; `OnExit(Playing)` → assert it is gone or paused. +- Restart loop test: Playing → GameOver → restart (GAL-55) → Playing; assert exactly one music entity at the end (no duplication). +- Headless: ensure the music asset loads under `nix develop` without blocking startup. diff --git a/TODO/GAL-52.md b/TODO/GAL-52.md index 34d3350..c6a04da 100644 --- a/TODO/GAL-52.md +++ b/TODO/GAL-52.md @@ -9,3 +9,17 @@ labels: [polish, ui] # GAL-52: Display Score, Lives, and Stage on the screen using `bevy_ui` Display Score, Lives, and Stage on the screen using `bevy_ui`. + +## Acceptance criteria + +- [ ] HUD shows `Score`, `PlayerLives`, and `CurrentStage` as on-screen `Text` nodes via `bevy_ui`. +- [ ] HUD updates each frame the underlying resource changes (use `Changed` filters or run-conditions). +- [ ] HUD is hidden (or not spawned) during `StartMenu`; visible during `Playing` and `GameOver`. +- [ ] Window title is no longer the source of truth for these values (kept only as debug, or removed — pick one). +- [ ] Layout survives window resize without overlapping the play field. + +## Integration test hints + +- Build `App`, mutate `Score` resource, run schedule one tick, query for the score `Text` component, assert its content includes the new score string. +- Transition into `StartMenu`; assert HUD nodes have `Display::None` or are despawned. +- Visual smoke test via `nix run .#take-screenshots` after resource changes; eyeball that text positions are sensible. diff --git a/TODO/GAL-53.md b/TODO/GAL-53.md index e72f729..2919823 100644 --- a/TODO/GAL-53.md +++ b/TODO/GAL-53.md @@ -9,3 +9,18 @@ labels: [polish, ui] # GAL-53: Implement a High Score system (saving/loading) Implement a High Score system (saving/loading). + +## Acceptance criteria + +- [ ] High score persists to disk under a platform-appropriate path (e.g. `dirs::data_dir()/bglga/highscore.json`). +- [ ] Loaded on startup into a `HighScore` resource; defaults to 0 if the file is absent or corrupt (no panic). +- [ ] Updated only when `Score > HighScore` on entering `GameOver`, then persisted. +- [ ] Visible on Start Menu (best so far) and Game Over (best vs current). +- [ ] Save path overridable via env var or test-only setter so tests don't pollute the user's real config. + +## Integration test hints + +- Use a tmp dir override; pre-write `{"high_score": 5000}`; build `App`; assert `HighScore` resource = 5000. +- Pre-write malformed JSON; assert loader falls back to 0 and doesn't panic. +- Run scenario: set `Score = 7000`; transition to `GameOver`; reload from disk; assert persisted value = 7000. +- Concurrency / crash safety: write via temp file + rename, not in-place truncate. diff --git a/TODO/GAL-55.md b/TODO/GAL-55.md index 12e197e..4f48d17 100644 --- a/TODO/GAL-55.md +++ b/TODO/GAL-55.md @@ -9,3 +9,18 @@ labels: [polish, ui] # GAL-55: Add a "Press R to Restart" message to the `GameOver` screen and implement restart logic Add a "Press R to Restart" message to the `GameOver` screen and implement restart logic. + +## Acceptance criteria + +- [ ] "Press R to Restart" rendered as part of `GameOverUI` (`RestartMessage` component already exists in `components.rs`). +- [ ] Pressing R while in `GameState::GameOver` transitions back to `GameState::Playing`. +- [ ] On restart, these reset to defaults: `Score`, `PlayerLives`, `CurrentStage`, `FormationState`, `AttackDiveTimer`, `EnemySpawnTimer`, `RestartPressed`. +- [ ] On restart, all `Enemy`, `Bullet`, `EnemyBullet`, `Explosion`, and `TractorBeam` entities are despawned. +- [ ] Player respawns at default position with invincibility window. +- [ ] R is ignored in `Playing` and `StartMenu` states. + +## Integration test hints + +- Build `App` in `GameOver`; insert a fake `Score = 1234`, `CurrentStage = 4`; simulate `KeyCode::KeyR` press via `ButtonInput`; tick; assert state is `Playing` and resources reset. +- Pre-spawn enemies/bullets/explosions before restart; after restart tick, assert their entity counts are zero. +- Negative test: send `KeyR` while in `Playing`; assert no resource reset and no state change.