NEO-99: update GdUnit tests for district visuals and prototype_npc_* targeting.

Add visual catalog transform tests and refresh target selection, marker,
and combat client suites for PrototypeNpcRegistry ids from NEO-91.
pull/134/head
VinPropane 2026-05-29 20:03:47 -04:00
parent fa3d48a6aa
commit ab7033e85a
12 changed files with 164 additions and 107 deletions

View File

@ -104,12 +104,12 @@ func test_request_cast_posts_target_id_string_when_set() -> void:
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"schemaVersion":1,"accepted":true}')
var c := _make_client(transport)
# Act
var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", "prototype_target_alpha"))
var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", "prototype_npc_melee"))
# Assert
assert_that(started).is_true()
var parsed: Variant = JSON.parse_string(transport.last_body)
var body: Dictionary = parsed
assert_that(body.get("targetId")).is_equal("prototype_target_alpha")
assert_that(body.get("targetId")).is_equal("prototype_npc_melee")
func test_request_cast_returns_false_when_http_request_fails_to_start() -> void:
@ -157,7 +157,7 @@ func test_cast_result_received_emits_false_with_reason_on_denied() -> void:
got.append([accepted, reason, resolution])
)
# Act
var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", "prototype_target_alpha"))
var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", "prototype_npc_melee"))
# Assert
assert_that(started).is_true()
assert_that(got.size()).is_equal(1)
@ -195,7 +195,7 @@ func test_cast_result_received_emits_combat_resolution_on_accept() -> void:
200,
(
'{"schemaVersion":1,"accepted":true,"combatResolution":'
+ '{"abilityId":"prototype_pulse","targetId":"prototype_target_alpha",'
+ '{"abilityId":"prototype_pulse","targetId":"prototype_npc_melee",'
+ '"damageDealt":25,"targetRemainingHp":75,"targetDefeated":false}}'
)
)
@ -207,7 +207,7 @@ func test_cast_result_received_emits_combat_resolution_on_accept() -> void:
got.append([accepted, reason, resolution])
)
# Act
var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", "prototype_target_alpha"))
var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", "prototype_npc_melee"))
# Assert
assert_that(started).is_true()
assert_that(got.size()).is_equal(1)
@ -222,7 +222,7 @@ func test_parse_cast_response_json_maps_combat_resolution() -> void:
# Arrange
var body := (
'{"schemaVersion":1,"accepted":true,"combatResolution":'
+ '{"abilityId":"prototype_pulse","targetId":"prototype_target_alpha",'
+ '{"abilityId":"prototype_pulse","targetId":"prototype_npc_melee",'
+ '"damageDealt":25,"targetRemainingHp":75,"targetDefeated":false}}'
)
# Act

View File

@ -13,7 +13,7 @@ class MockCastTransport:
)
var body_json: String = (
'{"schemaVersion":1,"accepted":true,"combatResolution":'
+ '{"abilityId":"prototype_pulse","targetId":"prototype_target_alpha",'
+ '{"abilityId":"prototype_pulse","targetId":"prototype_npc_melee",'
+ '"damageDealt":25,"targetRemainingHp":75,"targetDefeated":false}}'
)
@ -76,7 +76,7 @@ class CombatRefreshHarness:
return ""
func request_cast() -> void:
_cast.call("request_cast", 0, "prototype_pulse", "prototype_target_alpha")
_cast.call("request_cast", 0, "prototype_pulse", "prototype_npc_melee")
func test_successful_cast_triggers_combat_target_sync() -> void:
@ -106,7 +106,7 @@ func test_target_lock_change_triggers_combat_target_sync() -> void:
harness.setup(self, transport)
# Act
harness.on_target_state_changed(
{"lockedTargetId": "prototype_target_alpha", "validity": "ok", "sequence": 1}
{"lockedTargetId": "prototype_npc_melee", "validity": "ok", "sequence": 1}
)
# Assert
assert_that(harness.combat_sync_calls).is_equal(1)
@ -122,11 +122,11 @@ func test_target_validity_only_update_skips_combat_target_sync() -> void:
add_child(harness)
harness.setup(self, transport)
harness.on_target_state_changed(
{"lockedTargetId": "prototype_target_alpha", "validity": "ok", "sequence": 1}
{"lockedTargetId": "prototype_npc_melee", "validity": "ok", "sequence": 1}
)
# Act
harness.on_target_state_changed(
{"lockedTargetId": "prototype_target_alpha", "validity": "out_of_range", "sequence": 2}
{"lockedTargetId": "prototype_npc_melee", "validity": "out_of_range", "sequence": 2}
)
# Assert
assert_that(harness.combat_sync_calls).is_equal(1)

