extends GdUnitTestSuite ## NEO-97: combat-active gate + HUD label copy from snapshot fixtures. const NpcRuntimeClient := preload("res://scripts/npc_runtime_client.gd") const PrototypeTargetConstants := preload("res://scripts/prototype_target_constants.gd") class NpcCombatHudHarness: extends RefCounted var last_target_state: Dictionary = {} var npc_runtime_snapshot: Dictionary = {} var dev_player_id: String = "dev-local-1" func is_combat_active() -> bool: var lock_id := locked_target_id() if PrototypeTargetConstants.ANCHORS.has(lock_id): return true return not incoming_aggro_row().is_empty() func locked_target_id() -> String: var locked_variant: Variant = last_target_state.get("lockedTargetId", null) if locked_variant is String: return (locked_variant as String).strip_edges() return "" func incoming_aggro_row() -> Dictionary: if npc_runtime_snapshot.is_empty(): return {} return NpcRuntimeClient.row_for_aggro_holder(dev_player_id, npc_runtime_snapshot) func resolve_hud_npc_rows() -> Dictionary: var lock_id := locked_target_id() var locked_row: Dictionary = {} if PrototypeTargetConstants.ANCHORS.has(lock_id): locked_row = NpcRuntimeClient.npc_row(lock_id, npc_runtime_snapshot) var incoming_row: Dictionary = {} var aggro_row: Dictionary = incoming_aggro_row() if not aggro_row.is_empty(): var aggro_id: String = str(aggro_row.get("npcInstanceId", "")).strip_edges() if aggro_id != lock_id: incoming_row = aggro_row return {"locked": locked_row, "incoming": incoming_row} func build_npc_state_label() -> String: var rows: Dictionary = resolve_hud_npc_rows() var lines: PackedStringArray = PackedStringArray() var locked_row: Dictionary = rows.get("locked", {}) as Dictionary if not locked_row.is_empty(): var locked_id: String = str(locked_row.get("npcInstanceId", "")) lines.append( "NPC state: %s → %s" % [ PrototypeTargetConstants.display_short_name(locked_id), str(locked_row.get("state", "unknown")), ] ) var incoming_row: Dictionary = rows.get("incoming", {}) as Dictionary if not incoming_row.is_empty(): var incoming_id: String = str(incoming_row.get("npcInstanceId", "")) lines.append( "Incoming: %s → %s" % [ PrototypeTargetConstants.display_short_name(incoming_id), str(incoming_row.get("state", "unknown")), ] ) if lines.is_empty(): return "NPC state: —" return "\n".join(lines) 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}' + "]}" ) func test_npc_lock_makes_combat_active_without_aggro_snapshot() -> void: # Arrange var harness := NpcCombatHudHarness.new() harness.last_target_state = {"lockedTargetId": "prototype_npc_melee", "validity": "ok"} # Act var active: bool = harness.is_combat_active() # Assert assert_that(active).is_true() func test_aggro_holder_makes_combat_active_without_lock() -> void: # Arrange var harness := NpcCombatHudHarness.new() var parsed: Variant = NpcRuntimeClient.parse_runtime_json(_elite_aggro_snapshot_json()) harness.npc_runtime_snapshot = parsed as Dictionary # Act var active: bool = harness.is_combat_active() # Assert assert_that(active).is_true() func test_idle_without_lock_is_not_combat_active() -> void: # Arrange var harness := NpcCombatHudHarness.new() harness.npc_runtime_snapshot = {} # Act var active: bool = harness.is_combat_active() # Assert assert_that(active).is_false() func test_locked_plus_incoming_state_label() -> void: # Arrange var harness := NpcCombatHudHarness.new() harness.last_target_state = {"lockedTargetId": "prototype_npc_melee", "validity": "ok"} var parsed: Variant = NpcRuntimeClient.parse_runtime_json(_elite_aggro_snapshot_json()) harness.npc_runtime_snapshot = parsed as Dictionary # Act var label: String = harness.build_npc_state_label() # Assert assert_that(label).contains("NPC state: Melee → idle") assert_that(label).contains("Incoming: Elite → aggro")