extends Node # Updated each frame from ObserverSnapshot data (Protocol format: {tick, entities, tiles}). # Entities use Protocol decoded format: {entity_id, x, y, z, kind: {variant, data}}. # Tiles use format: [{x, y, z, type}]. var current_snapshot: Dictionary = {} var current_tick: int = 0 var player_position: Vector2 = Vector2.ZERO var visible_entities: Array = [] var visible_tiles: Array = [] var visible_positions: Dictionary = {} # Vector2i -> true, for fast fog lookups # v2 fields (D-015, D-031) var game_time: Dictionary = {} # {day, time_of_day, day_phase, tick_rate} or empty var player_facing: String = "North" # 8-directional facing direction var visibility_sectors: Dictionary = {} # Vector2i -> "Forward"/"Peripheral" # Player entity ID — the first entity is assumed to be the player (will be # refined when the server assigns explicit player entity IDs). var player_entity_id: int = 1 # v4 fields (#404/#405) var nearby_interactions: Array = [] # [{entity_id, entity_type, distance, verbs: [{kind, label, priority, available}]}] # v5 fields (#414) var current_monologue: Variant = null # {id, text, duration_seconds} or null # v6 fields (#449, D-053, D-065) var player_stance: String = "Walk" # Sprint/Walk/Careful/Crouch var player_inventory: Array = [] # [{item_id, name, slot}] # v7 fields (#435, D-061/D-062) var current_dialogue: Variant = null # {npc_name, npc_entity_id, speech, options: [{text, response_id, priority}]} or null # D-064: true while dialogue box is visible or fading out (300ms). # InputMapper suppresses movement when this is true. var dialogue_active: bool = false # v8 fields (#496): Gauntlet mode — room timer + personal bests var room_id: Variant = null # String room_id from snapshot, null in non-gauntlet mode var gauntlet_mode: bool = false # true when snapshot includes gauntlet_mode flag # v7 fields (#431, D-059/D-060) var pending_recognitions: Array = [] # [{entity_id, x, y, z, remaining_ticks, total_delay_ticks}] func apply_snapshot(snapshot: Dictionary) -> void: current_snapshot = snapshot if snapshot.has("tick"): current_tick = snapshot.tick if snapshot.has("entities"): visible_entities = snapshot.entities # Derive player position from the entity with kind.variant == "Player" var found_player := false for entity in visible_entities: if entity.has("kind") and entity.kind is Dictionary and entity.kind.get("variant") == "Player": player_position = Vector2(entity.x, entity.y) if entity.has("entity_id"): player_entity_id = entity.entity_id found_player = true break if not found_player and visible_entities.size() > 0: push_warning("GameState: no Player entity found in %d entities" % [ visible_entities.size()]) # Tiles for rendering: test mode sends "tiles", live server sends tile data in "visible_tiles" if snapshot.has("tiles"): visible_tiles = snapshot.tiles elif snapshot.has("visible_tiles") and snapshot.visible_tiles is Array and snapshot.visible_tiles.size() > 0: # Live server: visible_tiles now includes type from tile_kind field var has_type := false if snapshot.visible_tiles.size() > 0 and snapshot.visible_tiles[0] is Dictionary: has_type = snapshot.visible_tiles[0].has("type") if has_type: visible_tiles = snapshot.visible_tiles if snapshot.has("visible_positions"): visible_positions.clear() for pos in snapshot.visible_positions: visible_positions[Vector2i(pos.x, pos.y)] = true # v2: game_time (D-031) if snapshot.has("game_time") and snapshot.game_time is Dictionary: game_time = snapshot.game_time # v2: player_facing (D-015) if snapshot.has("player_facing") and snapshot.player_facing is String: player_facing = snapshot.player_facing # v4: nearby_interactions (#404/#405) if snapshot.has("nearby_interactions") and snapshot.nearby_interactions is Array: nearby_interactions = snapshot.nearby_interactions else: nearby_interactions = [] # v5: current_monologue (#414) if snapshot.has("current_monologue") and snapshot.current_monologue is Dictionary: current_monologue = snapshot.current_monologue else: current_monologue = null # v6: player_stance (#449, D-053) if snapshot.has("player_stance") and snapshot.player_stance is String: player_stance = snapshot.player_stance # v6: player_inventory (#449, D-065) if snapshot.has("player_inventory") and snapshot.player_inventory is Array: player_inventory = snapshot.player_inventory else: player_inventory = [] # v7: current_dialogue (#434, D-061) if snapshot.has("current_dialogue") and snapshot.current_dialogue is Dictionary: current_dialogue = snapshot.current_dialogue else: current_dialogue = null # v7: pending_recognitions (#431, D-059/D-060) if snapshot.has("pending_recognitions") and snapshot.pending_recognitions is Array: pending_recognitions = snapshot.pending_recognitions else: pending_recognitions = [] # v8: gauntlet mode (#496) — room_id and gauntlet_mode if snapshot.has("gauntlet_mode") and snapshot.gauntlet_mode == true: gauntlet_mode = true else: gauntlet_mode = false if snapshot.has("room_id") and snapshot.room_id is String: room_id = snapshot.room_id else: room_id = null # v2: visible_tiles with visibility sectors # Derives visible_positions when not explicitly provided (real server mode) if snapshot.has("visible_tiles") and snapshot.visible_tiles is Array and snapshot.visible_tiles.size() > 0: visibility_sectors.clear() var has_explicit_positions := snapshot.has("visible_positions") if not has_explicit_positions: visible_positions.clear() for vtile in snapshot.visible_tiles: if not vtile is Dictionary or not vtile.has("x") or not vtile.has("y"): continue var pos := Vector2i(vtile.x, vtile.y) if vtile.has("visibility"): visibility_sectors[pos] = vtile.visibility if not has_explicit_positions: visible_positions[pos] = true