Files
Cozypaw-Hospital/docs/superpowers/plans/2026-05-10-sprint-20-navigation-integration.md
T
2026-05-10 20:50:13 +02:00

11 KiB

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:

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:

func test_default_music_volume_constant_is_0_6() -> void:
	assert_eq(AudioManager.DEFAULT_MUSIC_VOLUME, 0.6)
  • Step 2: Run → verify FAIL
"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:

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:

const DEFAULT_MUSIC_VOLUME: float = 0.6
  • Step 5: Run → verify PASS
"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
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:

## 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
"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:

## 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
"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
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:

## 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
"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
git add scripts/main/main.gd
git commit -m "feat(nav): restore camera to saved room on game load"