# Sprint 20 — Navigation-Integration Design Spec ## Goal Close the loop between RoomNavigator, GameState, and AudioManager. Three concrete bugs: 1. `AudioManager.DEFAULT_MUSIC_VOLUME` missing → HUD crashes on music toggle 2. `GameState.current_room` never updated during gameplay → floor music never switches 3. Camera not restored to saved room on game load ## Changes ### GameState Add `set_current_room(room: String) -> void`: ```gdscript func set_current_room(room: String) -> void: current_room = room state_changed.emit() ``` GameState already persists `current_room` in `get_save_data()` / `apply_save_data()`. No version bump needed — structure unchanged. ### RoomNavigator Add room name lookup and call `GameState.set_current_room()` on every navigation: ```gdscript const _ROOM_NAMES: Dictionary = { Vector2i(0, 0): "reception", Vector2i(0, 1): "giftshop", Vector2i(0, 2): "restaurant", Vector2i(0, 3): "emergency", Vector2i(1, 0): "xray", Vector2i(1, 1): "pharmacy", Vector2i(1, 2): "lab", Vector2i(1, 3): "patient_rooms", Vector2i(2, 0): "ultrasound", Vector2i(2, 1): "delivery_room", Vector2i(2, 2): "nursery", } const HOME_ROOM_NAME: String = "garden_party" ``` `go_to_room(floor_index, room_index)` → after setting internal state, call `GameState.set_current_room(_ROOM_NAMES.get(Vector2i(floor_index, room_index), ""))`. `go_to_home()` → `GameState.set_current_room(HOME_ROOM_NAME)`. `go_to_hospital()` → `GameState.set_current_room(_ROOM_NAMES.get(Vector2i(_current_floor, _current_room), ""))`. Add public `get_room_name(floor_index: int, room_index: int) -> String` — returns the room name or `""`. Add `go_to_room_by_name(room_name: String) -> void` — reverse lookup: if `room_name == HOME_ROOM_NAME`, call `go_to_home()`; otherwise search `_ROOM_NAMES` for matching value and call `go_to_room()`. ### AudioManager Add missing constant: ```gdscript const DEFAULT_MUSIC_VOLUME: float = 0.6 ``` No other changes — the existing `_on_game_state_changed()` observer already handles room-based music switching correctly once `GameState.set_current_room()` is wired up. ### main.gd After `SaveManager.load_game()`, restore camera to the saved room: ```gdscript RoomNavigator.go_to_room_by_name(GameState.current_room) ``` (Called without tween wait — camera teleports to saved position on startup, no animation.) To suppress the cross-fade animation on startup (no music cross-fade before the initial room is set), `go_to_room_by_name` passes a flag or AudioManager's `_current_floor` is -1 so the first `play_floor_music` call is treated as initial — this already works correctly since `_current_floor` starts at -1. ## Testing **`test/unit/test_game_state.gd`** — append: - `test_set_current_room_updates_value` — calls `set_current_room("xray")`, asserts `current_room == "xray"` - `test_set_current_room_emits_state_changed` — connects to `state_changed`, calls `set_current_room`, verifies signal fired **`test/unit/test_room_navigator.gd`** — new file: - `test_room_names_dict_has_eleven_entries` - `test_get_room_name_floor0_room0_is_reception` - `test_get_room_name_floor1_room2_is_lab` - `test_get_room_name_floor2_room1_is_delivery_room` - `test_get_room_name_unknown_returns_empty` - `test_go_to_room_by_name_sets_home_for_garden_party` — verifies `is_at_home()` after call - `test_go_to_room_by_name_unknown_is_noop` — no crash, no state change **`test/unit/test_audio_manager.gd`** — append: - `test_default_music_volume_constant_is_0_6` — asserts `AudioManager.DEFAULT_MUSIC_VOLUME == 0.6` ## Out of Scope - Camera pan animation on game restore (instant teleport is sufficient) - Floor 2 home button wiring (navigation_arrow for garden already exists in Main.tscn) - Room-within-floor persistence beyond the existing `current_room` string