View File

@ -33,8 +33,9 @@ class MockHttpTransport:
static func _two_target_snapshot_json() -> String:
return (
'{"schemaVersion":1,"targets":['
+ '{"targetId":"prototype_target_alpha","maxHp":100,"currentHp":75,"defeated":false},'
+ '{"targetId":"prototype_target_beta","maxHp":100,"currentHp":100,"defeated":false}'
+ '{"targetId":"prototype_npc_melee","maxHp":100,"currentHp":75,"defeated":false},'
+ '{"targetId":"prototype_npc_ranged","maxHp":80,"currentHp":80,"defeated":false},'
+ '{"targetId":"prototype_npc_elite","maxHp":200,"currentHp":200,"defeated":false}'
+ "]}"
)
@ -86,7 +87,7 @@ func test_target_row_returns_alpha_hp_fields() -> void:
c.call("request_sync_from_server")
var snapshot: Dictionary = got[0]
# Act
var row: Dictionary = c.call("target_row", "prototype_target_alpha", snapshot) as Dictionary
var row: Dictionary = c.call("target_row", "prototype_npc_melee", snapshot) as Dictionary
# Assert
assert_that(int(row.get("currentHp", 0))).is_equal(75)
assert_that(int(row.get("maxHp", 0))).is_equal(100)

View File

@ -57,7 +57,7 @@ class GigRefreshHarness:
_gig.request_sync_from_server()
func request_cast() -> void:
_cast.call("request_cast", 0, "prototype_pulse", "prototype_target_alpha")
_cast.call("request_cast", 0, "prototype_pulse", "prototype_npc_melee")
func test_defeat_cast_triggers_gig_progression_sync() -> void:
@ -65,7 +65,7 @@ func test_defeat_cast_triggers_gig_progression_sync() -> void:
var transport := MockCastTransport.new()
transport.body_json = (
'{"schemaVersion":1,"accepted":true,"combatResolution":'
+ '{"abilityId":"prototype_pulse","targetId":"prototype_target_alpha",'
+ '{"abilityId":"prototype_pulse","targetId":"prototype_npc_melee",'
+ '"damageDealt":25,"targetRemainingHp":0,"targetDefeated":true}}'
)
var harness := GigRefreshHarness.new()
@ -87,7 +87,7 @@ func test_non_defeat_cast_skips_gig_progression_sync() -> void:
var transport := MockCastTransport.new()
transport.body_json = (
'{"schemaVersion":1,"accepted":true,"combatResolution":'
+ '{"abilityId":"prototype_pulse","targetId":"prototype_target_alpha",'
+ '{"abilityId":"prototype_pulse","targetId":"prototype_npc_melee",'
+ '"damageDealt":25,"targetRemainingHp":75,"targetDefeated":false}}'
)
var harness := GigRefreshHarness.new()

View File

