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