## 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()