neon-sprawl/client/test/npc_combat_hud_refresh_test.gd

161 lines
5.8 KiB
GDScript

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)")