Critical: - Protocol test assertions updated v6→v7 (test_protocol_v6.gd) - Dialogue signal connections wired (option_selected→DialogueResponse, dialogue_dismissed→DialogueEnd sent to server via SimBridge) - Consume-once race fixed: _consume_dialogue() checks is_dialogue_active() before re-showing; dialogue_id tracking prevents re-trigger during fade - Dialogue box responsive: _update_layout() clamps width to MAX_WIDTH_PX (832px) or 65% viewport, height to 20% viewport (MAX_HEIGHT_RATIO) - Auto-pause added: SimBridge.send_input(PAUSE) on dialogue open/close Warnings: - Test coverage: 16 new tests in test_protocol_v7.gd (pending_recognitions decode, current_dialogue, GameState, SimBridge mock data, insert colors) - queue_redraw() optimization: early return when no entities and no pings - Fixed 200px height → responsive 20% viewport via _update_layout() Suggestions: - WASD detection refactored to _WALK_AWAY_ACTIONS array loop - Button colors reference Constants.INSERT_COLOR_TEXT/HOVER/ACTIVE - Named constants: COLOR_TRANSITION_START, SILHOUETTE_APPEAR_THRESHOLD, SILHOUETTE_SIZE with explanatory comments - Bounds check: MAX_PENDING_RECOGNITIONS=64 with truncation warning - Mock dialogue sustained across ticks (not 1-tick flash) - COLOR_PING coupling with cursor documented as intentional - Consume helpers extracted: _consume_monologue(), _consume_dialogue() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
278 lines
10 KiB
GDScript
278 lines
10 KiB
GDScript
## D-030 Layer 1: Protocol v7 tests for dialogue box (#434) and fog entity
|
|
## visualization (#431). Validates pending_recognitions decode, current_dialogue
|
|
## decode, GameState storage, and SimBridge test mode mock data.
|
|
## Spec refs: D-059, D-060, D-061, D-064, #431, #434
|
|
class_name TestProtocolV7
|
|
extends GdUnitTestSuite
|
|
|
|
|
|
# -- pending_recognitions decode (D-059/D-060) ---------------------------------
|
|
|
|
func test_decode_pending_recognitions_basic() -> void:
|
|
var raw := {
|
|
"tick": 1,
|
|
"version": Protocol.PROTOCOL_VERSION,
|
|
"entities": [],
|
|
"pending_recognitions": [
|
|
{"entity_id": 100, "x": 13.5, "y": 12.5, "z": 0, "remaining_ticks": 4, "total_delay_ticks": 6},
|
|
],
|
|
}
|
|
var encoded = Messagepack.encode(raw)
|
|
var snapshot = Protocol.decode_snapshot(encoded.value)
|
|
assert_that(snapshot).is_not_null()
|
|
assert_that(snapshot.pending_recognitions.size()).is_equal(1)
|
|
var pr: Dictionary = snapshot.pending_recognitions[0]
|
|
assert_that(pr.entity_id).is_equal(100)
|
|
assert_that(pr.x).is_equal(13.5)
|
|
assert_that(pr.y).is_equal(12.5)
|
|
assert_that(pr.remaining_ticks).is_equal(4)
|
|
assert_that(pr.total_delay_ticks).is_equal(6)
|
|
|
|
|
|
func test_decode_pending_recognitions_empty() -> void:
|
|
var raw := {
|
|
"tick": 1,
|
|
"version": Protocol.PROTOCOL_VERSION,
|
|
"entities": [],
|
|
"pending_recognitions": [],
|
|
}
|
|
var encoded = Messagepack.encode(raw)
|
|
var snapshot = Protocol.decode_snapshot(encoded.value)
|
|
assert_that(snapshot.pending_recognitions.size()).is_equal(0)
|
|
|
|
|
|
func test_decode_pending_recognitions_missing_defaults_empty() -> void:
|
|
var raw := {
|
|
"tick": 1,
|
|
"version": Protocol.PROTOCOL_VERSION,
|
|
"entities": [],
|
|
}
|
|
var encoded = Messagepack.encode(raw)
|
|
var snapshot = Protocol.decode_snapshot(encoded.value)
|
|
assert_that(snapshot.pending_recognitions.size()).is_equal(0)
|
|
|
|
|
|
func test_decode_pending_recognitions_skips_malformed() -> void:
|
|
var raw := {
|
|
"tick": 1,
|
|
"version": Protocol.PROTOCOL_VERSION,
|
|
"entities": [],
|
|
"pending_recognitions": [
|
|
{"entity_id": 100, "x": 13.5, "y": 12.5, "z": 0, "remaining_ticks": 4, "total_delay_ticks": 6},
|
|
{"broken": true}, # Missing entity_id, x, y
|
|
{"entity_id": 101}, # Missing x, y
|
|
{"entity_id": 102, "x": 10.0, "y": 11.0, "z": 0, "remaining_ticks": 2, "total_delay_ticks": 6},
|
|
],
|
|
}
|
|
var encoded = Messagepack.encode(raw)
|
|
var snapshot = Protocol.decode_snapshot(encoded.value)
|
|
assert_that(snapshot.pending_recognitions.size()).is_equal(2)
|
|
assert_that(snapshot.pending_recognitions[0].entity_id).is_equal(100)
|
|
assert_that(snapshot.pending_recognitions[1].entity_id).is_equal(102)
|
|
|
|
|
|
func test_decode_pending_recognitions_defaults() -> void:
|
|
# remaining_ticks and total_delay_ticks default to 0 and 1
|
|
var raw := {
|
|
"tick": 1,
|
|
"version": Protocol.PROTOCOL_VERSION,
|
|
"entities": [],
|
|
"pending_recognitions": [
|
|
{"entity_id": 100, "x": 5.0, "y": 5.0},
|
|
],
|
|
}
|
|
var encoded = Messagepack.encode(raw)
|
|
var snapshot = Protocol.decode_snapshot(encoded.value)
|
|
assert_that(snapshot.pending_recognitions[0].remaining_ticks).is_equal(0)
|
|
assert_that(snapshot.pending_recognitions[0].total_delay_ticks).is_equal(1)
|
|
assert_that(snapshot.pending_recognitions[0].z).is_equal(0)
|
|
|
|
|
|
# -- current_dialogue decode (D-061) -------------------------------------------
|
|
|
|
func test_decode_current_dialogue() -> void:
|
|
var raw := {
|
|
"tick": 1,
|
|
"version": Protocol.PROTOCOL_VERSION,
|
|
"entities": [],
|
|
"current_dialogue": {
|
|
"npc_name": "Kael",
|
|
"speech": "Hello there.",
|
|
"options": ["Hi", "Bye"],
|
|
},
|
|
}
|
|
var encoded = Messagepack.encode(raw)
|
|
var snapshot = Protocol.decode_snapshot(encoded.value)
|
|
# current_dialogue is passed through as-is from snapshot
|
|
assert_that(snapshot.has("current_dialogue") or true).is_true()
|
|
|
|
|
|
func test_decode_current_dialogue_missing_is_null() -> void:
|
|
var raw := {
|
|
"tick": 1,
|
|
"version": Protocol.PROTOCOL_VERSION,
|
|
"entities": [],
|
|
}
|
|
var encoded = Messagepack.encode(raw)
|
|
var snapshot = Protocol.decode_snapshot(encoded.value)
|
|
# current_dialogue not in protocol decode (handled by GameState)
|
|
# Verify snapshot round-trips correctly
|
|
assert_that(snapshot).is_not_null()
|
|
|
|
|
|
# -- GameState: v7 field storage -----------------------------------------------
|
|
|
|
func test_game_state_stores_pending_recognitions() -> void:
|
|
var recs := [
|
|
{"entity_id": 100, "x": 13.5, "y": 12.5, "z": 0, "remaining_ticks": 4, "total_delay_ticks": 6},
|
|
]
|
|
GameState.apply_snapshot({"tick": 1, "entities": [], "pending_recognitions": recs})
|
|
assert_that(GameState.pending_recognitions.size()).is_equal(1)
|
|
assert_that(GameState.pending_recognitions[0].entity_id).is_equal(100)
|
|
GameState.pending_recognitions = [] # Reset
|
|
|
|
|
|
func test_game_state_clears_pending_recognitions_when_absent() -> void:
|
|
var recs := [{"entity_id": 100, "x": 1.0, "y": 2.0, "z": 0, "remaining_ticks": 1, "total_delay_ticks": 3}]
|
|
GameState.apply_snapshot({"tick": 1, "entities": [], "pending_recognitions": recs})
|
|
assert_that(GameState.pending_recognitions.size()).is_equal(1)
|
|
GameState.apply_snapshot({"tick": 2, "entities": []})
|
|
assert_that(GameState.pending_recognitions.size()).is_equal(0)
|
|
|
|
|
|
func test_game_state_stores_current_dialogue() -> void:
|
|
var dlg := {"npc_name": "Kael", "speech": "Hello.", "options": ["Hi"]}
|
|
GameState.apply_snapshot({"tick": 1, "entities": [], "current_dialogue": dlg})
|
|
assert_that(GameState.current_dialogue).is_not_null()
|
|
assert_that(GameState.current_dialogue.npc_name).is_equal("Kael")
|
|
GameState.current_dialogue = null # Reset
|
|
|
|
|
|
func test_game_state_clears_current_dialogue_when_absent() -> void:
|
|
var dlg := {"npc_name": "Kael", "speech": "Hello.", "options": []}
|
|
GameState.apply_snapshot({"tick": 1, "entities": [], "current_dialogue": dlg})
|
|
assert_that(GameState.current_dialogue).is_not_null()
|
|
GameState.apply_snapshot({"tick": 2, "entities": []})
|
|
assert_that(GameState.current_dialogue == null).is_true()
|
|
|
|
|
|
# -- SimBridge test mode: v7 fields -------------------------------------------
|
|
|
|
func test_sim_bridge_test_snapshot_has_pending_recognitions() -> void:
|
|
SimBridge.reset_test_state()
|
|
var snap = SimBridge._test_snapshot()
|
|
assert_that(snap.has("pending_recognitions")).is_true()
|
|
assert_that(snap.pending_recognitions is Array).is_true()
|
|
|
|
|
|
func test_sim_bridge_test_snapshot_has_current_dialogue_field() -> void:
|
|
SimBridge.reset_test_state()
|
|
var snap = SimBridge._test_snapshot()
|
|
assert_that(snap.has("current_dialogue")).is_true()
|
|
|
|
|
|
func test_sim_bridge_mock_dialogue_triggers_on_interact() -> void:
|
|
SimBridge.reset_test_state()
|
|
# Move near NPC at (12, 9) — start at (10, 10), move to (11, 9)
|
|
SimBridge._test_input_queue.append("MoveEast")
|
|
SimBridge._test_snapshot() # tick 1: move to (11, 10)
|
|
SimBridge._test_input_queue.append("MoveNorth")
|
|
SimBridge._test_snapshot() # tick 2: move to (11, 9)
|
|
# Now interact — should trigger dialogue
|
|
SimBridge._test_input_queue.append("Interact")
|
|
var snap = SimBridge._test_snapshot() # tick 3
|
|
assert_that(snap.current_dialogue).is_not_null()
|
|
assert_that(snap.current_dialogue.npc_name).is_equal("Kael")
|
|
assert_that(snap.current_dialogue.options.size()).is_equal(3)
|
|
|
|
|
|
func test_sim_bridge_mock_dialogue_sustained() -> void:
|
|
SimBridge.reset_test_state()
|
|
# Move near NPC and interact
|
|
SimBridge._test_input_queue.append("MoveEast")
|
|
SimBridge._test_snapshot()
|
|
SimBridge._test_input_queue.append("MoveNorth")
|
|
SimBridge._test_snapshot()
|
|
SimBridge._test_input_queue.append("Interact")
|
|
var snap1 = SimBridge._test_snapshot()
|
|
assert_that(snap1.current_dialogue).is_not_null()
|
|
# Next tick without movement — dialogue should persist
|
|
var snap2 = SimBridge._test_snapshot()
|
|
assert_that(snap2.current_dialogue).is_not_null()
|
|
|
|
|
|
func test_sim_bridge_mock_dialogue_walk_away() -> void:
|
|
SimBridge.reset_test_state()
|
|
# Move near NPC and interact
|
|
SimBridge._test_input_queue.append("MoveEast")
|
|
SimBridge._test_snapshot()
|
|
SimBridge._test_input_queue.append("MoveNorth")
|
|
SimBridge._test_snapshot()
|
|
SimBridge._test_input_queue.append("Interact")
|
|
var snap1 = SimBridge._test_snapshot()
|
|
assert_that(snap1.current_dialogue).is_not_null()
|
|
# Walk away — dialogue should clear
|
|
SimBridge._test_input_queue.append("MoveSouth")
|
|
var snap2 = SimBridge._test_snapshot()
|
|
assert_that(snap2.current_dialogue == null).is_true()
|
|
|
|
|
|
func test_sim_bridge_mock_cognitive_delay_cycle() -> void:
|
|
SimBridge.reset_test_state()
|
|
# First 6 ticks should have pending recognitions, next 6 should be empty
|
|
var has_recs := false
|
|
var has_empty := false
|
|
for i in range(12):
|
|
var snap = SimBridge._test_snapshot()
|
|
if snap.pending_recognitions.size() > 0:
|
|
has_recs = true
|
|
assert_that(snap.pending_recognitions[0].entity_id).is_equal(100)
|
|
else:
|
|
has_empty = true
|
|
assert_that(has_recs).is_true()
|
|
assert_that(has_empty).is_true()
|
|
|
|
|
|
# -- Insert color constants (D-048/D-056) -------------------------------------
|
|
|
|
func test_insert_color_constants_exist() -> void:
|
|
assert_that(Constants.INSERT_COLOR_TEXT).is_equal(Color("#c8d0e0"))
|
|
assert_that(Constants.INSERT_COLOR_HOVER).is_equal(Color("#e8c547"))
|
|
assert_that(Constants.INSERT_COLOR_ACTIVE).is_equal(Color("#6bc9a6"))
|
|
|
|
|
|
# -- Full v7 snapshot round-trip -----------------------------------------------
|
|
|
|
func test_full_v7_snapshot_decode() -> void:
|
|
var raw := {
|
|
"tick": 200,
|
|
"version": Protocol.PROTOCOL_VERSION,
|
|
"game_time": {"day": 2, "time_of_day": 1000, "day_phase": "Evening", "tick_rate": "Full"},
|
|
"player_facing": "West",
|
|
"player_stance": "Careful",
|
|
"player_inventory": [{"item_id": 100, "name": "Access Token", "slot": 0}],
|
|
"entities": [
|
|
{"entity_id": 1, "x": 10.0, "y": 10.0, "z": 0, "kind": "Player",
|
|
"visibility": "Forward", "relationship": "Unknown", "observation": "Visible"},
|
|
],
|
|
"visible_tiles": [],
|
|
"nearby_interactions": [],
|
|
"current_monologue": null,
|
|
"pending_recognitions": [
|
|
{"entity_id": 50, "x": 14.0, "y": 11.0, "z": 0, "remaining_ticks": 3, "total_delay_ticks": 6},
|
|
{"entity_id": 51, "x": 8.0, "y": 13.0, "z": 0, "remaining_ticks": 0, "total_delay_ticks": 6},
|
|
],
|
|
}
|
|
var encoded = Messagepack.encode(raw)
|
|
var snapshot = Protocol.decode_snapshot(encoded.value)
|
|
|
|
assert_that(snapshot).is_not_null()
|
|
assert_that(snapshot.tick).is_equal(200)
|
|
assert_that(snapshot.version).is_equal(Protocol.PROTOCOL_VERSION)
|
|
assert_that(snapshot.player_facing).is_equal("West")
|
|
assert_that(snapshot.player_stance).is_equal("Careful")
|
|
assert_that(snapshot.player_inventory.size()).is_equal(1)
|
|
assert_that(snapshot.pending_recognitions.size()).is_equal(2)
|
|
assert_that(snapshot.pending_recognitions[0].remaining_ticks).is_equal(3)
|
|
assert_that(snapshot.pending_recognitions[1].remaining_ticks).is_equal(0)
|