Extract SnapshotHandler (static snapshot parsing), SnapshotConsumers (non-dialogue consumers + audio handlers), and DialogueCoordinator (dialogue consumers + signal handlers) as class_name scripts. main.gd: 28KB → 13KB. game_state.gd: 20KB → 8.5KB. Autoload parse-order safety maintained via load() inline pattern. Ticket: #775 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
165 lines
6.1 KiB
GDScript
165 lines
6.1 KiB
GDScript
## Sprint 20 #557: GameState.apply_snapshot() refactor tests.
|
|
##
|
|
## Verifies D-020 compliance: apply_snapshot() reads server-authoritative values
|
|
## for stationary_ticks and zone_id directly from the snapshot when present,
|
|
## and degrades gracefully when the server has not yet added these fields.
|
|
##
|
|
## Does NOT replace test_game_state.gd or test_snapshot_zone_id.gd — those cover
|
|
## existing behaviour. This file covers the new code paths added in Sprint 20.
|
|
##
|
|
## D-030: fixture-based, server-free, no subprocess required.
|
|
class_name TestGameStateSprint20
|
|
extends GdUnitTestSuite
|
|
|
|
|
|
func before_each() -> void:
|
|
GameState.stationary_ticks = 0
|
|
SnapshotHandler._prev_player_position = Vector2(-1e9, -1e9)
|
|
GameState.current_zone_id = ""
|
|
GameState.player_position = Vector2.ZERO
|
|
GameState.visible_tiles = []
|
|
GameState.visible_positions = {}
|
|
GameState.visibility_sectors = {}
|
|
|
|
|
|
# -- stationary_ticks: server-authoritative path (D-020) ----------------------
|
|
|
|
func test_stationary_ticks_reads_server_value_when_present() -> void:
|
|
# When snapshot includes stationary_ticks, apply_snapshot() must use the
|
|
# server value directly without client-side accumulation (D-020).
|
|
var snapshot := {
|
|
"tick": 5,
|
|
"entities": [
|
|
{"entity_id": 1, "x": 10.0, "y": 10.0, "z": 0, "kind": {"variant": "Player", "data": null}},
|
|
],
|
|
"stationary_ticks": 42,
|
|
}
|
|
GameState.apply_snapshot(snapshot)
|
|
assert_int(GameState.stationary_ticks).override_failure_message(
|
|
"apply_snapshot() must read stationary_ticks=42 from snapshot (D-020)"
|
|
).is_equal(42)
|
|
|
|
|
|
func test_stationary_ticks_server_value_does_not_accumulate() -> void:
|
|
# Server-sent value must be assigned directly — NOT added to existing value.
|
|
# Two calls with stationary_ticks=10 must yield 10, not 20.
|
|
var snapshot := {
|
|
"tick": 1,
|
|
"entities": [
|
|
{"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}},
|
|
],
|
|
"stationary_ticks": 10,
|
|
}
|
|
GameState.apply_snapshot(snapshot)
|
|
GameState.apply_snapshot(snapshot)
|
|
assert_int(GameState.stationary_ticks).override_failure_message(
|
|
"Server value must be assigned directly, not accumulated (D-020)"
|
|
).is_equal(10)
|
|
|
|
|
|
func test_stationary_ticks_server_can_reset_to_zero() -> void:
|
|
# Server sends 0 when player moves — client must accept this reset.
|
|
GameState.stationary_ticks = 50
|
|
var snapshot := {
|
|
"tick": 2,
|
|
"entities": [
|
|
{"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}},
|
|
],
|
|
"stationary_ticks": 0,
|
|
}
|
|
GameState.apply_snapshot(snapshot)
|
|
assert_int(GameState.stationary_ticks).override_failure_message(
|
|
"Server reset to 0 must override client-held value"
|
|
).is_equal(0)
|
|
|
|
|
|
# -- stationary_ticks: DEPRECATED fallback (graceful degradation) --------------
|
|
|
|
func test_stationary_ticks_fallback_when_field_absent() -> void:
|
|
# When snapshot has no stationary_ticks, client-side accumulation must still
|
|
# run (backward compat until server ships field). No crash.
|
|
var snapshot := {
|
|
"tick": 1,
|
|
"entities": [
|
|
{"entity_id": 1, "x": 10.0, "y": 10.0, "z": 0, "kind": {"variant": "Player", "data": null}},
|
|
],
|
|
}
|
|
GameState.apply_snapshot(snapshot) # position changes from ZERO → resets to 0
|
|
GameState.apply_snapshot(snapshot) # position unchanged → increments
|
|
assert_int(GameState.stationary_ticks).override_failure_message(
|
|
"Client-side fallback must increment stationary_ticks when field absent"
|
|
).is_greater(0)
|
|
|
|
|
|
func test_apply_snapshot_missing_stationary_ticks_no_crash() -> void:
|
|
# Snapshots lacking stationary_ticks must not crash apply_snapshot().
|
|
var snapshot := {"tick": 1, "entities": []}
|
|
# No assertion needed beyond confirming no exception is raised.
|
|
GameState.apply_snapshot(snapshot)
|
|
assert_bool(true).is_true()
|
|
|
|
|
|
# -- zone_id: server-authoritative path (D-020/D-073) -------------------------
|
|
|
|
func test_current_zone_id_reads_server_value_when_present() -> void:
|
|
# When snapshot includes top-level zone_id, apply_snapshot() must use it
|
|
# directly without tile lookup (D-020).
|
|
var snapshot := {
|
|
"tick": 1,
|
|
"entities": [
|
|
{"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}},
|
|
],
|
|
"zone_id": "zone_server_direct",
|
|
}
|
|
GameState.apply_snapshot(snapshot)
|
|
assert_str(GameState.current_zone_id).override_failure_message(
|
|
"apply_snapshot() must read zone_id='zone_server_direct' from snapshot (D-020)"
|
|
).is_equal("zone_server_direct")
|
|
|
|
|
|
func test_current_zone_id_server_value_overrides_tile_data() -> void:
|
|
# When snapshot has both zone_id and visible_tiles with a different zone,
|
|
# the top-level zone_id field takes priority.
|
|
var snapshot := {
|
|
"tick": 1,
|
|
"entities": [
|
|
{"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}},
|
|
],
|
|
"zone_id": "zone_from_server",
|
|
"visible_tiles": [
|
|
{"x": 5, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_from_tile"},
|
|
],
|
|
}
|
|
GameState.apply_snapshot(snapshot)
|
|
assert_str(GameState.current_zone_id).override_failure_message(
|
|
"Top-level zone_id must override tile-derived zone when both present"
|
|
).is_equal("zone_from_server")
|
|
|
|
|
|
# -- zone_id: DEPRECATED fallback (graceful degradation) ----------------------
|
|
|
|
func test_current_zone_id_fallback_to_tile_lookup_when_absent() -> void:
|
|
# When snapshot lacks top-level zone_id, tile lookup fallback must run.
|
|
# Existing test_snapshot_zone_id.gd covers detailed scenarios; this is a
|
|
# smoke test confirming the fallback path still works after Sprint 20 refactor.
|
|
var snapshot := {
|
|
"tick": 1,
|
|
"entities": [
|
|
{"entity_id": 1, "x": 3.0, "y": 3.0, "z": 0, "kind": {"variant": "Player", "data": null}},
|
|
],
|
|
"visible_tiles": [
|
|
{"x": 3, "y": 3, "z": 0, "type": "floor", "zone_id": "zone_tile_fallback"},
|
|
],
|
|
}
|
|
GameState.apply_snapshot(snapshot)
|
|
assert_str(GameState.current_zone_id).override_failure_message(
|
|
"Tile lookup fallback must populate zone_id when top-level field absent"
|
|
).is_equal("zone_tile_fallback")
|
|
|
|
|
|
func test_apply_snapshot_missing_zone_id_no_crash() -> void:
|
|
# Snapshots lacking both zone_id and visible_tiles must not crash.
|
|
var snapshot := {"tick": 1, "entities": []}
|
|
GameState.apply_snapshot(snapshot)
|
|
assert_str(GameState.current_zone_id).is_equal("")
|