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