diff --git a/docs/superpowers/specs/2026-05-11-sprint-14-garden-party.md b/docs/superpowers/specs/2026-05-11-sprint-14-garden-party.md new file mode 100644 index 0000000..4d21eb6 --- /dev/null +++ b/docs/superpowers/specs/2026-05-11-sprint-14-garden-party.md @@ -0,0 +1,152 @@ +# Sprint 14 — Garden Party Spec + +## Goal + +Make the GardenParty scene a fully playable sandbox room with three interactive objects +(GiftBox auto-reset, Balloon pop/respawn, Cake cut/reset) and character snap-to-chair +at the party table. + +--- + +## Scope + +**In scope:** +- `gift_box.gd`: add `RESETTING` state (auto-reset 3 s after opening) +- `balloon.gd`: new script — tap to pop, auto-respawn after 5 s +- `cake.gd`: new script — tap to cut slice, auto-reset after 3 s +- `GardenParty.tscn`: replace GiftBox3 with Cake, add Cake node structure, add chair + ColorRects under snap points, attach balloon.gd to existing Balloon nodes +- Tests: extend `test_gift_box.gd`, add `test_balloon.gd`, add `test_cake.gd` + +**Out of scope:** +- New SFX keys (reuse `object_tap` for all three objects) +- Navigation changes (HomeButtonToGarden and HomeButtonReturn already wired) +- Additional characters beyond Bunny1 already in Main.tscn + +--- + +## Object Mechanics + +### GiftBox — RESETTING state + +New state added to the existing `State` enum: + +``` +CLOSED → OPENING → RESETTING → CLOSED +``` + +- `_on_lid_opened()`: state → `RESETTING`, gift fades in (existing tween), inline 3 s + tween interval runs in parallel → `_start_close_lid()` +- `_start_close_lid()`: lid tweens back (fade-in alpha 0→1, slide y back to 0) → + `_on_reset_complete()` +- `_on_reset_complete()`: state → `CLOSED` +- `OPEN` state removed — `RESETTING` covers "gift visible + waiting" +- New internal methods: `_start_close_lid()`, `_on_reset_complete()` +- Constant: `RESET_DELAY: float = 3.0` + +### Balloon + +New file: `scripts/objects/balloon.gd` +New scene attachment: `balloon.gd` attached to existing `Balloon1` / `Balloon2` nodes in +`GardenParty.tscn`. + +``` +IDLE → POPPING → POPPED → RESPAWNING → IDLE +``` + +State transitions: +- Touch input on balloon while `IDLE` → `_start_pop()` +- `_start_pop()`: `AudioManager.play_sfx("object_tap")`, state → `POPPING`, scale tween to + `Vector2.ZERO` (0.15 s, EASE_IN, TRANS_BACK) → `_on_pop_complete()` +- `_on_pop_complete()`: state → `POPPED`, starts 5 s tween interval → `_start_respawn()` +- `_start_respawn()`: state → `RESPAWNING`, scale tween to `Vector2.ONE` (0.3 s, EASE_OUT, + TRANS_BACK) → `_on_respawn_complete()` +- `_on_respawn_complete()`: state → `IDLE` + +Input: `_input(event)` — same touch-rect pattern as GiftBox, half-size 20 px (balloon is +small). + +Constants: `POP_DURATION`, `RESPAWN_DURATION`, `RESPAWN_DELAY`. + +### Cake + +New file: `scripts/objects/cake.gd` +New node structure in `GardenParty.tscn` (replaces GiftBox3 at x=820, y=464): + +``` +Cake (Node2D, cake.gd) +├── Base (ColorRect, ~80×40 px, warm brown) +└── Slice (ColorRect, ~30×40 px, slightly offset + rotated, lighter brown) +``` + +``` +WHOLE → CUTTING → CUT → RESETTING → WHOLE +``` + +State transitions: +- Touch input on Cake while `WHOLE` → `_start_cutting()` +- `_start_cutting()`: `AudioManager.play_sfx("object_tap")`, state → `CUTTING`, + tween `Slice.modulate.a` 1→0 (0.3 s, EASE_OUT) → `_on_cut_complete()` +- `_on_cut_complete()`: state → `CUT`, 3 s tween interval → `_start_reset()` +- `_start_reset()`: state → `RESETTING`, tween `Slice.modulate.a` 0→1 (0.3 s, EASE_IN) + → `_on_reset_complete()` +- `_on_reset_complete()`: state → `WHOLE` + +Input: touch rect `BUTTON_HALF_WIDTH = 40`, `BUTTON_HALF_HEIGHT = 20`. + +Constants: `CUT_DURATION`, `RESET_DELAY = 3.0`, `RESET_DURATION`. + +--- + +## Scene Changes (GardenParty.tscn) + +| Change | Detail | +|---|---| +| Remove `GiftBox3` | Was at position Vector2(820, 464) | +| Add `Cake` | Node2D at Vector2(820, 464), `cake.gd`, child nodes Base + Slice | +| Attach `balloon.gd` | To existing `Balloon1` and `Balloon2` ColorRect nodes | +| Add `ChairLeft` | ColorRect ~60×20 px, brown, under `SnapTableLeft` (x=530, y=480) | +| Add `ChairRight` | ColorRect ~60×20 px, brown, under `SnapTableRight` (x=750, y=480) | + +Navigation and snap-point logic unchanged. + +--- + +## Tests + +### `test_gift_box.gd` (extend existing) + +| Test | Assertion | +|---|---| +| `test_state_becomes_resetting_after_lid_opened` | Call `_on_lid_opened()` → state == `State.RESETTING` | +| `test_state_becomes_closed_after_reset_complete` | Call `_on_reset_complete()` → state == `State.CLOSED` | + +### `test_balloon.gd` (new, extends GutTest) + +| Test | Assertion | +|---|---| +| `test_initial_state_is_idle` | `state == State.IDLE` | +| `test_start_pop_sets_popping` | Call `_start_pop()` → `state == State.POPPING` | +| `test_on_pop_complete_sets_popped` | Call `_on_pop_complete()` → `state == State.POPPED` | +| `test_on_respawn_complete_sets_idle` | Call `_on_respawn_complete()` → `state == State.IDLE` | + +### `test_cake.gd` (new, extends GutTest) + +| Test | Assertion | +|---|---| +| `test_initial_state_is_whole` | `state == State.WHOLE` | +| `test_start_cutting_sets_cutting` | Call `_start_cutting()` → `state == State.CUTTING` | +| `test_on_cut_complete_sets_cut` | Call `_on_cut_complete()` → `state == State.CUT` | +| `test_on_reset_complete_sets_whole` | Call `_on_reset_complete()` → `state == State.WHOLE` | + +--- + +## Architecture Notes + +- All three objects follow the same state-machine pattern as existing objects (`cradle.gd`, + `gift_box.gd`, etc.) — no new abstractions. +- Tween-based animations are not unit-tested (visual only). State transitions triggered by + `tween.finished` callbacks are tested by calling the callbacks directly. +- `AudioServer.get_driver_name() == "Dummy"` guard is already in `AudioManager.play_sfx()` + — no additional guard needed in new scripts. +- `balloon.gd` scales the Node2D itself (no child sprite node needed at placeholder stage).