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>
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
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
|
||||
Reference in New Issue
Block a user