extends GdUnitTestSuite ## NEO-97: combat-active gate + HUD label copy from snapshot fixtures. const NpcRuntimeClient := preload("res://scripts/npc_runtime_client.gd") const NpcRuntimeHudState := preload("res://scripts/npc_runtime_hud_state.gd") const NpcCombatHudHelpers := preload("res://scripts/npc_combat_hud_helpers.gd") static func _elite_aggro_snapshot_json() -> String: return ( '{"schemaVersion":1,"serverTimeUtc":"2026-05-29T12:00:00Z","npcInstances":[' + '{"npcInstanceId":"prototype_npc_elite","behaviorDefId":"prototype_elite_boss",' + '"state":"aggro","aggroHolderPlayerId":"dev-local-1","activeTelegraph":null},' + '{"npcInstanceId":"prototype_npc_melee","behaviorDefId":"prototype_melee_rush",' + '"state":"idle","aggroHolderPlayerId":null,"activeTelegraph":null}' + "]}" ) static func _elite_telegraph_snapshot_json() -> String: return ( '{"schemaVersion":1,"serverTimeUtc":"2026-05-29T12:00:00Z","npcInstances":[' + '{"npcInstanceId":"prototype_npc_elite","behaviorDefId":"prototype_elite_boss",' + '"state":"telegraph_windup","aggroHolderPlayerId":"dev-local-1",' + '"activeTelegraph":{"telegraphId":"prototype_elite_slam","npcInstanceId":' + '"prototype_npc_elite","windupRemainingSeconds":2.5,"archetypeKind":"elite"}},' + '{"npcInstanceId":"prototype_npc_melee","behaviorDefId":"prototype_melee_rush",' + '"state":"idle","aggroHolderPlayerId":null,"activeTelegraph":null}' + "]}" ) func test_npc_lock_makes_combat_active_without_aggro_snapshot() -> void: # Arrange var target_state := {"lockedTargetId": "prototype_npc_melee", "validity": "ok"} # Act var active: bool = NpcCombatHudHelpers.is_combat_active(target_state, {}, "dev-local-1") # Assert assert_that(active).is_true() func test_aggro_holder_makes_combat_active_without_lock() -> void: # Arrange var parsed: Variant = NpcRuntimeClient.parse_runtime_json(_elite_aggro_snapshot_json()) # Act var active: bool = NpcCombatHudHelpers.is_combat_active({}, parsed as Dictionary, "dev-local-1") # Assert assert_that(active).is_true() func test_idle_without_lock_is_not_combat_active() -> void: # Arrange # Act var active: bool = NpcCombatHudHelpers.is_combat_active({}, {}, "dev-local-1") # Assert assert_that(active).is_false() func test_npc_state_label_resets_when_lock_cleared_without_aggro() -> void: # Arrange var cleared_target := {"lockedTargetId": null, "validity": "none"} var rows: Dictionary = NpcCombatHudHelpers.resolve_hud_npc_rows( cleared_target, {}, "dev-local-1" ) # Act var label: String = NpcCombatHudHelpers.build_npc_state_label(rows) # Assert assert_that(label).is_equal("NPC state: —") func test_combat_poll_gate_stops_when_idle() -> void: # Arrange var timer := Timer.new() timer.one_shot = false add_child(timer) timer.start() # Act var should_poll: bool = NpcCombatHudHelpers.is_combat_active({}, {}, "dev-local-1") if not should_poll: timer.stop() # Assert assert_that(should_poll).is_false() assert_that(timer.is_stopped()).is_true() func test_combat_poll_gate_starts_when_lock_held() -> void: # Arrange var timer := Timer.new() timer.one_shot = false add_child(timer) var target_state := {"lockedTargetId": "prototype_npc_elite", "validity": "ok"} # Act var should_poll: bool = NpcCombatHudHelpers.is_combat_active(target_state, {}, "dev-local-1") if should_poll and timer.is_stopped(): timer.start() # Assert assert_that(should_poll).is_true() assert_that(timer.is_stopped()).is_false() func test_locked_plus_incoming_state_label() -> void: # Arrange var target_state := {"lockedTargetId": "prototype_npc_melee", "validity": "ok"} var parsed: Variant = NpcRuntimeClient.parse_runtime_json(_elite_aggro_snapshot_json()) var rows: Dictionary = NpcCombatHudHelpers.resolve_hud_npc_rows( target_state, parsed as Dictionary, "dev-local-1" ) # Act var label: String = NpcCombatHudHelpers.build_npc_state_label(rows) # Assert assert_that(label).contains("NPC state: Melee → idle") assert_that(label).contains("Incoming: Elite → aggro") func test_telegraph_label_falls_back_to_incoming_row() -> void: # Arrange var target_state := {"lockedTargetId": "prototype_npc_melee", "validity": "ok"} var parsed: Variant = NpcRuntimeClient.parse_runtime_json(_elite_telegraph_snapshot_json()) var rows: Dictionary = NpcCombatHudHelpers.resolve_hud_npc_rows( target_state, parsed as Dictionary, "dev-local-1" ) # Act var label: String = NpcCombatHudHelpers.build_telegraph_label(rows) # Assert assert_that(label).contains("Telegraph: Elite prototype_elite_slam") assert_that(label).contains("(elite)") func test_telegraph_label_empty_when_local_windup_expired() -> void: # Arrange var short_windup_json := ( '{"schemaVersion":1,"serverTimeUtc":"2026-05-29T12:00:00Z","npcInstances":[' + '{"npcInstanceId":"prototype_npc_elite","behaviorDefId":"prototype_elite_boss",' + '"state":"telegraph_windup","aggroHolderPlayerId":"dev-local-1",' + '"activeTelegraph":{"telegraphId":"prototype_elite_slam","npcInstanceId":' + '"prototype_npc_elite","windupRemainingSeconds":0.1,"archetypeKind":"elite"}}' + "]}" ) var parsed: Variant = NpcRuntimeClient.parse_runtime_json(short_windup_json) var rows: Dictionary = NpcCombatHudHelpers.resolve_hud_npc_rows( {"lockedTargetId": "prototype_npc_elite", "validity": "ok"}, parsed as Dictionary, "dev-local-1" ) var hud_state: RefCounted = NpcRuntimeHudState.new() hud_state.call("apply_snapshot", parsed as Dictionary) await (Engine.get_main_loop() as SceneTree).create_timer(0.15).timeout # Act var label: String = NpcCombatHudHelpers.build_telegraph_label(rows, hud_state) # Assert assert_that(label).is_equal("Telegraph: —") func test_runtime_sync_error_surfaces_on_empty_npc_state_label() -> void: # Arrange # Act var label: String = NpcCombatHudHelpers.build_npc_state_label({}, "HTTP 500") # Assert assert_that(label).is_equal("NPC state: — (HTTP 500)")