@ -15,7 +15,7 @@ func test_resolve_returns_ok_with_target_from_locked_target_id() -> void:
# Arrange
var slots := _eight_empty_slots()
slots[0] = "prototype_pulse"
var target_state := {"lockedTargetId": "prototype_target_alpha", "sequence": 3}
var target_state := {"lockedTargetId": "prototype_npc_melee", "sequence": 3}
# Act
var outcome: Dictionary = Resolver.resolve(0, slots, target_state)
@ -24,7 +24,7 @@ func test_resolve_returns_ok_with_target_from_locked_target_id() -> void:
assert_that(bool(outcome.get(Resolver.KEY_OK, false))).is_true()
assert_that(int(outcome.get(Resolver.KEY_SLOT_INDEX, -1))).is_equal(0)
assert_that(outcome.get(Resolver.KEY_ABILITY_ID)).is_equal("prototype_pulse")
assert_that(outcome.get(Resolver.KEY_TARGET_ID)).is_equal("prototype_target_alpha")
assert_that(outcome.get(Resolver.KEY_TARGET_ID)).is_equal("prototype_npc_melee")
func test_resolve_returns_null_target_when_no_lock() -> void:
@ -59,7 +59,7 @@ func test_resolve_denies_empty_slot_without_ability() -> void:
# Arrange
var slots := _eight_empty_slots()
slots[3] = null
var target_state := {"lockedTargetId": "prototype_target_beta"}
var target_state := {"lockedTargetId": "prototype_npc_ranged"}
# Act
var outcome: Dictionary = Resolver.resolve(3, slots, target_state)

View File

@ -0,0 +1,25 @@
extends GdUnitTestSuite
const Catalog := preload("res://scripts/prototype_district_visual_catalog.gd")
func test_instantiate_prop_clears_baked_child_transform() -> void:
# Arrange
var packed: PackedScene = Catalog.INTERACTABLE_PROPS["prototype_resource_node_alpha"]
# Act
var visual: Node3D = Catalog.instantiate_prop(packed, 0.5)
# Assert
var mesh: Node3D = visual.get_node("IX_SalvageAlpha_Mesh") as Node3D
assert_that(mesh.position).is_equal(Vector3.ZERO)
func test_normalize_prop_visual_grounds_mesh_feet_at_floor() -> void:
# Arrange
var packed: PackedScene = Catalog.INTERACTABLE_PROPS["prototype_resource_node_alpha"]
var visual: Node3D = packed.instantiate() as Node3D
var mesh: MeshInstance3D = visual.get_node("IX_SalvageAlpha_Mesh") as MeshInstance3D
var min_y: float = mesh.get_aabb().position.y
# Act
Catalog.normalize_prop_visual(visual, 0.5)
# Assert
assert_float(visual.position.y).is_equal_approx(-0.5 - min_y, 0.0001)

View File

@ -0,0 +1 @@
uid://c5m8p2n7k4w9x

View File

@ -1,18 +1,20 @@
extends GdUnitTestSuite
## NEO-85: prototype target display helpers for combat dummy markers and HUD copy.
## NEO-85 / NEO-91: prototype NPC display helpers for combat markers and HUD copy.
const PrototypeTargetConstants := preload("res://scripts/prototype_target_constants.gd")
func test_display_short_name_maps_alpha_and_beta() -> void:
func test_display_short_name_maps_npc_archetypes() -> void:
# Arrange
# Act
var alpha: String = PrototypeTargetConstants.display_short_name("prototype_target_alpha")
var beta: String = PrototypeTargetConstants.display_short_name("prototype_target_beta")
var elite: String = PrototypeTargetConstants.display_short_name("prototype_npc_elite")
var melee: String = PrototypeTargetConstants.display_short_name("prototype_npc_melee")
var ranged: String = PrototypeTargetConstants.display_short_name("prototype_npc_ranged")
# Assert
assert_that(alpha).is_equal("Dummy α")
assert_that(beta).is_equal("Dummy β")
assert_that(elite).is_equal("Elite")
assert_that(melee).is_equal("Melee")
assert_that(ranged).is_equal("Ranged")
func test_display_short_name_falls_back_to_id() -> void:
@ -21,3 +23,13 @@ func test_display_short_name_falls_back_to_id() -> void:
var unknown: String = PrototypeTargetConstants.display_short_name("unknown_target")
# Assert
assert_that(unknown).is_equal("unknown_target")
func test_ordered_ids_matches_server_tab_cycle() -> void:
# Arrange
# Act
var ids: Array[String] = PrototypeTargetConstants.ordered_ids()
# Assert
assert_that(ids).is_equal(
["prototype_npc_elite", "prototype_npc_melee", "prototype_npc_ranged"]
)

View File

