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

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

View file

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

View file

@ -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<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
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`
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)
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
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.