class_name SnapshotHandler ## Applies ObserverSnapshot data to GameState fields (D-020). ## ## Extracted from game_state.gd to separate snapshot parsing from state storage. ## All methods are static — no instance state required. ## Called via GameState.apply_snapshot() which delegates here. # DEPRECATED: Client-side stationary_ticks fallback. Remove when server sends # "stationary_ticks" in ObserverSnapshot (D-020 violation). static var _prev_player_position: Vector2 = Vector2(-1e9, -1e9) static func apply(snapshot: Dictionary) -> void: GameState.current_snapshot = snapshot if snapshot.has("tick"): GameState.current_tick = snapshot.tick if snapshot.has("entities"): GameState.visible_entities = snapshot.entities var found_player := false for entity in GameState.visible_entities: if ( entity.has("kind") and entity.kind is Dictionary and entity.kind.get("variant") == "Player" ): GameState.player_position = Vector2(entity.x, entity.y) if entity.has("entity_id"): GameState.player_entity_id = entity.entity_id found_player = true break if not found_player and GameState.visible_entities.size() > 0: push_warning( ( "GameState: no Player entity found in %d entities" % [GameState.visible_entities.size()] ) ) # D-020/D-071 (#530): Server-authoritative stationary_ticks for ListeningFocus boost. if snapshot.has("stationary_ticks") and snapshot.stationary_ticks is int: GameState.stationary_ticks = snapshot.stationary_ticks else: # DEPRECATED fallback — client-side accumulation. Remove when server sends field. if GameState.player_position == _prev_player_position: GameState.stationary_ticks += 1 else: GameState.stationary_ticks = 0 _prev_player_position = GameState.player_position # Tiles for rendering: test mode sends "tiles", live server sends "visible_tiles" if snapshot.has("tiles"): GameState.visible_tiles = snapshot.tiles elif ( snapshot.has("visible_tiles") and snapshot.visible_tiles is Array and snapshot.visible_tiles.size() > 0 ): 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: GameState.visible_tiles = snapshot.visible_tiles if snapshot.has("visible_positions"): GameState.visible_positions.clear() for pos in snapshot.visible_positions: GameState.visible_positions[Vector2i(pos.x, pos.y)] = true # v2: game_time (D-031) if snapshot.has("game_time") and snapshot.game_time is Dictionary: GameState.game_time = snapshot.game_time # v2: player_facing (D-015) if snapshot.has("player_facing") and snapshot.player_facing is String: GameState.player_facing = snapshot.player_facing # v4: nearby_interactions (#404/#405) if snapshot.has("nearby_interactions") and snapshot.nearby_interactions is Array: GameState.nearby_interactions = snapshot.nearby_interactions else: GameState.nearby_interactions = [] # v5: current_monologue (#414) if snapshot.has("current_monologue") and snapshot.current_monologue is Dictionary: GameState.current_monologue = snapshot.current_monologue else: GameState.current_monologue = null # #122: lattice_profile if snapshot.has("lattice_profile") and snapshot.lattice_profile is String: GameState.lattice_profile = snapshot.lattice_profile # v6: player_stance (#449, D-053) if snapshot.has("player_stance") and snapshot.player_stance is String: GameState.player_stance = snapshot.player_stance # v6: player_inventory (#449, D-065) if snapshot.has("player_inventory") and snapshot.player_inventory is Array: GameState.player_inventory = snapshot.player_inventory else: GameState.player_inventory = [] # v7: current_dialogue (#434, D-061) if snapshot.has("current_dialogue") and snapshot.current_dialogue is Dictionary: GameState.current_dialogue = snapshot.current_dialogue else: GameState.current_dialogue = null # v7: pending_recognitions (#431, D-059/D-060) if snapshot.has("pending_recognitions") and snapshot.pending_recognitions is Array: GameState.pending_recognitions = snapshot.pending_recognitions else: GameState.pending_recognitions = [] # v9: conversation_events (#535, D-078) if snapshot.has("conversation_events") and snapshot.conversation_events is Array: GameState.conversation_events = snapshot.conversation_events else: GameState.conversation_events = [] # v9: conversation_ended (#535, D-078) if snapshot.has("conversation_ended") and snapshot.conversation_ended is Array: GameState.conversation_ended = snapshot.conversation_ended else: GameState.conversation_ended = [] # v8: dialogue_response (#305, D-028) if snapshot.has("dialogue_response") and snapshot.dialogue_response is Dictionary: GameState.dialogue_response = snapshot.dialogue_response else: GameState.dialogue_response = null # v8: gauntlet mode (#496) if snapshot.has("gauntlet_mode") and snapshot.gauntlet_mode == true: GameState.gauntlet_mode = true else: GameState.gauntlet_mode = false if snapshot.has("room_id") and snapshot.room_id is String: GameState.room_id = snapshot.room_id else: GameState.room_id = null # OQ-07 (#522): insert_active if snapshot.has("insert_active") and snapshot.insert_active is bool: GameState.insert_active = snapshot.insert_active else: GameState.insert_active = true # #507: rng_seed if snapshot.has("rng_seed"): GameState.rng_seed = snapshot.rng_seed else: GameState.rng_seed = null # D-018: Sound events — partition by range_category. if snapshot.has("sound_events") and snapshot.sound_events is Array: GameState.medium_sound_events = [] GameState.close_sound_events = [] for se in snapshot.sound_events: if not se is Dictionary: continue var rc: String = se.get("range_category", "") if rc == "Medium": GameState.medium_sound_events.append(se) elif rc == "Close": GameState.close_sound_events.append(se) else: GameState.medium_sound_events = [] GameState.close_sound_events = [] # v10: discovered_pois (#151, D-013) if snapshot.has("discovered_pois") and snapshot.discovered_pois is Array: GameState.discovered_pois = snapshot.discovered_pois elif snapshot.has("poi_list") and snapshot.poi_list is Array: GameState.discovered_pois = snapshot.poi_list # v14: examine_result (#174, #242) if snapshot.has("examine_result") and snapshot.examine_result is Dictionary: GameState.current_examine_result = snapshot.examine_result else: GameState.current_examine_result = null # v15: save_result (#554, D-085) if snapshot.has("save_result") and snapshot.save_result is Dictionary: GameState.save_result = snapshot.save_result else: GameState.save_result = null # v18: debug_response (#580) if snapshot.has("debug_response") and snapshot.debug_response is Dictionary: GameState.debug_response = snapshot.debug_response else: GameState.debug_response = null # v20: settings_response (#627, D-138) if snapshot.has("settings_response") and snapshot.settings_response is Dictionary: GameState.settings_response = snapshot.settings_response var sr: Dictionary = snapshot.settings_response if sr.get("kind") == "full": var sr_settings: Variant = sr.get("settings") if sr_settings is Array: for entry in sr_settings: if not entry is Dictionary: continue if entry.get("key") == "ai_dialogue.enabled": var val: Variant = entry.get("value") if val != null: GameState.ai_enhanced_dialogue_enabled = _extract_bool_setting( "ai_dialogue.enabled", val ) else: GameState.settings_response = null # v21: economy_snapshot (#824, D-181) if snapshot.has("economy_snapshot") and snapshot.economy_snapshot is Dictionary: GameState.economy_snapshot = snapshot.economy_snapshot else: GameState.economy_snapshot = null # #718: character_visual_descriptor — restored from server snapshot on save/load. if ( snapshot.has("character_visual_descriptor") and snapshot.character_visual_descriptor is Dictionary ): var CVD := load("res://scripts/rendering/character_visual_descriptor.gd") if CVD != null: var restored = CVD.from_dict(snapshot.character_visual_descriptor) if restored != null: GameState.character_visual_descriptor = restored # v14: player_knowledge (#264, D-041) if snapshot.has("player_knowledge") and snapshot.player_knowledge is Dictionary: GameState.player_knowledge = snapshot.player_knowledge # D-020/D-073 (#529): Server-authoritative zone_id for zone ambient crossfade. if snapshot.has("zone_id") and snapshot.zone_id is String: GameState.current_zone_id = snapshot.zone_id else: # DEPRECATED fallback — client-side tile lookup. Remove when server sends top-level "zone_id". var tile_by_coord: Dictionary = {} for vtile in GameState.visible_tiles: if vtile is Dictionary and vtile.has("x") and vtile.has("y"): tile_by_coord[Vector2i(vtile.x, vtile.y)] = vtile var player_pos_key := Vector2i( int(GameState.player_position.x), int(GameState.player_position.y) ) var player_tile = tile_by_coord.get(player_pos_key, null) GameState.current_zone_id = player_tile.get("zone_id", "") if player_tile else "" # v2: visible_tiles with visibility sectors if ( snapshot.has("visible_tiles") and snapshot.visible_tiles is Array and snapshot.visible_tiles.size() > 0 ): GameState.visibility_sectors.clear() var has_explicit_positions := snapshot.has("visible_positions") if not has_explicit_positions: GameState.visible_positions.clear() GameState.boundary_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) var vis_sector: String = vtile.get("visibility", "") if vtile.has("visibility"): GameState.visibility_sectors[pos] = vis_sector if vis_sector == "BoundaryWall": GameState.boundary_positions[pos] = true elif not has_explicit_positions: GameState.visible_positions[pos] = true ## Extract a bool from a tagged-union {"Bool": true} or plain bool value. static func _extract_bool_setting(key: String, val: Variant) -> bool: if val is bool: return val if val is Dictionary and val.has("Bool"): return bool(val["Bool"]) push_warning("GameState: unexpected type for setting '%s': %s" % [key, str(val)]) return false