8f0569766c
GiftBox auto-reset, Balloon pop/respawn, Cake cut/reset, chair snap points. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
153 lines
5.5 KiB
Markdown
153 lines
5.5 KiB
Markdown
# 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).
|