extends RefCounted ## Headless tests for the door lock mechanic and flashlight perception bonus. const Rules := preload("res://src/core/rules.gd") const DoorState := preload("res://src/core/door_state.gd") const NpcProfile := preload("res://src/core/npc_profile.gd") const NPCController := preload("res://src/core/npc_controller.gd") func test_door_toggle_transitions() -> bool: var door := DoorState.new() var a := TestUtils.check_eq(door.locked, false, "starts unlocked") var b := TestUtils.check_eq(door.toggle(), true, "toggle locks") var c := TestUtils.check_eq(door.toggle(), false, "toggle unlocks") return a and b and c func test_locked_door_thwarts_force() -> bool: var door := DoorState.new() door.set_locked(true) var result := door.attempt_force() var a := TestUtils.check_eq(result["succeeded"], false, "locked door blocks forced entry") var b := TestUtils.check_eq(result["outcome"], "thwarted", "thwarted outcome") return a and b func test_unlocked_door_admits_force() -> bool: var door := DoorState.new() var result := door.attempt_force() var a := TestUtils.check_eq(result["succeeded"], true, "open door lets forced entry through") var b := TestUtils.check_eq(result["outcome"], "entered", "entered outcome") return a and b func test_flashlight_aims_a_tight_forward_spot() -> bool: ## The lamp sits on the guard's head and aims straight down the view axis with ## a narrow cone: a wide cone washed the whole entrance instead of lighting the ## door being checked (realism pass, 2026-11-01). var root := Node3D.new() var gm: Node = load("res://src/game/game_manager.gd").new() root.add_child(gm) var guard: Node3D = gm.call("_build_guard") var lamp: SpotLight3D = guard.get_node_or_null("Head/Flashlight") as SpotLight3D var a := TestUtils.check(lamp != null, "flashlight lives on the guard's head") var b := TestUtils.check(lamp != null and lamp.rotation_degrees.is_equal_approx(Vector3.ZERO), "flashlight aims straight ahead") var c := TestUtils.check(lamp != null and lamp.spot_angle <= 20.0, "flashlight cone is narrow enough to read as a torch") var d := TestUtils.check(lamp != null and not lamp.visible, "flashlight starts off") var e := TestUtils.check(lamp != null and is_zero_approx(lamp.light_volumetric_fog_energy), "headless/compatibility builds leave the flashlight non-volumetric") root.free() return a and b and c and d and e func test_flashlight_widens_perception() -> bool: ## Without the flashlight, an NPC at AWARE_RADIUS + 3 m is unaware. ## With the flashlight bonus (4 m), the same distance registers as aware. var p := NpcProfile.new() p.id = "nightwalker" p.npc_type = Rules.NpcType.TOURIST var dim := NPCController.new() dim.setup("d1", p, "wander", 1) var unlit: Dictionary = dim.perceive_guard(NPCController.AWARE_RADIUS + 3.0, 0.0) var a := TestUtils.check_eq(unlit["band"], "unaware", "unlit: too far to notice") var lit := NPCController.new() lit.setup("l1", p, "wander", 2) var lit_result: Dictionary = lit.perceive_guard(NPCController.AWARE_RADIUS + 3.0, 4.0) var b := TestUtils.check_eq(lit_result["band"], "aware", "flashlight bonus makes NPC aware") return a and b