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) <noreply@anthropic.com>
This commit is contained in:
Harald Hoyer 2026-05-07 09:53:09 +02:00
parent 53938f22b5
commit 365e3a7cc4
14 changed files with 188 additions and 0 deletions

View file

@ -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-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-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). - [ ] [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.

View file

@ -9,3 +9,15 @@ labels: [gameplay, advanced-mechanics]
# GAL-35: Allow the player to shoot a Boss that is holding a captured ship # 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. 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).

View file

@ -9,3 +9,15 @@ labels: [gameplay, advanced-mechanics]
# GAL-36: Implement the logic to free the captured ship upon shooting the Boss # 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. 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.

View file

@ -9,3 +9,18 @@ labels: [gameplay, advanced-mechanics]
# GAL-37: Implement the dual fighter mode (controlling two ships, firing two bullets) # GAL-37: Implement the dual fighter mode (controlling two ships, firing two bullets)
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.

View file

@ -9,3 +9,17 @@ labels: [gameplay, advanced-mechanics]
# GAL-40: Design and implement intricate flight patterns for enemies that do not shoot # 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. 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.

View file

@ -9,3 +9,17 @@ labels: [gameplay, advanced-mechanics]
# GAL-41: Award bonus points for destroying all enemies in the stage # GAL-41: Award bonus points for destroying all enemies in the stage
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.

View file

@ -9,3 +9,17 @@ labels: [polish, visuals]
# GAL-43: Replace placeholder geometric shapes with actual sprites # GAL-43: Replace placeholder geometric shapes with actual sprites
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<Image>`.
- 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).

View file

@ -15,3 +15,14 @@ Wire up audio plugin, sound effects, and background music.
- [ ] [GAL-47](GAL-47.md) — Integrate `bevy_audio`. - [ ] [GAL-47](GAL-47.md) — Integrate `bevy_audio`.
- [ ] [GAL-48](GAL-48.md) — Add sound effects (shooting, explosions, player death, tractor beam, etc.). - [ ] [GAL-48](GAL-48.md) — Add sound effects (shooting, explosions, player death, tractor beam, etc.).
- [ ] [GAL-49](GAL-49.md) — Add background music. - [ ] [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.

View file

@ -9,3 +9,16 @@ labels: [polish, audio]
# GAL-47: Integrate `bevy_audio` # GAL-47: Integrate `bevy_audio`
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.

View file

@ -9,3 +9,17 @@ labels: [polish, audio]
# GAL-48: Add sound effects (shooting, explosions, player death, tractor beam, etc.) # GAL-48: Add sound effects (shooting, explosions, player death, tractor beam, etc.)
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<PlaySfx>`), 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.

View file

@ -9,3 +9,17 @@ labels: [polish, audio]
# GAL-49: Add background music # GAL-49: Add background music
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.

View file

@ -9,3 +9,17 @@ labels: [polish, ui]
# GAL-52: Display Score, Lives, and Stage on the screen using `bevy_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`. 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<T>` 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.

View file

@ -9,3 +9,18 @@ labels: [polish, ui]
# GAL-53: Implement a High Score system (saving/loading) # GAL-53: Implement a High Score system (saving/loading)
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.

View file

@ -9,3 +9,18 @@ labels: [polish, ui]
# GAL-55: Add a "Press R to Restart" message to the `GameOver` screen and implement restart logic # 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. 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<KeyCode>`; 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.