Files
settled-reach/client/tests/test_game_state.gd
T
jpmschweitzerandClaude Opus 4.6 260eefcdd1 refactor(client): decompose main.gd and game_state.gd god objects
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>
2026-04-05 09:59:34 +02:00

220 lines
8.5 KiB
GDScript

## GameState.apply_snapshot() tests — v2+ field coverage.
##
## Complements test_snapshot_parsing.gd (which covers v1 basics: tick, entities,
## player_position). This file covers v2+ fields and derived state.
##
## D-030: fixture-based, server-free, no subprocess required.
class_name TestGameState
extends GdUnitTestSuite
func before_each() -> void:
# Reset fields touched by these tests to known defaults.
GameState.current_tick = 0
GameState.player_position = Vector2.ZERO
GameState.player_facing = "North"
GameState.game_time = {}
GameState.nearby_interactions = []
GameState.current_monologue = null
GameState.player_stance = "Walk"
GameState.player_inventory = []
GameState.stationary_ticks = 0
SnapshotHandler._prev_player_position = Vector2(-1e9, -1e9)
GameState.current_zone_id = ""
GameState.insert_active = true
# -- v2: game_time (D-031) -------------------------------------------------
func test_apply_snapshot_sets_game_time() -> void:
var snapshot := {
"tick": 10,
"entities": [],
"game_time": {"day": 3, "time_of_day": 480, "day_phase": "Morning", "tick_rate": 1},
}
GameState.apply_snapshot(snapshot)
assert_that(GameState.game_time).is_not_null()
assert_that(GameState.game_time.get("day")).is_equal(3)
assert_that(GameState.game_time.get("time_of_day")).is_equal(480)
func test_apply_snapshot_game_time_missing_keeps_previous() -> void:
GameState.game_time = {"day": 2, "time_of_day": 360}
GameState.apply_snapshot({"tick": 5, "entities": []})
# No "game_time" key → field unchanged
assert_that(GameState.game_time.get("day")).is_equal(2)
# -- v2: player_facing (D-015) --------------------------------------------
func test_apply_snapshot_sets_player_facing() -> void:
var snapshot := {
"tick": 1,
"entities": [],
"player_facing": "Southeast",
}
GameState.apply_snapshot(snapshot)
assert_that(GameState.player_facing).is_equal("Southeast")
func test_apply_snapshot_player_facing_missing_keeps_default() -> void:
GameState.player_facing = "West"
GameState.apply_snapshot({"tick": 1, "entities": []})
assert_that(GameState.player_facing).is_equal("West")
# -- v4: nearby_interactions (#404/#405) ----------------------------------
func test_apply_snapshot_sets_nearby_interactions() -> void:
var interactions := [
{"entity_id": 5, "entity_type": "Npc", "distance": 1.2,
"verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}]},
]
GameState.apply_snapshot({"tick": 1, "entities": [], "nearby_interactions": interactions})
assert_that(GameState.nearby_interactions.size()).is_equal(1)
assert_that(GameState.nearby_interactions[0].get("entity_id")).is_equal(5)
func test_apply_snapshot_nearby_interactions_absent_clears_list() -> void:
GameState.nearby_interactions = [{"entity_id": 1}]
GameState.apply_snapshot({"tick": 2, "entities": []})
assert_that(GameState.nearby_interactions.size()).is_equal(0)
# -- v5: current_monologue (#414) -----------------------------------------
func test_apply_snapshot_sets_monologue() -> void:
var monologue := {
"id": "m1", "text": "Something is off here.",
"duration_seconds": 4.0, "priority": 1, "is_urgent": false,
}
GameState.apply_snapshot({"tick": 1, "entities": [], "current_monologue": monologue})
assert_that(GameState.current_monologue).is_not_null()
assert_that(GameState.current_monologue.get("text")).is_equal("Something is off here.")
func test_apply_snapshot_monologue_absent_clears_field() -> void:
GameState.current_monologue = {"id": "old", "text": "Old line."}
GameState.apply_snapshot({"tick": 2, "entities": []})
assert_that(GameState.current_monologue).is_null()
# -- v6: player_stance (#449, D-053) --------------------------------------
func test_apply_snapshot_sets_player_stance() -> void:
GameState.apply_snapshot({"tick": 1, "entities": [], "player_stance": "Crouch"})
assert_that(GameState.player_stance).is_equal("Crouch")
# -- v6: player_inventory (#449, D-065) -----------------------------------
func test_apply_snapshot_sets_player_inventory() -> void:
var inventory := [{"item_id": 42, "name": "Security pass", "slot": 0}]
GameState.apply_snapshot({"tick": 1, "entities": [], "player_inventory": inventory})
assert_that(GameState.player_inventory.size()).is_equal(1)
assert_that(GameState.player_inventory[0].get("name")).is_equal("Security pass")
func test_apply_snapshot_inventory_absent_clears_list() -> void:
GameState.player_inventory = [{"item_id": 1}]
GameState.apply_snapshot({"tick": 2, "entities": []})
assert_that(GameState.player_inventory.size()).is_equal(0)
# -- Stationary ticks: server-authoritative (D-020/D-071) -----------------
func test_stationary_ticks_from_server_snapshot() -> void:
## D-020: When server sends stationary_ticks, client reads it directly.
GameState.apply_snapshot({"tick": 1, "entities": [], "stationary_ticks": 42})
assert_int(GameState.stationary_ticks).is_equal(42)
func test_stationary_ticks_server_value_overrides_client_accumulation() -> void:
## D-020: Server value takes priority — client must not accumulate on top of it.
var snapshot := {
"tick": 1,
"entities": [{"entity_id": 1, "x": 10.0, "y": 10.0, "z": 0, "kind": {"variant": "Player", "data": null}}],
"stationary_ticks": 5,
}
GameState.apply_snapshot(snapshot)
GameState.apply_snapshot(snapshot) # same position, but server sends 5 again
assert_int(GameState.stationary_ticks).is_equal(5) # server value, not 6
func test_stationary_ticks_missing_field_degrades_gracefully() -> void:
## D-020 fallback: when server omits stationary_ticks, no crash, defaults to 0.
GameState.stationary_ticks = 0
GameState.apply_snapshot({"tick": 1, "entities": []})
# No crash; field retains a valid integer value.
assert_int(GameState.stationary_ticks).is_greater_equal(0)
# -- Stationary ticks: deprecated client-side fallback (D-071) -----------
func test_stationary_ticks_fallback_increments_when_position_unchanged() -> void:
## Deprecated fallback: client accumulates when server omits the field.
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) # first call: position changes from sentinel
GameState.apply_snapshot(snapshot) # second call: position unchanged → +1
assert_int(GameState.stationary_ticks).is_greater(0)
func test_stationary_ticks_fallback_resets_on_movement() -> void:
## Deprecated fallback: client resets on movement when server omits the field.
var s1 := {
"tick": 1,
"entities": [{"entity_id": 1, "x": 10.0, "y": 10.0, "z": 0, "kind": {"variant": "Player", "data": null}}],
}
var s2 := {
"tick": 2,
"entities": [{"entity_id": 1, "x": 11.0, "y": 10.0, "z": 0, "kind": {"variant": "Player", "data": null}}],
}
GameState.apply_snapshot(s1)
GameState.apply_snapshot(s1) # stationary
assert_int(GameState.stationary_ticks).is_greater(0)
GameState.apply_snapshot(s2) # moved → reset
assert_int(GameState.stationary_ticks).is_equal(0)
# -- Zone ID: server-authoritative (D-020/D-073) -------------------------
func test_zone_id_from_server_snapshot() -> void:
## D-020: When server sends top-level zone_id, client reads it directly.
GameState.apply_snapshot({"tick": 1, "entities": [], "zone_id": "zone_alpha"})
assert_that(GameState.current_zone_id).is_equal("zone_alpha")
func test_zone_id_server_value_overrides_tile_derivation() -> void:
## D-020: Server top-level zone_id takes priority over tile-derived zone_id.
var snapshot := {
"tick": 1,
"entities": [{"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}}],
"tiles": [{"x": 5, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_from_tile"}],
"zone_id": "zone_from_server",
}
GameState.apply_snapshot(snapshot)
assert_that(GameState.current_zone_id).is_equal("zone_from_server")
func test_zone_id_missing_field_degrades_gracefully() -> void:
## D-020 fallback: when server omits zone_id and no tiles match, defaults to "".
GameState.current_zone_id = ""
GameState.apply_snapshot({"tick": 1, "entities": []})
assert_that(GameState.current_zone_id).is_equal("")
# -- insert_active (OQ-07, #522) -----------------------------------------
func test_apply_snapshot_insert_active_false() -> void:
GameState.apply_snapshot({"tick": 1, "entities": [], "insert_active": false})
assert_bool(GameState.insert_active).is_false()
func test_apply_snapshot_insert_active_defaults_true_when_absent() -> void:
GameState.insert_active = false
GameState.apply_snapshot({"tick": 2, "entities": []})
assert_bool(GameState.insert_active).is_true()