Compare commits
5 Commits
1d65bf21dc
...
8f5d7ed592
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f5d7ed592 | |||
| 48c7e96b38 | |||
| 3189703d24 | |||
| c2edaf2761 | |||
| 43a7e6bde4 |
@@ -0,0 +1,347 @@
|
||||
# Sprint 20 — Navigation Integration Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Wire RoomNavigator → GameState → AudioManager so floor music switches automatically when the player navigates rooms. Fix the AudioManager.DEFAULT_MUSIC_VOLUME crash. Restore camera to saved room on game load.
|
||||
|
||||
**Architecture:** `GameState.set_current_room()` emits `state_changed`, which AudioManager already observes. `RoomNavigator` calls `set_current_room()` after every navigation. `main.gd` calls `RoomNavigator.go_to_room_by_name()` on start to restore camera. `AudioManager` gets the missing `DEFAULT_MUSIC_VOLUME` constant.
|
||||
|
||||
**Tech Stack:** GDScript 4 (static types), GUT v9.6.0.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: GameState.set_current_room + AudioManager constant + tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/autoload/GameState.gd`
|
||||
- Modify: `scripts/autoload/AudioManager.gd`
|
||||
- Modify: `test/unit/test_game_state.gd` (append 2 tests)
|
||||
- Modify: `test/unit/test_audio_manager.gd` (append 1 test)
|
||||
|
||||
- [ ] **Step 1: Write failing tests**
|
||||
|
||||
Append to `test/unit/test_game_state.gd`:
|
||||
|
||||
```gdscript
|
||||
func test_set_current_room_updates_value() -> void:
|
||||
GameState.set_current_room("xray")
|
||||
assert_eq(GameState.current_room, "xray")
|
||||
GameState.set_current_room("reception")
|
||||
|
||||
|
||||
func test_set_current_room_emits_state_changed() -> void:
|
||||
var signal_count: int = 0
|
||||
GameState.state_changed.connect(func() -> void: signal_count += 1, CONNECT_ONE_SHOT)
|
||||
GameState.set_current_room("pharmacy")
|
||||
assert_eq(signal_count, 1)
|
||||
GameState.set_current_room("reception")
|
||||
```
|
||||
|
||||
Append to `test/unit/test_audio_manager.gd`:
|
||||
|
||||
```gdscript
|
||||
func test_default_music_volume_constant_is_0_6() -> void:
|
||||
assert_eq(AudioManager.DEFAULT_MUSIC_VOLUME, 0.6)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → verify FAIL**
|
||||
|
||||
```bash
|
||||
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit --path "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-20-navigation-integration"
|
||||
```
|
||||
|
||||
Expected: 3 new tests fail — `set_current_room` not defined, `DEFAULT_MUSIC_VOLUME` not defined.
|
||||
|
||||
- [ ] **Step 3: Add set_current_room to GameState.gd**
|
||||
|
||||
Add after `clear_chest_state`:
|
||||
|
||||
```gdscript
|
||||
func set_current_room(room: String) -> void:
|
||||
current_room = room
|
||||
state_changed.emit()
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add DEFAULT_MUSIC_VOLUME to AudioManager.gd**
|
||||
|
||||
Add after the `CROSSFADE_DURATION` constant:
|
||||
|
||||
```gdscript
|
||||
const DEFAULT_MUSIC_VOLUME: float = 0.6
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run → verify PASS**
|
||||
|
||||
```bash
|
||||
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit --path "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-20-navigation-integration"
|
||||
```
|
||||
|
||||
Expected: all previous tests plus 3 new tests pass. Total ≥ 212.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/autoload/GameState.gd scripts/autoload/AudioManager.gd test/unit/test_game_state.gd test/unit/test_audio_manager.gd
|
||||
git commit -m "feat(nav): add GameState.set_current_room and AudioManager.DEFAULT_MUSIC_VOLUME"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: RoomNavigator room names + navigation wiring + tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/systems/room_navigator.gd`
|
||||
- Create: `test/unit/test_room_navigator.gd`
|
||||
|
||||
- [ ] **Step 1: Write failing tests**
|
||||
|
||||
Create `test/unit/test_room_navigator.gd`:
|
||||
|
||||
```gdscript
|
||||
## Tests for RoomNavigator room name lookup and go_to_room_by_name.
|
||||
extends GutTest
|
||||
|
||||
|
||||
func test_room_names_dict_has_eleven_entries() -> void:
|
||||
assert_eq(RoomNavigator._ROOM_NAMES.size(), 11)
|
||||
|
||||
|
||||
func test_get_room_name_floor0_room0_is_reception() -> void:
|
||||
assert_eq(RoomNavigator.get_room_name(0, 0), "reception")
|
||||
|
||||
|
||||
func test_get_room_name_floor0_room3_is_emergency() -> void:
|
||||
assert_eq(RoomNavigator.get_room_name(0, 3), "emergency")
|
||||
|
||||
|
||||
func test_get_room_name_floor1_room2_is_lab() -> void:
|
||||
assert_eq(RoomNavigator.get_room_name(1, 2), "lab")
|
||||
|
||||
|
||||
func test_get_room_name_floor2_room1_is_delivery_room() -> void:
|
||||
assert_eq(RoomNavigator.get_room_name(2, 1), "delivery_room")
|
||||
|
||||
|
||||
func test_get_room_name_unknown_returns_empty_string() -> void:
|
||||
assert_eq(RoomNavigator.get_room_name(99, 0), "")
|
||||
assert_eq(RoomNavigator.get_room_name(0, 99), "")
|
||||
|
||||
|
||||
func test_go_to_room_by_name_garden_party_sets_is_at_home() -> void:
|
||||
RoomNavigator.go_to_room_by_name("garden_party")
|
||||
assert_true(RoomNavigator.is_at_home())
|
||||
RoomNavigator._is_at_home = false
|
||||
|
||||
|
||||
func test_go_to_room_by_name_unknown_is_noop() -> void:
|
||||
var floor_before: int = RoomNavigator.get_current_floor()
|
||||
RoomNavigator.go_to_room_by_name("nonexistent_room_xyz")
|
||||
assert_eq(RoomNavigator.get_current_floor(), floor_before)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → verify FAIL**
|
||||
|
||||
```bash
|
||||
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless --import --path "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-20-navigation-integration"
|
||||
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit --path "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-20-navigation-integration"
|
||||
```
|
||||
|
||||
Expected: 8 new tests fail — `_ROOM_NAMES`, `get_room_name`, `go_to_room_by_name` not defined.
|
||||
|
||||
- [ ] **Step 3: Update room_navigator.gd**
|
||||
|
||||
Full replacement:
|
||||
|
||||
```gdscript
|
||||
## RoomNavigator — autoload that moves the Camera2D smoothly between hospital floors, rooms, and the home/garden area.
|
||||
extends Node
|
||||
|
||||
signal room_changed(floor_index: int, room_index: int)
|
||||
signal home_entered()
|
||||
signal hospital_entered()
|
||||
|
||||
const FLOOR_HEIGHT: float = 720.0
|
||||
const ROOM_WIDTH: float = 1280.0
|
||||
const HOME_CAMERA_X: float = 640.0
|
||||
const HOME_CAMERA_Y: float = 1080.0
|
||||
const CAMERA_TWEEN_DURATION: float = 0.6
|
||||
const HOME_ROOM_NAME: String = "garden_party"
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
var _current_floor: int = 0
|
||||
var _current_room: int = 0
|
||||
var _is_at_home: bool = false
|
||||
var _camera: Camera2D
|
||||
var _active_tween: Tween
|
||||
|
||||
|
||||
func initialize(camera: Camera2D) -> void:
|
||||
_camera = camera
|
||||
|
||||
|
||||
func go_to_floor(floor_index: int) -> void:
|
||||
go_to_room(floor_index, 0)
|
||||
|
||||
|
||||
func go_to_room(floor_index: int, room_index: int) -> void:
|
||||
if _camera == null:
|
||||
return
|
||||
if not _is_at_home and floor_index == _current_floor and room_index == _current_room:
|
||||
return
|
||||
_is_at_home = false
|
||||
_current_floor = floor_index
|
||||
_current_room = room_index
|
||||
var room_name: String = _ROOM_NAMES.get(Vector2i(floor_index, room_index), "")
|
||||
if not room_name.is_empty():
|
||||
GameState.set_current_room(room_name)
|
||||
var target_x: float = room_index * ROOM_WIDTH + ROOM_WIDTH * 0.5
|
||||
var target_y: float = floor_index * -FLOOR_HEIGHT + FLOOR_HEIGHT * 0.5
|
||||
if _active_tween != null:
|
||||
_active_tween.kill()
|
||||
_active_tween = create_tween()
|
||||
_active_tween.set_ease(Tween.EASE_IN_OUT)
|
||||
_active_tween.set_trans(Tween.TRANS_SINE)
|
||||
_active_tween.tween_property(_camera, "position", Vector2(target_x, target_y), CAMERA_TWEEN_DURATION)
|
||||
_active_tween.finished.connect(func() -> void: room_changed.emit(floor_index, room_index))
|
||||
|
||||
|
||||
func go_to_home() -> void:
|
||||
if _camera == null:
|
||||
return
|
||||
if _is_at_home:
|
||||
return
|
||||
_is_at_home = true
|
||||
GameState.set_current_room(HOME_ROOM_NAME)
|
||||
if _active_tween != null:
|
||||
_active_tween.kill()
|
||||
_active_tween = create_tween()
|
||||
_active_tween.set_ease(Tween.EASE_IN_OUT)
|
||||
_active_tween.set_trans(Tween.TRANS_SINE)
|
||||
_active_tween.tween_property(_camera, "position", Vector2(HOME_CAMERA_X, HOME_CAMERA_Y), CAMERA_TWEEN_DURATION)
|
||||
_active_tween.finished.connect(func() -> void: home_entered.emit())
|
||||
|
||||
|
||||
func go_to_hospital() -> void:
|
||||
if _camera == null:
|
||||
return
|
||||
if not _is_at_home:
|
||||
return
|
||||
_is_at_home = false
|
||||
var room_name: String = _ROOM_NAMES.get(Vector2i(_current_floor, _current_room), "")
|
||||
if not room_name.is_empty():
|
||||
GameState.set_current_room(room_name)
|
||||
var target_x: float = _current_room * ROOM_WIDTH + ROOM_WIDTH * 0.5
|
||||
var target_y: float = _current_floor * -FLOOR_HEIGHT + FLOOR_HEIGHT * 0.5
|
||||
if _active_tween != null:
|
||||
_active_tween.kill()
|
||||
_active_tween = create_tween()
|
||||
_active_tween.set_ease(Tween.EASE_IN_OUT)
|
||||
_active_tween.set_trans(Tween.TRANS_SINE)
|
||||
_active_tween.tween_property(_camera, "position", Vector2(target_x, target_y), CAMERA_TWEEN_DURATION)
|
||||
_active_tween.finished.connect(func() -> void: hospital_entered.emit())
|
||||
|
||||
|
||||
func go_to_room_by_name(room_name: String) -> void:
|
||||
if room_name == HOME_ROOM_NAME:
|
||||
go_to_home()
|
||||
return
|
||||
for key: Vector2i in _ROOM_NAMES:
|
||||
if _ROOM_NAMES[key] == room_name:
|
||||
go_to_room(key.x, key.y)
|
||||
return
|
||||
|
||||
|
||||
func get_room_name(floor_index: int, room_index: int) -> String:
|
||||
return _ROOM_NAMES.get(Vector2i(floor_index, room_index), "")
|
||||
|
||||
|
||||
func get_current_floor() -> int:
|
||||
return _current_floor
|
||||
|
||||
|
||||
func get_current_room() -> int:
|
||||
return _current_room
|
||||
|
||||
|
||||
func is_at_home() -> bool:
|
||||
return _is_at_home
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run → verify PASS**
|
||||
|
||||
```bash
|
||||
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit --path "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-20-navigation-integration"
|
||||
```
|
||||
|
||||
Expected: all tests pass including 8 new RoomNavigator tests. Total ≥ 220.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/systems/room_navigator.gd test/unit/test_room_navigator.gd
|
||||
git commit -m "feat(nav): wire RoomNavigator to GameState.set_current_room and add room name lookup"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: main.gd camera restore on load
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/main/main.gd`
|
||||
|
||||
No new tests — camera tween is untestable (visual). Verified manually.
|
||||
|
||||
- [ ] **Step 1: Update main.gd**
|
||||
|
||||
Replace the existing file:
|
||||
|
||||
```gdscript
|
||||
## Main — scene root: wires up RoomNavigator and restores saved character positions.
|
||||
extends Node2D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
RoomNavigator.initialize($Camera2D)
|
||||
SaveManager.load_game()
|
||||
AudioManager.set_music_volume(GameState.music_volume)
|
||||
AudioManager.set_sfx_volume(GameState.sfx_volume)
|
||||
_apply_saved_state()
|
||||
RoomNavigator.go_to_room_by_name(GameState.current_room)
|
||||
|
||||
|
||||
func _apply_saved_state() -> void:
|
||||
for character in $Characters.get_children():
|
||||
if character is Character and character.data != null:
|
||||
if GameState.has_character_position(character.data.id):
|
||||
character.global_position = GameState.get_character_position(character.data.id)
|
||||
```
|
||||
|
||||
The only change: `RoomNavigator.go_to_room_by_name(GameState.current_room)` added at the end of `_ready()`. On a fresh game, `GameState.current_room = "reception"` so the camera starts at Floor 0, Reception — the correct default. On subsequent loads, it restores to wherever the player was.
|
||||
|
||||
- [ ] **Step 2: Run full test suite**
|
||||
|
||||
```bash
|
||||
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit --path "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-20-navigation-integration"
|
||||
```
|
||||
|
||||
Expected: all tests pass. No regressions.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/main/main.gd
|
||||
git commit -m "feat(nav): restore camera to saved room on game load"
|
||||
```
|
||||
@@ -0,0 +1,99 @@
|
||||
# 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
|
||||
@@ -3,6 +3,7 @@
|
||||
extends Node
|
||||
|
||||
const CROSSFADE_DURATION: float = 0.8
|
||||
const DEFAULT_MUSIC_VOLUME: float = 0.6
|
||||
|
||||
const _MUSIC_MAP: Dictionary = {
|
||||
0: "res://assets/audio/music/floor_0.ogg",
|
||||
|
||||
@@ -73,6 +73,11 @@ func clear_chest_state(chest_id: String) -> void:
|
||||
state_changed.emit()
|
||||
|
||||
|
||||
func set_current_room(room: String) -> void:
|
||||
current_room = room
|
||||
state_changed.emit()
|
||||
|
||||
|
||||
func get_save_data() -> Dictionary:
|
||||
var positions: Dictionary = {}
|
||||
for key: String in _character_positions:
|
||||
|
||||
@@ -8,6 +8,7 @@ func _ready() -> void:
|
||||
AudioManager.set_music_volume(GameState.music_volume)
|
||||
AudioManager.set_sfx_volume(GameState.sfx_volume)
|
||||
_apply_saved_state()
|
||||
RoomNavigator.go_to_room_by_name(GameState.current_room)
|
||||
|
||||
|
||||
func _apply_saved_state() -> void:
|
||||
|
||||
@@ -10,6 +10,21 @@ const ROOM_WIDTH: float = 1280.0
|
||||
const HOME_CAMERA_X: float = 640.0
|
||||
const HOME_CAMERA_Y: float = 1080.0
|
||||
const CAMERA_TWEEN_DURATION: float = 0.6
|
||||
const HOME_ROOM_NAME: String = "garden_party"
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
var _current_floor: int = 0
|
||||
var _current_room: int = 0
|
||||
@@ -27,13 +42,16 @@ func go_to_floor(floor_index: int) -> void:
|
||||
|
||||
|
||||
func go_to_room(floor_index: int, room_index: int) -> void:
|
||||
if _camera == null:
|
||||
return
|
||||
if not _is_at_home and floor_index == _current_floor and room_index == _current_room:
|
||||
return
|
||||
_is_at_home = false
|
||||
_current_floor = floor_index
|
||||
_current_room = room_index
|
||||
var room_name: String = _ROOM_NAMES.get(Vector2i(floor_index, room_index), "")
|
||||
if not room_name.is_empty():
|
||||
GameState.set_current_room(room_name)
|
||||
if _camera == null:
|
||||
return
|
||||
var target_x: float = room_index * ROOM_WIDTH + ROOM_WIDTH * 0.5
|
||||
var target_y: float = floor_index * -FLOOR_HEIGHT + FLOOR_HEIGHT * 0.5
|
||||
if _active_tween != null:
|
||||
@@ -46,11 +64,12 @@ func go_to_room(floor_index: int, room_index: int) -> void:
|
||||
|
||||
|
||||
func go_to_home() -> void:
|
||||
if _camera == null:
|
||||
return
|
||||
if _is_at_home:
|
||||
return
|
||||
_is_at_home = true
|
||||
GameState.set_current_room(HOME_ROOM_NAME)
|
||||
if _camera == null:
|
||||
return
|
||||
if _active_tween != null:
|
||||
_active_tween.kill()
|
||||
_active_tween = create_tween()
|
||||
@@ -61,11 +80,14 @@ func go_to_home() -> void:
|
||||
|
||||
|
||||
func go_to_hospital() -> void:
|
||||
if _camera == null:
|
||||
return
|
||||
if not _is_at_home:
|
||||
return
|
||||
_is_at_home = false
|
||||
var room_name: String = _ROOM_NAMES.get(Vector2i(_current_floor, _current_room), "")
|
||||
if not room_name.is_empty():
|
||||
GameState.set_current_room(room_name)
|
||||
if _camera == null:
|
||||
return
|
||||
var target_x: float = _current_room * ROOM_WIDTH + ROOM_WIDTH * 0.5
|
||||
var target_y: float = _current_floor * -FLOOR_HEIGHT + FLOOR_HEIGHT * 0.5
|
||||
if _active_tween != null:
|
||||
@@ -77,6 +99,20 @@ func go_to_hospital() -> void:
|
||||
_active_tween.finished.connect(func() -> void: hospital_entered.emit())
|
||||
|
||||
|
||||
func go_to_room_by_name(room_name: String) -> void:
|
||||
if room_name == HOME_ROOM_NAME:
|
||||
go_to_home()
|
||||
return
|
||||
for key: Vector2i in _ROOM_NAMES:
|
||||
if _ROOM_NAMES[key] == room_name:
|
||||
go_to_room(key.x, key.y)
|
||||
return
|
||||
|
||||
|
||||
func get_room_name(floor_index: int, room_index: int) -> String:
|
||||
return _ROOM_NAMES.get(Vector2i(floor_index, room_index), "")
|
||||
|
||||
|
||||
func get_current_floor() -> int:
|
||||
return _current_floor
|
||||
|
||||
|
||||
@@ -69,3 +69,7 @@ func test_music_map_has_all_four_floors() -> void:
|
||||
assert_true(AudioManager._MUSIC_MAP.has(1))
|
||||
assert_true(AudioManager._MUSIC_MAP.has(2))
|
||||
assert_true(AudioManager._MUSIC_MAP.has(3))
|
||||
|
||||
|
||||
func test_default_music_volume_constant_is_0_6() -> void:
|
||||
assert_eq(AudioManager.DEFAULT_MUSIC_VOLUME, 0.6)
|
||||
|
||||
@@ -181,3 +181,16 @@ func test_apply_save_data_restores_chest_state() -> void:
|
||||
}
|
||||
GameState.apply_save_data(data)
|
||||
assert_eq(GameState.get_chest_state("reception_desk_test"), ["clipboard", "pen"])
|
||||
|
||||
|
||||
func test_set_current_room_updates_value() -> void:
|
||||
GameState.set_current_room("xray")
|
||||
assert_eq(GameState.current_room, "xray")
|
||||
GameState.set_current_room("reception")
|
||||
|
||||
|
||||
func test_set_current_room_emits_state_changed() -> void:
|
||||
watch_signals(GameState)
|
||||
GameState.set_current_room("pharmacy")
|
||||
assert_signal_emitted(GameState, "state_changed")
|
||||
GameState.set_current_room("reception")
|
||||
|
||||
@@ -74,13 +74,13 @@ func test_go_to_floor_does_not_emit_if_already_on_floor_room_zero() -> void:
|
||||
assert_signal_not_emitted(_nav, "room_changed")
|
||||
|
||||
|
||||
func test_initialize_with_null_camera_prevents_state_update() -> void:
|
||||
func test_initialize_with_null_camera_still_updates_state() -> void:
|
||||
var nav: Node = RoomNavigatorScript.new()
|
||||
add_child_autofree(nav)
|
||||
nav.initialize(null)
|
||||
nav.go_to_room(1, 2)
|
||||
assert_eq(nav.get_current_floor(), 0)
|
||||
assert_eq(nav.get_current_room(), 0)
|
||||
assert_eq(nav.get_current_floor(), 1)
|
||||
assert_eq(nav.get_current_room(), 2)
|
||||
|
||||
|
||||
func test_multiple_room_changes_update_state() -> void:
|
||||
@@ -138,3 +138,40 @@ func test_go_to_room_after_home_clears_is_at_home() -> void:
|
||||
_nav.go_to_home()
|
||||
_nav.go_to_room(0, 0)
|
||||
assert_false(_nav.is_at_home())
|
||||
|
||||
|
||||
func test_room_names_dict_has_eleven_entries() -> void:
|
||||
assert_eq(RoomNavigator._ROOM_NAMES.size(), 11)
|
||||
|
||||
|
||||
func test_get_room_name_floor0_room0_is_reception() -> void:
|
||||
assert_eq(RoomNavigator.get_room_name(0, 0), "reception")
|
||||
|
||||
|
||||
func test_get_room_name_floor0_room3_is_emergency() -> void:
|
||||
assert_eq(RoomNavigator.get_room_name(0, 3), "emergency")
|
||||
|
||||
|
||||
func test_get_room_name_floor1_room2_is_lab() -> void:
|
||||
assert_eq(RoomNavigator.get_room_name(1, 2), "lab")
|
||||
|
||||
|
||||
func test_get_room_name_floor2_room1_is_delivery_room() -> void:
|
||||
assert_eq(RoomNavigator.get_room_name(2, 1), "delivery_room")
|
||||
|
||||
|
||||
func test_get_room_name_unknown_returns_empty_string() -> void:
|
||||
assert_eq(RoomNavigator.get_room_name(99, 0), "")
|
||||
assert_eq(RoomNavigator.get_room_name(0, 99), "")
|
||||
|
||||
|
||||
func test_go_to_room_by_name_garden_party_sets_is_at_home() -> void:
|
||||
RoomNavigator.go_to_room_by_name("garden_party")
|
||||
assert_true(RoomNavigator.is_at_home())
|
||||
RoomNavigator._is_at_home = false
|
||||
|
||||
|
||||
func test_go_to_room_by_name_unknown_is_noop() -> void:
|
||||
var floor_before: int = RoomNavigator.get_current_floor()
|
||||
RoomNavigator.go_to_room_by_name("nonexistent_room_xyz")
|
||||
assert_eq(RoomNavigator.get_current_floor(), floor_before)
|
||||
|
||||
Reference in New Issue
Block a user