@ -4,20 +4,32 @@ extends GdUnitTestSuite
const Markers := preload("res://scripts/prototype_target_markers.gd")
const ALPHA_ID := "prototype_target_alpha"
const MELEE_ID := "prototype_npc_melee"
func test_set_locked_target_brightens_body_albedo_without_emission() -> void:
func test_set_locked_target_brightens_ring_without_emission() -> void:
# Arrange
var markers: Node3D = Markers.new()
auto_free(markers)
add_child(markers)
await get_tree().process_frame
var body: MeshInstance3D = markers.get_node("Marker_%s/DummyBody" % ALPHA_ID) as MeshInstance3D
var body_mat: StandardMaterial3D = body.material_override as StandardMaterial3D
var idle_color: Color = body_mat.albedo_color
var ring: MeshInstance3D = markers.get_node("Marker_%s/RadiusRing" % MELEE_ID) as MeshInstance3D
var ring_mat: StandardMaterial3D = ring.material_override as StandardMaterial3D
var idle_color: Color = ring_mat.albedo_color
# Act
markers.set_locked_target(ALPHA_ID)
markers.set_locked_target(MELEE_ID)
# Assert
assert_that(body_mat.albedo_color).is_not_equal(idle_color)
assert_that(body_mat.emission_enabled).is_false()
assert_that(ring_mat.albedo_color).is_not_equal(idle_color)
assert_that(ring_mat.emission_enabled).is_false()
func test_spawn_marker_uses_imported_prop_visual() -> void:
# Arrange
var markers: Node3D = Markers.new()
auto_free(markers)
add_child(markers)
await get_tree().process_frame
# Act
var visual: Node = markers.get_node("Marker_%s/DummyBody/PropVisual" % MELEE_ID)
# Assert
assert_that(visual).is_not_null()

View File

@ -4,8 +4,9 @@ extends GdUnitTestSuite
const TargetSelectionTestDouble := preload("res://test/target_selection_test_double.gd")
const ALPHA_ID := "prototype_target_alpha"
const BETA_ID := "prototype_target_beta"
const ELITE_ID := "prototype_npc_elite"
const MELEE_ID := "prototype_npc_melee"
const RANGED_ID := "prototype_npc_ranged"
class MockHttpTransport:
@ -74,7 +75,7 @@ func test_tab_lock_change_emits_tab() -> void:
# Arrange
var transport := MockHttpTransport.new()
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, MELEE_ID, "ok", 1)
)
var c := _make_client(transport)
var events: Array = []
@ -86,7 +87,7 @@ func test_tab_lock_change_emits_tab() -> void:
var last: Dictionary = events[0] as Dictionary
assert_that(last.get("cause", "")).is_equal("tab")
assert_that(last.get("previous")).is_null()
assert_that(last.get("next")).is_equal(ALPHA_ID)
assert_that(last.get("next")).is_equal(MELEE_ID)
assert_that(int(last.get("sequence", -1))).is_equal(1)
@ -94,7 +95,7 @@ func test_clear_lock_change_emits_clear() -> void:
# Arrange
var transport := MockHttpTransport.new()
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, MELEE_ID, "ok", 1)
)
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, null, "none", 2))
var c := _make_client(transport)
@ -107,7 +108,7 @@ func test_clear_lock_change_emits_clear() -> void:
assert_that(events.size()).is_equal(2)
assert_that(int((events[0] as Dictionary).get("sequence", -1))).is_equal(1)
assert_that((events[1] as Dictionary).get("cause", "")).is_equal("clear")
assert_that((events[1] as Dictionary).get("previous")).is_equal(ALPHA_ID)
assert_that((events[1] as Dictionary).get("previous")).is_equal(MELEE_ID)
assert_that((events[1] as Dictionary).get("next")).is_null()
assert_that(int((events[1] as Dictionary).get("sequence", -1))).is_equal(2)
@ -116,9 +117,9 @@ func test_get_lock_id_change_emits_server_correction() -> void:
# Arrange
var transport := MockHttpTransport.new()
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, MELEE_ID, "ok", 1)
)
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, _target_state_json(BETA_ID, "ok", 2))
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, _target_state_json(RANGED_ID, "ok", 2))
var c := _make_client(transport)
var events: Array = []
c.selection_event.connect(func(ev: Dictionary) -> void: events.append(ev.duplicate()))
@ -129,8 +130,8 @@ func test_get_lock_id_change_emits_server_correction() -> void:
assert_that(events.size()).is_equal(2)
var last: Dictionary = events[1] as Dictionary
assert_that(last.get("cause", "")).is_equal("server_correction")
assert_that(last.get("previous")).is_equal(ALPHA_ID)
assert_that(last.get("next")).is_equal(BETA_ID)
assert_that(last.get("previous")).is_equal(MELEE_ID)
assert_that(last.get("next")).is_equal(RANGED_ID)
assert_that(int(last.get("sequence", -1))).is_equal(2)
@ -138,10 +139,10 @@ func test_get_validity_only_change_emits_nothing() -> void:
# Arrange
var transport := MockHttpTransport.new()
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, MELEE_ID, "ok", 1)
)
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _target_state_json(ALPHA_ID, "out_of_range", 1)
HTTPRequest.RESULT_SUCCESS, 200, _target_state_json(MELEE_ID, "out_of_range", 1)
)
var c := _make_client(transport)
var events: Array = []
@ -167,6 +168,6 @@ func test_denial_without_lock_id_change_emits_nothing() -> void:
var events: Array = []
c.selection_event.connect(func(ev: Dictionary) -> void: events.append(ev.duplicate()))
# Act
c.request_select_target_id(ALPHA_ID)
c.request_select_target_id(MELEE_ID)
# Assert
assert_that(events.size()).is_equal(0)

