Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
278 lines
10 KiB
GDScript
278 lines
10 KiB
GDScript
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
|
|
|
|
# #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
|