extends RefCounted ## Focus cone math and radial menu wrap/cancel (ADR-0008, ADR-0009). const NpcFocus := preload("res://src/game/npc_focus.gd") const ActionRadial := preload("res://src/game/ui/action_radial.gd") func test_cone_picks_centered_head() -> bool: var origin := Vector3(0, 1.6, 0) var forward := Vector3(0, 0, -1) var heads: Array = [ Vector3(3, 1.6, -4), Vector3(0.05, 1.6, -4), Vector3(-2, 1.6, -4), ] var idx := NpcFocus.pick_index(origin, forward, heads) return TestUtils.check_eq(idx, 1, "head nearest the look axis wins") func test_cone_rejects_behind_and_far() -> bool: var origin := Vector3(0, 1.6, 0) var forward := Vector3(0, 0, -1) var behind := NpcFocus.score_head(origin, forward, Vector3(0, 1.6, 4)) var far := NpcFocus.score_head(origin, forward, Vector3(0, 1.6, -40)) var a := TestUtils.check_eq(behind, -1.0, "behind the camera is ignored") var b := TestUtils.check_eq(far, -1.0, "out of range is ignored") return a and b func test_radial_wrap_and_skip_wand() -> bool: var menu: Node = ActionRadial.new() menu.open_menu("Guest", false) var start: int = menu.selected menu.step(-1) var a := TestUtils.check(menu.selected != start, "step wrap moves selection") var b := TestUtils.check(menu.is_slice_enabled(menu.selected), "landed on an enabled slice") # Walk forward until we would hit wand; it must skip. var wand_index := _slice_index("wand") var warning_index := _slice_index("warning") menu.selected = wand_index - 1 menu.step(1) var c := TestUtils.check_eq(menu.slice_id(menu.selected), "warning", "wand skipped when venue has none") menu.free() return a and b and c func test_radial_cancel_does_not_confirm() -> bool: var menu: Node = ActionRadial.new() menu.open_menu("Guest", true) var got := {"id": ""} menu.confirmed.connect(func(id): got["id"] = id) menu.cancelled.connect(func(): got["id"] = "cancelled") menu.close_menu() var a := TestUtils.check_eq(got["id"], "", "closing without confirm emits nothing") var slice: String = menu.confirm_selected() var b := TestUtils.check_eq(slice, "", "confirm while closed is empty") menu.free() return a and b func test_radial_confirm_returns_id() -> bool: var menu: Node = ActionRadial.new() menu.open_menu("Guest", true) menu.selected = 0 var slice: String = menu.confirm_selected() menu.free() return TestUtils.check_eq(slice, "raise_hand", "space-confirm uses highlighted slice") func test_angle_top_is_first_slice() -> bool: var menu: Node = ActionRadial.new() var idx: int = menu.angle_to_index(-PI / 2.0) menu.free() return TestUtils.check_eq(idx, 0, "top of the doughnut is slice 0") func test_wand_not_on_w() -> bool: var src := FileAccess.get_file_as_string("res://src/game/game_manager.gd") var a := TestUtils.check(not src.contains("KEY_W:"), "W is not a game-manager action key") var b := TestUtils.check(src.contains("is_action_pressed(\"wand\")"), "wand goes through the named action") var c := TestUtils.check(not _action_has_key("wand", KEY_W), "wand action is not bound to W") var d := TestUtils.check(_action_has_key("wand", KEY_C), "wand action is bound to C") return a and b and c and d func test_named_input_actions_exist() -> bool: var menu_ok := _action_has_key("action_menu", KEY_SPACE) var confirm_ok := _action_has_key("confirm", KEY_SPACE) and _action_has_key("confirm", KEY_ENTER) var cancel_ok := _action_has_key("cancel", KEY_ESCAPE) var inspect_ok := _action_has_key("inspect", KEY_E) return TestUtils.check(menu_ok, "action_menu has Space") \ and TestUtils.check(confirm_ok, "confirm has Space + Enter") \ and TestUtils.check(cancel_ok, "cancel has Esc") \ and TestUtils.check(inspect_ok, "inspect has E") func test_cancel_has_right_mouse() -> bool: var events: Array = InputMap.action_get_events("cancel") var found := false for event in events: if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT: found = true return TestUtils.check(found, "cancel action includes right mouse button") func _slice_index(id: String) -> int: for i in ActionRadial.SLICE_DEFS.size(): if str(ActionRadial.SLICE_DEFS[i]["id"]) == id: return i return -1 func _action_has_key(action: String, keycode: Key) -> bool: if not InputMap.has_action(action): return false for event in InputMap.action_get_events(action): if event is InputEventKey and event.physical_keycode == keycode: return true return false