View File

@ -13,7 +13,7 @@ extends GdUnitTestSuite
const PositionAuthorityTestDouble := preload("res://test/position_authority_test_double.gd")
const TargetSelectionTestDouble := preload("res://test/target_selection_test_double.gd")
const ALPHA_ID := "prototype_target_alpha"
const ALPHA_ID := "prototype_npc_melee"
# Mirrors the transports from the per-client suites: a queue-backed `Node` with a

View File

@ -6,8 +6,9 @@ extends GdUnitTestSuite
const TargetSelectionTestDouble := preload("res://test/target_selection_test_double.gd")
const ALPHA_ID := "prototype_target_alpha"
const BETA_ID := "prototype_target_beta"
const ELITE_ID := "prototype_npc_elite"
const MELEE_ID := "prototype_npc_melee"
const RANGED_ID := "prototype_npc_ranged"
# In-process transport: not an HTTPRequest subclass (Godot 4 will not call script `request()`
@ -101,7 +102,7 @@ func test_tab_from_no_lock_selects_first_id() -> void:
# Arrange
var transport := MockHttpTransport.new()
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ELITE_ID, "ok", 1)
)
var c := _make_client(transport)
monitor_signals(c)
@ -110,49 +111,53 @@ func test_tab_from_no_lock_selects_first_id() -> void:
# Assert
assert_that(transport.last_url).contains("/target/select")
assert_that(transport.last_method).is_equal(HTTPClient.METHOD_POST)
assert_that(transport.last_body).contains('"targetId":"%s"' % ALPHA_ID)
assert_that(transport.last_body).contains('"targetId":"%s"' % ELITE_ID)
await assert_signal(c).is_emitted("target_state_changed", any())
assert_that(c.cached_state().get("lockedTargetId")).is_equal(ALPHA_ID)
assert_that(c.cached_state().get("lockedTargetId")).is_equal(ELITE_ID)
func test_tab_from_alpha_requests_beta() -> void:
func test_tab_from_elite_requests_melee() -> void:
# Arrange
var transport := MockHttpTransport.new()
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ELITE_ID, "ok", 1)
)
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, BETA_ID, "ok", 2)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, MELEE_ID, "ok", 2)
)
var c := _make_client(transport)
# Act
c.request_tab_next()
c.request_tab_next()
# Assert
assert_that(transport.last_body).contains('"targetId":"%s"' % BETA_ID)
assert_that(c.cached_state().get("lockedTargetId")).is_equal(BETA_ID)
assert_that(transport.last_body).contains('"targetId":"%s"' % MELEE_ID)
assert_that(c.cached_state().get("lockedTargetId")).is_equal(MELEE_ID)
func test_tab_wraps_from_beta_to_alpha() -> void:
func test_tab_wraps_from_ranged_back_to_elite() -> void:
# Arrange
var transport := MockHttpTransport.new()
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ELITE_ID, "ok", 1)
)
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, BETA_ID, "ok", 2)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, MELEE_ID, "ok", 2)
)
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 3)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, RANGED_ID, "ok", 3)
)
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ELITE_ID, "ok", 4)
)
var c := _make_client(transport)
# Act
c.request_tab_next()
c.request_tab_next()
c.request_tab_next()
c.request_tab_next()
# Assert
assert_that(transport.last_body).contains('"targetId":"%s"' % ALPHA_ID)
assert_that(c.cached_state().get("lockedTargetId")).is_equal(ALPHA_ID)
assert_that(transport.last_body).contains('"targetId":"%s"' % ELITE_ID)
assert_that(c.cached_state().get("lockedTargetId")).is_equal(ELITE_ID)
func test_denial_reflects_authoritative_target_state() -> void:
@ -166,7 +171,7 @@ func test_denial_reflects_authoritative_target_state() -> void:
)
var c := _make_client(transport)
# Act
c.request_select_target_id(ALPHA_ID)
c.request_select_target_id(MELEE_ID)
# Assert
var state: Dictionary = c.cached_state()
assert_that(bool(state.get("selectionApplied", true))).is_false()
@ -192,21 +197,21 @@ func test_authoritative_ack_while_locked_fires_refresh_get() -> void:
var transport := MockHttpTransport.new()
# Step 1: lock alpha via POST.
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, MELEE_ID, "ok", 1)
)
# Step 2: movement-triggered GET shows out_of_range (soft lock).
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _target_state_json(ALPHA_ID, "out_of_range", 1)
HTTPRequest.RESULT_SUCCESS, 200, _target_state_json(MELEE_ID, "out_of_range", 1)
)
var c := _make_client(transport)
# Act
c.request_select_target_id(ALPHA_ID)
c.request_select_target_id(MELEE_ID)
var count_before_ack: int = transport.request_count
c.on_authoritative_ack(Vector3.ZERO)
# Assert
assert_that(transport.request_count).is_equal(count_before_ack + 1)
assert_that(c.cached_state().get("validity")).is_equal("out_of_range")
assert_that(c.cached_state().get("lockedTargetId")).is_equal(ALPHA_ID)
assert_that(c.cached_state().get("lockedTargetId")).is_equal(MELEE_ID)
func test_authoritative_ack_without_lock_fires_nothing() -> void:
@ -223,14 +228,14 @@ func test_two_acks_within_cooldown_collapse_to_one_get() -> void:
# Arrange
var transport := MockHttpTransport.new()
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, MELEE_ID, "ok", 1)
)
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _target_state_json(ALPHA_ID, "out_of_range", 1)
HTTPRequest.RESULT_SUCCESS, 200, _target_state_json(MELEE_ID, "out_of_range", 1)
)
var c := _make_client(transport)
# Act
c.request_select_target_id(ALPHA_ID)
c.request_select_target_id(MELEE_ID)
var count_before: int = transport.request_count
c.on_authoritative_ack(Vector3.ZERO)
c.on_authoritative_ack(Vector3.ZERO)
@ -262,7 +267,7 @@ func test_select_post_kicks_freshness_stream_before_posting() -> void:
# Arrange
var transport := MockHttpTransport.new()
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, MELEE_ID, "ok", 1)
)
var c := _make_client(transport)
var authority := FreshnessAuthorityStub.new()
@ -271,7 +276,7 @@ func test_select_post_kicks_freshness_stream_before_posting() -> void:
var player := _make_player(Vector3(1.5, 0.5, -2.25))
c.set_freshness_kick(authority, player)
# Act
c.request_select_target_id(ALPHA_ID)
c.request_select_target_id(MELEE_ID)
# Assert
# Exactly one freshness submit, matching the pre-POST capsule position.
assert_that(authority.submit_count).is_equal(1)
@ -287,7 +292,7 @@ func test_select_post_includes_position_hint_when_player_wired() -> void:
# `positionHint` so the server's range check can bypass any stale stored snap.
var transport := MockHttpTransport.new()
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, BETA_ID, "ok", 1)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, RANGED_ID, "ok", 1)
)
var c := _make_client(transport)
var authority := FreshnessAuthorityStub.new()
@ -298,12 +303,12 @@ func test_select_post_includes_position_hint_when_player_wired() -> void:
var player := _make_player(Vector3(3.0, 0.5, 5.0))
c.set_freshness_kick(authority, player)
# Act
c.request_select_target_id(BETA_ID)
c.request_select_target_id(RANGED_ID)
# Assert
var parsed: Variant = JSON.parse_string(transport.last_body)
assert_that(parsed is Dictionary).is_true()
var body: Dictionary = parsed
assert_that(body.get("targetId")).is_equal(BETA_ID)
assert_that(body.get("targetId")).is_equal(RANGED_ID)
assert_that(body.has("positionHint")).is_true()
var hint: Dictionary = body["positionHint"]
assert_float(hint.get("x")).is_equal_approx(3.0, 0.0001)
@ -317,11 +322,11 @@ func test_select_post_without_freshness_wiring_is_a_noop() -> void:
# still POST normally, *without* a `positionHint` (hint is opt-in per wiring).
var transport := MockHttpTransport.new()
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, MELEE_ID, "ok", 1)
)
var c := _make_client(transport)
# Act
c.request_select_target_id(ALPHA_ID)
c.request_select_target_id(MELEE_ID)
# Assert
assert_that(transport.last_url).contains("/target/select")
# No `positionHint` key in the body when no player is wired — re-parse so the assertion
@ -329,29 +334,29 @@ func test_select_post_without_freshness_wiring_is_a_noop() -> void:
var parsed: Variant = JSON.parse_string(transport.last_body)
assert_that(parsed is Dictionary).is_true()
assert_that((parsed as Dictionary).has("positionHint")).is_false()
assert_that(c.cached_state().get("lockedTargetId")).is_equal(ALPHA_ID)
assert_that(c.cached_state().get("lockedTargetId")).is_equal(MELEE_ID)
func test_tab_from_no_lock_skips_out_of_range_and_picks_beta() -> void:
func test_tab_from_no_lock_skips_out_of_range_and_picks_ranged() -> void:
# Arrange
# Player stands next to beta (well outside alpha's ring). Without the range-aware
# tab cycle the server denies with `out_of_range` on alpha — see NEO-24 follow-up #3.
# Player stands next to ranged only (elite/melee rings are out of reach). Without the
# range-aware tab cycle the server denies on the first id in cycle order.
var transport := MockHttpTransport.new()
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, BETA_ID, "ok", 1)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, RANGED_ID, "ok", 1)
)
var c := _make_client(transport)
var authority := FreshnessAuthorityStub.new()
auto_free(authority)
add_child(authority)
# Near beta anchor (3, 3), ~1.4 m horizontal — inside 6 m. Far from alpha at (-3, -3).
var player := _make_player(Vector3(4.0, 0.5, 4.0))
# Near ranged anchor (3, 3), inside 6 m; elite at origin and melee at (-3, -3) are out.
var player := _make_player(Vector3(7.0, 0.5, 7.0))
c.set_freshness_kick(authority, player)
# Act
c.request_tab_next()
# Assert
assert_that(transport.last_body).contains('"targetId":"%s"' % BETA_ID)
assert_that(c.cached_state().get("lockedTargetId")).is_equal(BETA_ID)
assert_that(transport.last_body).contains('"targetId":"%s"' % RANGED_ID)
assert_that(c.cached_state().get("lockedTargetId")).is_equal(RANGED_ID)
func test_tab_from_no_lock_with_nothing_in_range_falls_back_to_first_id() -> void:
@ -372,21 +377,21 @@ func test_tab_from_no_lock_with_nothing_in_range_falls_back_to_first_id() -> voi
# Act
c.request_tab_next()
# Assert
assert_that(transport.last_body).contains('"targetId":"%s"' % ALPHA_ID)
assert_that(transport.last_body).contains('"targetId":"%s"' % ELITE_ID)
assert_that(c.cached_state().get("reasonCode", "")).is_equal("out_of_range")
func test_tab_from_locked_alpha_at_origin_swaps_to_beta_when_both_in_range() -> void:
func test_tab_from_locked_elite_at_origin_swaps_to_melee_when_both_in_range() -> void:
# Arrange
# With alpha (-3,-3) and beta (3,3) sharing r=6, origin is in both rings. Locked to
# alpha, Tab should request beta (normal cycle behavior; range-aware skip-current
# does not interfere when the next id is also in range).
# With elite (0,0), melee (-3,-3), and ranged (3,3) sharing r=6, origin is in elite/melee
# rings. Locked to elite, Tab should request melee (normal cycle behavior; range-aware
# skip-current does not interfere when the next id is also in range).
var transport := MockHttpTransport.new()
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ELITE_ID, "ok", 1)
)
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, BETA_ID, "ok", 2)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, MELEE_ID, "ok", 2)
)
var c := _make_client(transport)
var authority := FreshnessAuthorityStub.new()
@ -398,8 +403,8 @@ func test_tab_from_locked_alpha_at_origin_swaps_to_beta_when_both_in_range() ->
c.request_tab_next()
c.request_tab_next()
# Assert
assert_that(transport.last_body).contains('"targetId":"%s"' % BETA_ID)
assert_that(c.cached_state().get("lockedTargetId")).is_equal(BETA_ID)
assert_that(transport.last_body).contains('"targetId":"%s"' % MELEE_ID)
assert_that(c.cached_state().get("lockedTargetId")).is_equal(MELEE_ID)
func test_tab_when_only_current_lock_is_in_range_falls_back_to_cycle_for_denial() -> void:
@ -410,39 +415,39 @@ func test_tab_when_only_current_lock_is_in_range_falls_back_to_cycle_for_denial(
# visible reaction to Tab.
var transport := MockHttpTransport.new()
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, MELEE_ID, "ok", 1)
)
transport.enqueue(
HTTPRequest.RESULT_SUCCESS,
200,
_select_response_json(false, ALPHA_ID, "ok", 1, "out_of_range")
_select_response_json(false, MELEE_ID, "ok", 1, "out_of_range")
)
var c := _make_client(transport)
var authority := FreshnessAuthorityStub.new()
auto_free(authority)
add_child(authority)
# Near alpha (-3, -3), ~1.4 m in — beta (3, 3) is ~8.5 m out.
var player := _make_player(Vector3(-2.0, 0.5, -2.0))
# Near melee (-3, -3), inside its ring — elite at origin and ranged at (3, 3) are out.
var player := _make_player(Vector3(-8.0, 0.5, -3.0))
c.set_freshness_kick(authority, player)
# Act
c.request_select_target_id(ALPHA_ID)
c.request_select_target_id(MELEE_ID)
c.request_tab_next()
# Assert
assert_that(transport.last_body).contains('"targetId":"%s"' % BETA_ID)
assert_that(transport.last_body).contains('"targetId":"%s"' % RANGED_ID)
assert_that(c.cached_state().get("reasonCode", "")).is_equal("out_of_range")
assert_that(c.cached_state().get("lockedTargetId")).is_equal(ALPHA_ID)
assert_that(c.cached_state().get("lockedTargetId")).is_equal(MELEE_ID)
func test_tab_without_player_ref_still_uses_plain_cycle_order() -> void:
# Arrange
# Regression: headless unit tests and any client that skips `set_freshness_kick(...)`
# must keep the pre-existing cycle-order semantics. Alpha is requested from no-lock.
# must keep the pre-existing cycle-order semantics. Elite is requested from no-lock.
var transport := MockHttpTransport.new()
transport.enqueue(
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ELITE_ID, "ok", 1)
)
var c := _make_client(transport)
# Act
c.request_tab_next()
# Assert
assert_that(transport.last_body).contains('"targetId":"%s"' % ALPHA_ID)
assert_that(transport.last_body).contains('"targetId":"%s"' % ELITE_ID)