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:
@@ -19,7 +19,8 @@ var player_position: Vector2 = Vector2.ZERO
|
|||||||
var visible_entities: Array = []
|
var visible_entities: Array = []
|
||||||
var visible_tiles: Array = []
|
var visible_tiles: Array = []
|
||||||
var visible_positions: Dictionary = {} # Vector2i -> true, for fast fog lookups (normal LOS tiles)
|
var visible_positions: Dictionary = {} # Vector2i -> true, for fast fog lookups (normal LOS tiles)
|
||||||
var boundary_positions: Dictionary = {} # Vector2i -> true, BoundaryWall margin tiles (#585) — visible in fog but not explored
|
var boundary_positions: Dictionary = {} # Vector2i -> true, BoundaryWall margin tiles (#585)
|
||||||
|
# — visible in fog but not explored
|
||||||
|
|
||||||
# v2 fields (D-015, D-031)
|
# v2 fields (D-015, D-031)
|
||||||
var game_time: Dictionary = {} # {day, time_of_day, day_phase, tick_rate} or empty
|
var game_time: Dictionary = {} # {day, time_of_day, day_phase, tick_rate} or empty
|
||||||
@@ -50,7 +51,8 @@ var player_stance: String = "Walk" # Sprint/Walk/Careful/Crouch
|
|||||||
var player_inventory: Array = [] # [{item_id, name, slot}]
|
var player_inventory: Array = [] # [{item_id, name, slot}]
|
||||||
|
|
||||||
# v7 fields (#435, D-061/D-062)
|
# 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
|
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).
|
# D-064: true while dialogue box is visible or fading out (300ms).
|
||||||
# InputMapper suppresses movement when this is true.
|
# InputMapper suppresses movement when this is true.
|
||||||
@@ -152,8 +154,7 @@ var close_sound_events: Array = []
|
|||||||
# Fallback: client-side accumulation (deprecated, remove when server populates field).
|
# Fallback: client-side accumulation (deprecated, remove when server populates field).
|
||||||
# ListeningFocus boost activates at 30+ ticks (main.gd manages the dip).
|
# ListeningFocus boost activates at 30+ ticks (main.gd manages the dip).
|
||||||
var stationary_ticks: int = 0
|
var stationary_ticks: int = 0
|
||||||
# DEPRECATED: Only used by client-side accumulation fallback. Remove with fallback.
|
# DEPRECATED: _prev_player_position moved to SnapshotHandler (client-side accumulation fallback).
|
||||||
var _prev_player_position: Vector2 = Vector2(-1e9, -1e9) # sentinel: no previous position
|
|
||||||
|
|
||||||
# D-073 (#529): Server-authoritative zone_id from the player's current tile.
|
# D-073 (#529): Server-authoritative zone_id from the player's current tile.
|
||||||
# D-020: Read directly from snapshot "zone_id" field.
|
# D-020: Read directly from snapshot "zone_id" field.
|
||||||
@@ -162,276 +163,7 @@ var _prev_player_position: Vector2 = Vector2(-1e9, -1e9) # sentinel: no previou
|
|||||||
var current_zone_id: String = ""
|
var current_zone_id: String = ""
|
||||||
|
|
||||||
func apply_snapshot(snapshot: Dictionary) -> void:
|
func apply_snapshot(snapshot: Dictionary) -> void:
|
||||||
current_snapshot = snapshot
|
# Autoload parse-order: class_name types are not registered when autoloads compile.
|
||||||
|
# load() returns the cached resource after the first call — essentially free per-tick.
|
||||||
if snapshot.has("tick"):
|
var SH := load("res://scripts/snapshot_handler.gd")
|
||||||
current_tick = snapshot.tick
|
SH.apply(snapshot)
|
||||||
|
|
||||||
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()])
|
|
||||||
|
|
||||||
# D-020/D-071 (#530): Server-authoritative stationary_ticks for ListeningFocus boost.
|
|
||||||
# Prefer server-sent value; fall back to client-side accumulation until server populates.
|
|
||||||
if snapshot.has("stationary_ticks") and snapshot.stationary_ticks is int:
|
|
||||||
# D-020: direct field assignment from server-authoritative snapshot.
|
|
||||||
stationary_ticks = snapshot.stationary_ticks
|
|
||||||
else:
|
|
||||||
# DEPRECATED fallback — client-side accumulation. Remove when server sends
|
|
||||||
# "stationary_ticks" in ObserverSnapshot (D-020 violation: derives behavior-
|
|
||||||
# driving state on the client). Server tracks this in ListeningFocus component.
|
|
||||||
if player_position == _prev_player_position:
|
|
||||||
stationary_ticks += 1
|
|
||||||
else:
|
|
||||||
stationary_ticks = 0
|
|
||||||
_prev_player_position = player_position
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# #122: lattice_profile — character insert capability level for monologue colour
|
|
||||||
if snapshot.has("lattice_profile") and snapshot.lattice_profile is String:
|
|
||||||
lattice_profile = snapshot.lattice_profile
|
|
||||||
|
|
||||||
# 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 = []
|
|
||||||
|
|
||||||
# v9: conversation_events (#535, D-078) — overheard NPC-to-NPC lines
|
|
||||||
if snapshot.has("conversation_events") and snapshot.conversation_events is Array:
|
|
||||||
conversation_events = snapshot.conversation_events
|
|
||||||
else:
|
|
||||||
conversation_events = []
|
|
||||||
|
|
||||||
# v9: conversation_ended (#535, D-078) — pairs whose conversation ended
|
|
||||||
if snapshot.has("conversation_ended") and snapshot.conversation_ended is Array:
|
|
||||||
conversation_ended = snapshot.conversation_ended
|
|
||||||
else:
|
|
||||||
conversation_ended = []
|
|
||||||
|
|
||||||
# v8: dialogue_response (#305, D-028) — NPC follow-up after player choice
|
|
||||||
if snapshot.has("dialogue_response") and snapshot.dialogue_response is Dictionary:
|
|
||||||
dialogue_response = snapshot.dialogue_response
|
|
||||||
else:
|
|
||||||
dialogue_response = null
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# OQ-07 (#522): insert_active — defaults true (v0.1 always has insert).
|
|
||||||
# Server may send false for characters without an insert in future sprints.
|
|
||||||
if snapshot.has("insert_active") and snapshot.insert_active is bool:
|
|
||||||
insert_active = snapshot.insert_active
|
|
||||||
else:
|
|
||||||
insert_active = true
|
|
||||||
|
|
||||||
# #507: rng_seed — server sends current RNG seed for replay determinism.
|
|
||||||
# Field: "rng_seed" (u64 as integer). Null if server does not include it.
|
|
||||||
if snapshot.has("rng_seed"):
|
|
||||||
rng_seed = snapshot.rng_seed
|
|
||||||
else:
|
|
||||||
rng_seed = null
|
|
||||||
|
|
||||||
# D-018: Sound events from server — partition by range_category.
|
|
||||||
# #126: Medium → fog-edge directional indicators.
|
|
||||||
# #125: Close → positional 2D audio via AudioManager.
|
|
||||||
if snapshot.has("sound_events") and snapshot.sound_events is Array:
|
|
||||||
medium_sound_events = []
|
|
||||||
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":
|
|
||||||
medium_sound_events.append(se)
|
|
||||||
elif rc == "Close":
|
|
||||||
close_sound_events.append(se)
|
|
||||||
else:
|
|
||||||
medium_sound_events = []
|
|
||||||
close_sound_events = []
|
|
||||||
|
|
||||||
# v10: discovered_pois (#151, D-013) — server sends POIs discovered by the player.
|
|
||||||
# Accepts "discovered_pois" or "poi_list" key — both map to the same client field.
|
|
||||||
# Only update if the field is present — absence means "no change since last tick".
|
|
||||||
if snapshot.has("discovered_pois") and snapshot.discovered_pois is Array:
|
|
||||||
discovered_pois = snapshot.discovered_pois
|
|
||||||
elif snapshot.has("poi_list") and snapshot.poi_list is Array:
|
|
||||||
discovered_pois = snapshot.poi_list
|
|
||||||
|
|
||||||
# v14: examine_result (#174, #242) — character-filtered observation from Examine verb.
|
|
||||||
if snapshot.has("examine_result") and snapshot.examine_result is Dictionary:
|
|
||||||
current_examine_result = snapshot.examine_result
|
|
||||||
else:
|
|
||||||
current_examine_result = null
|
|
||||||
|
|
||||||
# v15: save_result (#554, D-085) — one-shot save/load confirmation from server.
|
|
||||||
if snapshot.has("save_result") and snapshot.save_result is Dictionary:
|
|
||||||
save_result = snapshot.save_result
|
|
||||||
else:
|
|
||||||
save_result = null
|
|
||||||
|
|
||||||
# v18: debug_response (#580) — debug console command result.
|
|
||||||
if snapshot.has("debug_response") and snapshot.debug_response is Dictionary:
|
|
||||||
debug_response = snapshot.debug_response
|
|
||||||
else:
|
|
||||||
debug_response = null
|
|
||||||
|
|
||||||
# v20: settings_response (#627, D-138) — one-shot settings ack/dump from server.
|
|
||||||
# "full" kind → iterate settings array and hydrate matching fields.
|
|
||||||
if snapshot.has("settings_response") and snapshot.settings_response is Dictionary:
|
|
||||||
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:
|
|
||||||
ai_enhanced_dialogue_enabled = _extract_bool_setting("ai_dialogue.enabled", val)
|
|
||||||
else:
|
|
||||||
settings_response = null
|
|
||||||
|
|
||||||
# #718: character_visual_descriptor — restored from server snapshot on save/load.
|
|
||||||
# Server persists the descriptor and includes it in ObserverSnapshot after load.
|
|
||||||
# Only update when field is present (null means no change).
|
|
||||||
if snapshot.has("character_visual_descriptor") and snapshot.character_visual_descriptor is Dictionary:
|
|
||||||
# load() returns a cached script — safe to call per-tick once the resource is in cache.
|
|
||||||
# Cannot use CharacterVisualDescriptor directly: autoloads compile before global class_names
|
|
||||||
# are registered, causing a parse-time "not declared" error.
|
|
||||||
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:
|
|
||||||
character_visual_descriptor = restored
|
|
||||||
|
|
||||||
# v14: player_knowledge (#264, D-041) — partial KG dump for journal panel.
|
|
||||||
# Only update when field is present (null means no change, server sends when KG changes).
|
|
||||||
if snapshot.has("player_knowledge") and snapshot.player_knowledge is Dictionary:
|
|
||||||
player_knowledge = snapshot.player_knowledge
|
|
||||||
|
|
||||||
# D-020/D-073 (#529): Server-authoritative zone_id for zone ambient crossfade.
|
|
||||||
# Prefer server-sent top-level value; fall back to client-side tile lookup until
|
|
||||||
# server populates top-level "zone_id" in ObserverSnapshot.
|
|
||||||
if snapshot.has("zone_id") and snapshot.zone_id is String:
|
|
||||||
# D-020: direct field assignment from server-authoritative snapshot.
|
|
||||||
current_zone_id = snapshot.zone_id
|
|
||||||
else:
|
|
||||||
# DEPRECATED fallback — client-side tile lookup. Remove when server sends
|
|
||||||
# top-level "zone_id" in ObserverSnapshot (D-020 violation: derives zone
|
|
||||||
# identity on the client via tile iteration). Server sends zone_id per
|
|
||||||
# VisibleTile but not as a top-level snapshot field.
|
|
||||||
var _tile_by_coord: Dictionary = {}
|
|
||||||
for vtile in 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(player_position.x), int(player_position.y))
|
|
||||||
var player_tile = _tile_by_coord.get(player_pos_key, null)
|
|
||||||
current_zone_id = player_tile.get("zone_id", "") if player_tile else ""
|
|
||||||
|
|
||||||
# v2: visible_tiles with visibility sectors
|
|
||||||
# Derives visible_positions when not explicitly provided (real server mode).
|
|
||||||
# #585: BoundaryWall tiles go to boundary_positions — rendered in fog but not marked explored.
|
|
||||||
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()
|
|
||||||
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"):
|
|
||||||
visibility_sectors[pos] = vis_sector
|
|
||||||
# #585: BoundaryWall tiles are margin tiles visible through fog but not persistently
|
|
||||||
# explored — they don't update the player's exploration memory when they leave LOS.
|
|
||||||
if vis_sector == "BoundaryWall":
|
|
||||||
boundary_positions[pos] = true
|
|
||||||
elif not has_explicit_positions:
|
|
||||||
visible_positions[pos] = true
|
|
||||||
|
|
||||||
|
|
||||||
# -- Helpers ------------------------------------------------------------------
|
|
||||||
|
|
||||||
## Extract a bool from a tagged-union {"Bool": true} or plain bool value.
|
|
||||||
## Handles both serde encoding styles; emits push_warning on unrecognised format.
|
|
||||||
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
|
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
class_name DialogueCoordinator
|
||||||
|
## Dialogue snapshot consumers and signal handlers extracted from main.gd (#775).
|
||||||
|
##
|
||||||
|
## Owns NPC identity tracking state (_last_dialogue_npc_id/name) shared between
|
||||||
|
## consuming dialogue snapshots and handling dialogue_box signals.
|
||||||
|
## Registered with SnapshotEventRouter; reads from GameState directly.
|
||||||
|
|
||||||
|
var dialogue_box: Node = null
|
||||||
|
var monologue_display: Node = null
|
||||||
|
var journal_panel: Node = null
|
||||||
|
|
||||||
|
# Shared mutable reference — GDScript arrays are reference types.
|
||||||
|
# main.gd and this coordinator both append to the same array.
|
||||||
|
var _pending_record_inputs: Array = []
|
||||||
|
|
||||||
|
var _last_dialogue_npc_id: int = -1 # D-064: NPC entity_id for WalkAway input
|
||||||
|
var _last_dialogue_npc_name: String = "" # #535: NPC name for dialogue_response attribution
|
||||||
|
var _last_dialogue_tick: int = -1
|
||||||
|
var _last_confrontation_tick: int = -1
|
||||||
|
|
||||||
|
|
||||||
|
func init(refs: Dictionary, pending_record_inputs: Array) -> DialogueCoordinator:
|
||||||
|
dialogue_box = refs.get("dialogue_box")
|
||||||
|
monologue_display = refs.get("monologue_display")
|
||||||
|
journal_panel = refs.get("journal_panel")
|
||||||
|
_pending_record_inputs = pending_record_inputs
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
## Connect dialogue_box signals. Call from main.gd._ready() after init().
|
||||||
|
func connect_signals() -> void:
|
||||||
|
if not dialogue_box:
|
||||||
|
return
|
||||||
|
dialogue_box.option_selected.connect(on_dialogue_option_selected)
|
||||||
|
dialogue_box.dialogue_dismissed.connect(on_dialogue_dismissed)
|
||||||
|
dialogue_box.confrontation_monologue.connect(on_confrontation_monologue)
|
||||||
|
dialogue_box.pause_requested.connect(on_dialogue_pause_requested)
|
||||||
|
dialogue_box.unpause_requested.connect(on_dialogue_unpause_requested)
|
||||||
|
# D-020 (#558): Decoupled signals — coordinator routes state changes.
|
||||||
|
dialogue_box.dialogue_state_changed.connect(on_dialogue_state_changed)
|
||||||
|
dialogue_box.audio_dip_requested.connect(on_audio_dip_requested)
|
||||||
|
dialogue_box.audio_dip_cleared.connect(on_audio_dip_cleared)
|
||||||
|
|
||||||
|
|
||||||
|
# -- Snapshot consumers (registered with SnapshotEventRouter) -----------------
|
||||||
|
|
||||||
|
# Consume-once per tick with ID tracking: show dialogue, then clear.
|
||||||
|
func consume_dialogue() -> void:
|
||||||
|
if GameState.current_dialogue == null or not dialogue_box:
|
||||||
|
return
|
||||||
|
if GameState.current_tick == _last_dialogue_tick:
|
||||||
|
return
|
||||||
|
if dialogue_box.is_dialogue_active():
|
||||||
|
GameState.current_dialogue = null
|
||||||
|
return
|
||||||
|
_last_dialogue_tick = GameState.current_tick
|
||||||
|
# #264: Close journal when dialogue opens
|
||||||
|
if journal_panel and journal_panel.has_method("close"):
|
||||||
|
journal_panel.close()
|
||||||
|
var dlg: Dictionary = GameState.current_dialogue
|
||||||
|
_last_dialogue_npc_id = dlg.get("npc_entity_id", -1)
|
||||||
|
_last_dialogue_npc_name = dlg.get("npc_name", "")
|
||||||
|
dialogue_box.show_dialogue(
|
||||||
|
dlg.get("npc_name", ""),
|
||||||
|
dlg.get("speech", ""),
|
||||||
|
dlg.get("options", []),
|
||||||
|
_last_dialogue_npc_id
|
||||||
|
)
|
||||||
|
GameState.current_dialogue = null
|
||||||
|
|
||||||
|
|
||||||
|
# #535: Consume overheard NPC-NPC conversation events (D-078).
|
||||||
|
func consume_conversation_events() -> void:
|
||||||
|
if not dialogue_box:
|
||||||
|
return
|
||||||
|
for event in GameState.conversation_events:
|
||||||
|
dialogue_box.append_conversation_event(event)
|
||||||
|
GameState.conversation_events = []
|
||||||
|
|
||||||
|
|
||||||
|
# #535: Handle conversation_ended events.
|
||||||
|
func consume_conversation_ended() -> void:
|
||||||
|
if not dialogue_box:
|
||||||
|
return
|
||||||
|
for event in GameState.conversation_ended:
|
||||||
|
dialogue_box.on_conversation_ended(event)
|
||||||
|
GameState.conversation_ended = []
|
||||||
|
|
||||||
|
|
||||||
|
# #535: Consume dialogue_response — NPC follow-up line after player picks an option.
|
||||||
|
func consume_dialogue_response() -> void:
|
||||||
|
if GameState.dialogue_response == null or not dialogue_box:
|
||||||
|
return
|
||||||
|
var dr: Dictionary = GameState.dialogue_response
|
||||||
|
var speaker_entity_id: int = dr.get("speaker_entity_id", _last_dialogue_npc_id)
|
||||||
|
var speaker_color_index: int = dr.get("speaker_color_index", -1)
|
||||||
|
var speaker_name: String = dr.get("speaker_name", _last_dialogue_npc_name)
|
||||||
|
dialogue_box.update_entity_display(speaker_entity_id, speaker_name, speaker_color_index)
|
||||||
|
dialogue_box.append_dialogue_response(speaker_name, dr.get("text", ""), speaker_entity_id)
|
||||||
|
GameState.dialogue_response = null
|
||||||
|
|
||||||
|
|
||||||
|
# -- Signal handlers ----------------------------------------------------------
|
||||||
|
|
||||||
|
# D-061: Handle dialogue option selection -> send to server
|
||||||
|
func on_dialogue_option_selected(response_id: String, _text: String) -> void:
|
||||||
|
SimBridge.send_input({
|
||||||
|
"action": InputMapper.Action.INTERACT,
|
||||||
|
"timestamp_msec": Time.get_ticks_msec(),
|
||||||
|
"action_data": {
|
||||||
|
"target_entity_id": null,
|
||||||
|
"verb": "DialogueResponse",
|
||||||
|
"response_id": response_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# D-063: Confrontation beat monologue -> show on monologue display (layer 7)
|
||||||
|
func on_confrontation_monologue(text: String, duration: float) -> void:
|
||||||
|
if not monologue_display:
|
||||||
|
return
|
||||||
|
if GameState.current_tick == _last_confrontation_tick:
|
||||||
|
return
|
||||||
|
_last_confrontation_tick = GameState.current_tick
|
||||||
|
monologue_display.show_monologue(text, duration, 3, true)
|
||||||
|
|
||||||
|
|
||||||
|
# D-061: Auto-pause on dialogue open
|
||||||
|
func on_dialogue_pause_requested() -> void:
|
||||||
|
var input := {"action": InputMapper.Action.PAUSE, "timestamp_msec": Time.get_ticks_msec()}
|
||||||
|
SimBridge.send_input(input)
|
||||||
|
_pending_record_inputs.append(input)
|
||||||
|
|
||||||
|
|
||||||
|
# D-061: Auto-unpause on dialogue close
|
||||||
|
func on_dialogue_unpause_requested() -> void:
|
||||||
|
var input := {"action": InputMapper.Action.UNPAUSE, "timestamp_msec": Time.get_ticks_msec()}
|
||||||
|
SimBridge.send_input(input)
|
||||||
|
_pending_record_inputs.append(input)
|
||||||
|
|
||||||
|
|
||||||
|
# D-064: Handle walk-away -> send WalkAway{npc_id} to server
|
||||||
|
func on_dialogue_dismissed() -> void:
|
||||||
|
SimBridge.send_input({
|
||||||
|
"action": InputMapper.Action.INTERACT,
|
||||||
|
"timestamp_msec": Time.get_ticks_msec(),
|
||||||
|
"action_data": {
|
||||||
|
"target_entity_id": _last_dialogue_npc_id if _last_dialogue_npc_id >= 0 else null,
|
||||||
|
"verb": "WalkAway",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# D-020 (#558): Coordinator handles dialogue state changes.
|
||||||
|
func on_dialogue_state_changed(active: bool) -> void:
|
||||||
|
GameState.dialogue_active = active
|
||||||
|
|
||||||
|
|
||||||
|
# D-020 (#558): Route audio dip requests.
|
||||||
|
func on_audio_dip_requested(profile: String) -> void:
|
||||||
|
AudioManager.apply_dip(profile)
|
||||||
|
|
||||||
|
|
||||||
|
# D-020 (#558): Route audio dip clear.
|
||||||
|
func on_audio_dip_cleared() -> void:
|
||||||
|
AudioManager.clear_dip()
|
||||||
+61
-383
@@ -1,5 +1,15 @@
|
|||||||
extends Node2D
|
extends Node2D
|
||||||
|
|
||||||
|
const TELEPORT_DISTANCE_THRESHOLD: float = 5.0
|
||||||
|
|
||||||
|
var _camera_anchored: bool = false
|
||||||
|
var _flash_rect: ColorRect = null # #502/#501: ephemeral screen flash overlay
|
||||||
|
var _teleport_in_progress: bool = false # #501/#117: forces camera snap on next frame
|
||||||
|
var _pending_record_inputs: Array = [] # #507: accumulates server-bound inputs across frames
|
||||||
|
var _router: SnapshotEventRouter # #559: callable-based snapshot dispatch
|
||||||
|
var _consumers: SnapshotConsumers # #775: non-dialogue snapshot consumers
|
||||||
|
var _dialogue: DialogueCoordinator # #775: dialogue consumers + signal handlers
|
||||||
|
|
||||||
@onready var world_renderer = $World
|
@onready var world_renderer = $World
|
||||||
@onready var fog_entities = $World/FogEntities # D-059/D-060: cognitive delay fog visualization
|
@onready var fog_entities = $World/FogEntities # D-059/D-060: cognitive delay fog visualization
|
||||||
@onready var camera = $Camera2D
|
@onready var camera = $Camera2D
|
||||||
@@ -26,37 +36,17 @@ extends Node2D
|
|||||||
@onready var news_ticker = $UILayer/NewsTicker # #592: scrolling headline bar (D-049 z-7)
|
@onready var news_ticker = $UILayer/NewsTicker # #592: scrolling headline bar (D-049 z-7)
|
||||||
@onready var star_map = $UILayer/HUD/StarMap # #674: star map insert module (hop-ring view)
|
@onready var star_map = $UILayer/HUD/StarMap # #674: star map insert module (hop-ring view)
|
||||||
|
|
||||||
var _last_dialogue_npc_id: int = -1 # D-064: NPC entity_id for WalkAway input
|
|
||||||
var _last_dialogue_npc_name: String = "" # #535: NPC name for dialogue_response attribution
|
|
||||||
var _camera_anchored: bool = false
|
|
||||||
var _last_monologue_tick: int = -1 # Prevent re-consuming monologue when same tick polled twice
|
|
||||||
var _last_dialogue_tick: int = -1
|
|
||||||
var _last_confrontation_tick: int = -1 # Deduplicate confrontation_monologue signals within same tick
|
|
||||||
var _known_recognition_ids: Dictionary = {} # D-067: entity_ids that have already chimed
|
|
||||||
var _known_triangle_ids: Dictionary = {} # #590: triangle_ids that have already fired the activation chime
|
|
||||||
var _flash_rect: ColorRect = null # #502/#501: ephemeral screen flash overlay (shared: teleport preempts amber)
|
|
||||||
var _teleport_in_progress: bool = false # #501/#117: forces camera snap (not lerp) on next _process frame
|
|
||||||
var _pending_record_inputs: Array = [] # #507: accumulates server-bound inputs across frames; flushed into record_tick() on snapshot arrival
|
|
||||||
var _current_zone: String = "" # D-073 (#529): zone tracking for ambient crossfades
|
|
||||||
var _router: SnapshotEventRouter # #559: callable-based snapshot dispatch
|
|
||||||
|
|
||||||
const LISTENING_FOCUS_TICKS: int = 30 # D-071: stationary ticks before ListeningFocus boost activates
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
print("The Settled Reach — client initialized")
|
print("The Settled Reach — client initialized")
|
||||||
|
|
||||||
# #117: Manual lerp approach — disable Godot's built-in Camera2D smoothing.
|
# #117: Manual lerp approach — disable Godot's built-in Camera2D smoothing.
|
||||||
# We lerp camera.global_position directly in _process() using CAMERA_SMOOTHING_SPEED,
|
|
||||||
# matching entity_renderer.gd's exponential smoothing pattern. Built-in smoothing
|
|
||||||
# would conflict because we'd be setting global_position to the target every frame.
|
|
||||||
camera.position_smoothing_enabled = false
|
camera.position_smoothing_enabled = false
|
||||||
|
|
||||||
# Connect to simulation (test mode sets CONNECTED immediately)
|
# Connect to simulation (test mode sets CONNECTED immediately)
|
||||||
SimBridge.connect_to_sim()
|
SimBridge.connect_to_sim()
|
||||||
|
|
||||||
# #257: If returning from main menu "Load Game" selection, defer dispatch until connected.
|
# #257: Deferred load dispatch
|
||||||
# In test mode, connect_to_sim() sets CONNECTED synchronously — dispatch fires immediately.
|
|
||||||
# In live mode, state is CONNECTING — signal handler dispatches once connected.
|
|
||||||
if not GameState.pending_load_path.is_empty():
|
if not GameState.pending_load_path.is_empty():
|
||||||
if loading_screen:
|
if loading_screen:
|
||||||
loading_screen.show_loading()
|
loading_screen.show_loading()
|
||||||
@@ -66,46 +56,52 @@ func _ready() -> void:
|
|||||||
SimBridge.connection_state_changed.connect(_on_sim_connected_for_load)
|
SimBridge.connection_state_changed.connect(_on_sim_connected_for_load)
|
||||||
|
|
||||||
# Camera anchor: snap to player position before the first frame renders.
|
# Camera anchor: snap to player position before the first frame renders.
|
||||||
# In test mode poll_snapshot() returns synchronously — position is set
|
|
||||||
# immediately. In live mode the snapshot isn't available yet — _process
|
|
||||||
# handles it via the lerp block in _process().
|
|
||||||
var first_snapshot: Variant = SimBridge.poll_snapshot()
|
var first_snapshot: Variant = SimBridge.poll_snapshot()
|
||||||
if first_snapshot != null:
|
if first_snapshot != null:
|
||||||
GameState.apply_snapshot(first_snapshot)
|
GameState.apply_snapshot(first_snapshot)
|
||||||
camera.global_position = GameState.player_position * Constants.TILE_SIZE
|
camera.global_position = GameState.player_position * Constants.TILE_SIZE
|
||||||
_camera_anchored = true
|
_camera_anchored = true
|
||||||
|
|
||||||
# D-061: Connect dialogue box signals
|
# #775: Initialize extracted components
|
||||||
if dialogue_box:
|
_consumers = SnapshotConsumers.new().init({
|
||||||
dialogue_box.option_selected.connect(_on_dialogue_option_selected)
|
"monologue_display": monologue_display,
|
||||||
dialogue_box.dialogue_dismissed.connect(_on_dialogue_dismissed)
|
"dialogue_box": dialogue_box,
|
||||||
dialogue_box.confrontation_monologue.connect(_on_confrontation_monologue)
|
"examine_display": examine_display,
|
||||||
dialogue_box.pause_requested.connect(_on_dialogue_pause_requested)
|
"loading_screen": loading_screen,
|
||||||
dialogue_box.unpause_requested.connect(_on_dialogue_unpause_requested)
|
"debug_console": debug_console,
|
||||||
# D-020 (#558): Decoupled signals — coordinator routes state changes.
|
"cursor_renderer": cursor_renderer,
|
||||||
dialogue_box.dialogue_state_changed.connect(_on_dialogue_state_changed)
|
"interaction_list": interaction_list,
|
||||||
dialogue_box.audio_dip_requested.connect(_on_audio_dip_requested)
|
"interaction_prompt": interaction_prompt,
|
||||||
dialogue_box.audio_dip_cleared.connect(_on_audio_dip_cleared)
|
"minimap": minimap,
|
||||||
|
"star_map": star_map,
|
||||||
|
}, _screen_flash)
|
||||||
|
|
||||||
|
_dialogue = DialogueCoordinator.new().init({
|
||||||
|
"dialogue_box": dialogue_box,
|
||||||
|
"monologue_display": monologue_display,
|
||||||
|
"journal_panel": journal_panel,
|
||||||
|
}, _pending_record_inputs)
|
||||||
|
_dialogue.connect_signals()
|
||||||
|
|
||||||
# #496: Print gauntlet session summary on disconnect
|
# #496: Print gauntlet session summary on disconnect
|
||||||
if gauntlet_hud:
|
if gauntlet_hud:
|
||||||
SimBridge.connection_state_changed.connect(_on_connection_state_changed)
|
SimBridge.connection_state_changed.connect(_on_connection_state_changed)
|
||||||
|
|
||||||
# #559: Register snapshot dispatch handlers — replaces inline dispatch in _process().
|
# #559: Register snapshot dispatch handlers
|
||||||
_router = SnapshotEventRouter.new()
|
_router = SnapshotEventRouter.new()
|
||||||
# Always-run: child nodes that update from GameState on every snapshot tick.
|
# Always-run: child nodes that update from GameState on every snapshot tick.
|
||||||
if world_renderer:
|
if world_renderer:
|
||||||
_router.register_always(world_renderer.update_from_state)
|
_router.register_always(world_renderer.update_from_state)
|
||||||
_router.register_always(_propagate_insert_state)
|
_router.register_always(_consumers.propagate_insert_state)
|
||||||
_router.register_always(_update_interaction_list)
|
_router.register_always(_consumers.update_interaction_list)
|
||||||
if inventory_grid:
|
if inventory_grid:
|
||||||
_router.register_always(inventory_grid.update_from_state)
|
_router.register_always(inventory_grid.update_from_state)
|
||||||
if stance_indicator:
|
if stance_indicator:
|
||||||
_router.register_always(stance_indicator.update_from_state)
|
_router.register_always(stance_indicator.update_from_state)
|
||||||
if fog_entities:
|
if fog_entities:
|
||||||
_router.register_always(fog_entities.update_from_state)
|
_router.register_always(fog_entities.update_from_state)
|
||||||
_router.register_always(_play_recognition_chimes)
|
_router.register_always(_consumers.play_recognition_chimes)
|
||||||
_router.register_always(_handle_triangle_crisis_events)
|
_router.register_always(_consumers.handle_triangle_crisis_events)
|
||||||
if gauntlet_hud:
|
if gauntlet_hud:
|
||||||
_router.register_always(gauntlet_hud.update_from_state)
|
_router.register_always(gauntlet_hud.update_from_state)
|
||||||
if checklist_overlay:
|
if checklist_overlay:
|
||||||
@@ -118,27 +114,27 @@ func _ready() -> void:
|
|||||||
_router.register_always(journal_panel.update_from_state)
|
_router.register_always(journal_panel.update_from_state)
|
||||||
if debug_overlay:
|
if debug_overlay:
|
||||||
_router.register_always(debug_overlay.update_from_state)
|
_router.register_always(debug_overlay.update_from_state)
|
||||||
_router.register_always(_play_close_sound_events)
|
_router.register_always(_consumers.play_close_sound_events)
|
||||||
_router.register_always(_update_zone)
|
_router.register_always(_consumers.update_zone)
|
||||||
_router.register_always(_update_listening_focus)
|
_router.register_always(_consumers.update_listening_focus)
|
||||||
_router.register_always(_consume_examine_result)
|
_router.register_always(_consumers.consume_examine_result)
|
||||||
# Keyed: consume methods guarded by specific snapshot fields.
|
# Keyed: consume methods guarded by specific snapshot fields.
|
||||||
_router.register("current_monologue", _consume_monologue)
|
_router.register("current_monologue", _consumers.consume_monologue)
|
||||||
_router.register("current_dialogue", _consume_dialogue)
|
_router.register("current_dialogue", _dialogue.consume_dialogue)
|
||||||
_router.register("conversation_events", _consume_conversation_events)
|
_router.register("conversation_events", _dialogue.consume_conversation_events)
|
||||||
_router.register("conversation_ended", _consume_conversation_ended)
|
_router.register("conversation_ended", _dialogue.consume_conversation_ended)
|
||||||
_router.register("dialogue_response", _consume_dialogue_response)
|
_router.register("dialogue_response", _dialogue.consume_dialogue_response)
|
||||||
_router.register("save_result", _consume_save_result)
|
_router.register("save_result", _consumers.consume_save_result)
|
||||||
_router.register("debug_response", _consume_debug_response)
|
_router.register("debug_response", _consumers.consume_debug_response)
|
||||||
|
|
||||||
# #581: Wire settings_dialog debug console toggle → debug_console.set_enabled
|
# #581: Wire settings_dialog debug console toggle
|
||||||
if settings_dialog and debug_console:
|
if settings_dialog and debug_console:
|
||||||
settings_dialog.debug_console_toggled.connect(debug_console.set_enabled)
|
settings_dialog.debug_console_toggled.connect(debug_console.set_enabled)
|
||||||
|
|
||||||
# #581 D-088: Wire debug console pause/unpause — sim must not advance during debug input
|
# #581 D-088: Wire debug console pause/unpause
|
||||||
if debug_console:
|
if debug_console:
|
||||||
debug_console.pause_requested.connect(_on_dialogue_pause_requested)
|
debug_console.pause_requested.connect(_dialogue.on_dialogue_pause_requested)
|
||||||
debug_console.unpause_requested.connect(_on_dialogue_unpause_requested)
|
debug_console.unpause_requested.connect(_dialogue.on_dialogue_unpause_requested)
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_key_input(event: InputEvent) -> void:
|
func _unhandled_key_input(event: InputEvent) -> void:
|
||||||
@@ -160,20 +156,15 @@ func _process(delta: float) -> void:
|
|||||||
_teleport_transition()
|
_teleport_transition()
|
||||||
|
|
||||||
# Late anchor: live mode — first snapshot arrives during _process.
|
# Late anchor: live mode — first snapshot arrives during _process.
|
||||||
# Smoothing is already OFF (disabled in _ready), so setting
|
|
||||||
# global_position takes effect immediately with no lerp.
|
|
||||||
if not _camera_anchored:
|
if not _camera_anchored:
|
||||||
camera.global_position = GameState.player_position * Constants.TILE_SIZE
|
camera.global_position = GameState.player_position * Constants.TILE_SIZE
|
||||||
_camera_anchored = true
|
_camera_anchored = true
|
||||||
|
|
||||||
# #559: Dispatch snapshot to registered handlers (router pattern).
|
# #559: Dispatch snapshot to registered handlers (router pattern).
|
||||||
# Always-run handlers update child nodes; keyed handlers fire for present fields.
|
|
||||||
_router.dispatch(snapshot)
|
_router.dispatch(snapshot)
|
||||||
|
|
||||||
# Track camera to player (D-015: locked, fixed-north).
|
# Track camera to player (D-015: locked, fixed-north).
|
||||||
# #117: Manual exponential smoothing — same pattern as EntityRenderer.LERP_SPEED.
|
# #117: Manual exponential smoothing.
|
||||||
# Teleport (flag set by _teleport_transition): snap immediately, resume lerp next frame.
|
|
||||||
# Init: camera already snapped in _ready() or late-anchor path above.
|
|
||||||
if _camera_anchored:
|
if _camera_anchored:
|
||||||
var target := GameState.player_position * Constants.TILE_SIZE
|
var target := GameState.player_position * Constants.TILE_SIZE
|
||||||
if _teleport_in_progress:
|
if _teleport_in_progress:
|
||||||
@@ -184,11 +175,9 @@ func _process(delta: float) -> void:
|
|||||||
camera.global_position = camera.global_position.lerp(target, weight)
|
camera.global_position = camera.global_position.lerp(target, weight)
|
||||||
|
|
||||||
# Send queued input to simulation
|
# Send queued input to simulation
|
||||||
# #507: Server-bound inputs are accumulated into _pending_record_inputs across frames.
|
|
||||||
# At 60fps/10tps, inputs on non-snapshot frames must not be lost from the ring buffer.
|
|
||||||
var inputs = InputMapper.flush_queue()
|
var inputs = InputMapper.flush_queue()
|
||||||
for input in inputs:
|
for input in inputs:
|
||||||
# #495: F12 WRONG button — client-only, trigger bug report capture
|
# #495: F12 WRONG button — client-only
|
||||||
if input.action == InputMapper.Action.BUG_REPORT:
|
if input.action == InputMapper.Action.BUG_REPORT:
|
||||||
if bug_report_dialog and not bug_report_dialog.is_active():
|
if bug_report_dialog and not bug_report_dialog.is_active():
|
||||||
bug_report_dialog.start_capture()
|
bug_report_dialog.start_capture()
|
||||||
@@ -197,7 +186,7 @@ func _process(delta: float) -> void:
|
|||||||
if input.action == InputMapper.Action.OPEN_JOURNAL:
|
if input.action == InputMapper.Action.OPEN_JOURNAL:
|
||||||
_toggle_journal()
|
_toggle_journal()
|
||||||
continue
|
continue
|
||||||
# #257: LOAD_GAME — send first, then show loading screen (avoids stuck overlay if send fails)
|
# #257: LOAD_GAME — send first, then show loading screen
|
||||||
if input.action == InputMapper.Action.LOAD_GAME:
|
if input.action == InputMapper.Action.LOAD_GAME:
|
||||||
var err := SimBridge.send_input(input)
|
var err := SimBridge.send_input(input)
|
||||||
_pending_record_inputs.append(input)
|
_pending_record_inputs.append(input)
|
||||||
@@ -224,7 +213,6 @@ func _process(delta: float) -> void:
|
|||||||
if target_id < 0 and interaction_prompt:
|
if target_id < 0 and interaction_prompt:
|
||||||
target_id = interaction_prompt.get_interaction_target()
|
target_id = interaction_prompt.get_interaction_target()
|
||||||
verb = interaction_prompt.get_selected_verb()
|
verb = interaction_prompt.get_selected_verb()
|
||||||
# Always send struct form for Interact (#415) — server expects named fields
|
|
||||||
if target_id >= 0:
|
if target_id >= 0:
|
||||||
input["action_data"] = {
|
input["action_data"] = {
|
||||||
"target_entity_id": target_id,
|
"target_entity_id": target_id,
|
||||||
@@ -238,9 +226,7 @@ func _process(delta: float) -> void:
|
|||||||
SimBridge.send_input(input)
|
SimBridge.send_input(input)
|
||||||
_pending_record_inputs.append(input)
|
_pending_record_inputs.append(input)
|
||||||
|
|
||||||
# #507: Record tick data to ring buffer — once per server tick (snapshot arrival).
|
# #507: Record tick data to ring buffer
|
||||||
# Flushes all inputs accumulated since the last snapshot (across multiple display frames),
|
|
||||||
# then clears the accumulator for the next tick.
|
|
||||||
if snapshot != null and bug_report_dialog and bug_report_dialog.has_method("record_tick"):
|
if snapshot != null and bug_report_dialog and bug_report_dialog.has_method("record_tick"):
|
||||||
bug_report_dialog.record_tick(
|
bug_report_dialog.record_tick(
|
||||||
GameState.current_tick,
|
GameState.current_tick,
|
||||||
@@ -250,296 +236,13 @@ func _process(delta: float) -> void:
|
|||||||
_pending_record_inputs.clear()
|
_pending_record_inputs.clear()
|
||||||
|
|
||||||
|
|
||||||
# OQ-07 (#522): Propagate insert state to all z-layer-6 display nodes.
|
|
||||||
# Cursor shape still fires (D-056 option a) — only verb labels suppressed.
|
|
||||||
func _propagate_insert_state() -> void:
|
|
||||||
var insert_state := GameState.insert_active
|
|
||||||
if cursor_renderer:
|
|
||||||
cursor_renderer.set_insert_active(insert_state)
|
|
||||||
if interaction_list:
|
|
||||||
interaction_list.set_insert_active(insert_state)
|
|
||||||
if interaction_prompt:
|
|
||||||
interaction_prompt.set_insert_active(insert_state)
|
|
||||||
if minimap:
|
|
||||||
minimap.set_insert_active(insert_state)
|
|
||||||
if star_map:
|
|
||||||
star_map.set_insert_active(insert_state)
|
|
||||||
|
|
||||||
|
|
||||||
# D-057: Update interaction list from game state.
|
|
||||||
# Suppress during dialogue — player is in conversation, verb list is noise.
|
|
||||||
func _update_interaction_list() -> void:
|
|
||||||
if not interaction_list:
|
|
||||||
return
|
|
||||||
if dialogue_box and dialogue_box.is_dialogue_active():
|
|
||||||
if interaction_list.is_showing():
|
|
||||||
interaction_list.hide_list()
|
|
||||||
else:
|
|
||||||
interaction_list.update_from_state()
|
|
||||||
|
|
||||||
|
|
||||||
# D-018 #125: Play close-range sound events — fired once per snapshot tick.
|
|
||||||
# Each event is passed to AudioManager.play_sound_event() for 2D positional playback
|
|
||||||
# on the WorldSFX bus. Events with no registered asset are silently skipped (D-038).
|
|
||||||
# Consume-once: events are cleared after processing so they don't replay if
|
|
||||||
# _process runs again before the next server tick (D-009 multiplayer-safe pattern).
|
|
||||||
func _play_close_sound_events() -> void:
|
|
||||||
for evt in GameState.close_sound_events:
|
|
||||||
if not evt is Dictionary or not evt.has("x") or not evt.has("y"):
|
|
||||||
continue
|
|
||||||
AudioManager.play_sound_event(
|
|
||||||
evt.get("event_type", ""),
|
|
||||||
Vector2(float(evt.x), float(evt.y))
|
|
||||||
)
|
|
||||||
GameState.close_sound_events = []
|
|
||||||
|
|
||||||
|
|
||||||
# D-067: Recognition chime — fires sfx_monologue_chime when a fog entity
|
|
||||||
# enters the cognitive delay recognition queue for the first time.
|
|
||||||
# "The chime marks the character's attention shifting" (D-067).
|
|
||||||
# IDs persist for the session — one chime per entity, no re-trigger on
|
|
||||||
# fog oscillation or server re-send. Cleared on room change (teleport).
|
|
||||||
func _play_recognition_chimes() -> void:
|
|
||||||
for rec in GameState.pending_recognitions:
|
|
||||||
if not rec is Dictionary or not rec.has("entity_id"):
|
|
||||||
continue
|
|
||||||
var eid: int = rec.entity_id
|
|
||||||
if not _known_recognition_ids.has(eid):
|
|
||||||
_known_recognition_ids[eid] = true
|
|
||||||
AudioManager.play(AudioManager.CHIME_RECOGNITION)
|
|
||||||
|
|
||||||
|
|
||||||
# #590 D-072/D-089: Triangle activation consumer — fires sfx_monologue_chime_urgent once
|
|
||||||
# per triangle_id. The tell_state on the activated NPC and subsequent proximity monologue
|
|
||||||
# lines are the visible consequence (D-039 wow moment #2 "The Character's Eye").
|
|
||||||
# No overlay is shown — the chime is the only client-side reaction (D-039 intent).
|
|
||||||
func _handle_triangle_crisis_events() -> void:
|
|
||||||
var events: Array = GameState.current_snapshot.get("triangle_crisis_events", [])
|
|
||||||
for ev in events:
|
|
||||||
if not ev is Dictionary or not ev.has("triangle_id"):
|
|
||||||
continue
|
|
||||||
var tid: int = ev.triangle_id
|
|
||||||
if not _known_triangle_ids.has(tid):
|
|
||||||
_known_triangle_ids[tid] = true
|
|
||||||
AudioManager.play(AudioManager.CHIME_ACTIVATION, AudioManager.BUS_UI_SOUNDS)
|
|
||||||
|
|
||||||
|
|
||||||
# D-073 (#529): Zone ambient crossfade — reads zone_id from GameState.current_zone_id
|
|
||||||
# (extracted in apply_snapshot(), server-authoritative per D-020).
|
|
||||||
# Calls AudioManager.set_zone() when zone changes (AudioManager handles crossfade).
|
|
||||||
func _update_zone() -> void:
|
|
||||||
var zone := GameState.current_zone_id
|
|
||||||
if zone != _current_zone:
|
|
||||||
_current_zone = zone
|
|
||||||
AudioManager.set_zone(zone)
|
|
||||||
|
|
||||||
|
|
||||||
# D-071 (#530): ListeningFocus boost — World SFX +2.5dB when stationary 30+ ticks.
|
|
||||||
# Uses AudioManager.get_active_dip() as single source of truth (no separate flag).
|
|
||||||
# Only activates when no other dip (dialogue/confrontation) is running.
|
|
||||||
# Only deactivates its own dip — never touches dialogue/confrontation.
|
|
||||||
# D-070: no UI indicator — the boost is "felt, not computed."
|
|
||||||
func _update_listening_focus() -> void:
|
|
||||||
var current_dip := AudioManager.get_active_dip()
|
|
||||||
var threshold_met := GameState.stationary_ticks >= LISTENING_FOCUS_TICKS
|
|
||||||
if threshold_met and current_dip == "":
|
|
||||||
AudioManager.apply_dip("listening_focus")
|
|
||||||
elif not threshold_met and current_dip == "listening_focus":
|
|
||||||
AudioManager.clear_dip()
|
|
||||||
|
|
||||||
|
|
||||||
# Consume-once per tick: show monologue text, then clear.
|
|
||||||
# Tick guard prevents re-triggering when the same tick is polled multiple
|
|
||||||
# times (client FPS > sim tick rate).
|
|
||||||
func _consume_monologue() -> void:
|
|
||||||
if GameState.current_monologue == null or not monologue_display:
|
|
||||||
return
|
|
||||||
if GameState.current_tick == _last_monologue_tick:
|
|
||||||
return
|
|
||||||
_last_monologue_tick = GameState.current_tick
|
|
||||||
var mono: Dictionary = GameState.current_monologue
|
|
||||||
monologue_display.show_monologue(
|
|
||||||
mono.get("text", ""),
|
|
||||||
mono.get("duration_seconds", 5.0),
|
|
||||||
mono.get("priority", 2),
|
|
||||||
mono.get("is_urgent", false)
|
|
||||||
)
|
|
||||||
# #502: Amber flash on room reset
|
|
||||||
var mono_id: String = mono.get("id", "")
|
|
||||||
if mono_id.begins_with("room_reset"):
|
|
||||||
_screen_flash(Constants.ENTITY_COLOR_POI, 0.15)
|
|
||||||
GameState.current_monologue = null
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Consume-once per tick with ID tracking: show dialogue, then clear.
|
|
||||||
# Tick guard + is_dialogue_active check prevent re-triggering.
|
|
||||||
func _consume_dialogue() -> void:
|
|
||||||
if GameState.current_dialogue == null or not dialogue_box:
|
|
||||||
return
|
|
||||||
if GameState.current_tick == _last_dialogue_tick:
|
|
||||||
return
|
|
||||||
if dialogue_box.is_dialogue_active():
|
|
||||||
GameState.current_dialogue = null
|
|
||||||
return
|
|
||||||
_last_dialogue_tick = GameState.current_tick
|
|
||||||
# #264: Close journal when dialogue opens (cannot be open simultaneously)
|
|
||||||
if journal_panel and journal_panel.has_method("close"):
|
|
||||||
journal_panel.close()
|
|
||||||
var dlg: Dictionary = GameState.current_dialogue
|
|
||||||
_last_dialogue_npc_id = dlg.get("npc_entity_id", -1)
|
|
||||||
_last_dialogue_npc_name = dlg.get("npc_name", "")
|
|
||||||
dialogue_box.show_dialogue(
|
|
||||||
dlg.get("npc_name", ""),
|
|
||||||
dlg.get("speech", ""),
|
|
||||||
dlg.get("options", []),
|
|
||||||
_last_dialogue_npc_id
|
|
||||||
)
|
|
||||||
GameState.current_dialogue = null
|
|
||||||
|
|
||||||
|
|
||||||
# #535: Consume overheard NPC-NPC conversation events (D-078).
|
|
||||||
# Each event carries pre-occluded text — render verbatim in the dialogue log.
|
|
||||||
func _consume_conversation_events() -> void:
|
|
||||||
if not dialogue_box:
|
|
||||||
return
|
|
||||||
for event in GameState.conversation_events:
|
|
||||||
dialogue_box.append_conversation_event(event)
|
|
||||||
GameState.conversation_events = []
|
|
||||||
|
|
||||||
|
|
||||||
# #535: Handle conversation_ended events — notify dialogue box to stop tracking pairs.
|
|
||||||
func _consume_conversation_ended() -> void:
|
|
||||||
if not dialogue_box:
|
|
||||||
return
|
|
||||||
for event in GameState.conversation_ended:
|
|
||||||
dialogue_box.on_conversation_ended(event)
|
|
||||||
GameState.conversation_ended = []
|
|
||||||
|
|
||||||
|
|
||||||
# #535: Consume dialogue_response — NPC follow-up line after player picks an option.
|
|
||||||
# Updates dialogue_box entity display registry with speaker identity from the wire.
|
|
||||||
func _consume_dialogue_response() -> void:
|
|
||||||
if GameState.dialogue_response == null or not dialogue_box:
|
|
||||||
return
|
|
||||||
var dr: Dictionary = GameState.dialogue_response
|
|
||||||
# v0.1: falls back to _last_dialogue_npc_id if wire omits speaker_entity_id.
|
|
||||||
# Edge case: fast re-engagement with a different NPC could misattribute — low probability.
|
|
||||||
var speaker_entity_id: int = dr.get("speaker_entity_id", _last_dialogue_npc_id)
|
|
||||||
var speaker_color_index: int = dr.get("speaker_color_index", -1)
|
|
||||||
var speaker_name: String = dr.get("speaker_name", _last_dialogue_npc_name)
|
|
||||||
dialogue_box.update_entity_display(speaker_entity_id, speaker_name, speaker_color_index)
|
|
||||||
dialogue_box.append_dialogue_response(speaker_name, dr.get("text", ""), speaker_entity_id)
|
|
||||||
GameState.dialogue_response = null
|
|
||||||
|
|
||||||
|
|
||||||
# #554/#257: Show save/load result notification; hide loading screen on load complete.
|
|
||||||
func _consume_save_result() -> void:
|
|
||||||
if GameState.save_result == null:
|
|
||||||
return
|
|
||||||
var result: Dictionary = GameState.save_result
|
|
||||||
GameState.save_result = null # consume once
|
|
||||||
# #257: Dismiss loading screen regardless of success/failure
|
|
||||||
if loading_screen:
|
|
||||||
loading_screen.hide_loading()
|
|
||||||
var msg: String
|
|
||||||
if result.get("success", false):
|
|
||||||
if result.get("kind", "") == "save":
|
|
||||||
msg = UIStrings.get_text("notifications.save_complete")
|
|
||||||
else:
|
|
||||||
msg = UIStrings.get_text("notifications.load_complete")
|
|
||||||
else:
|
|
||||||
if result.get("kind", "") == "save":
|
|
||||||
msg = UIStrings.get_text("notifications.save_failed")
|
|
||||||
else:
|
|
||||||
msg = UIStrings.get_text("notifications.load_failed")
|
|
||||||
if monologue_display:
|
|
||||||
monologue_display.show_notification(msg)
|
|
||||||
|
|
||||||
|
|
||||||
# #581: Forward debug_response from server to the debug console.
|
|
||||||
func _consume_debug_response() -> void:
|
|
||||||
if GameState.debug_response == null or not debug_console:
|
|
||||||
return
|
|
||||||
debug_console.append_response(GameState.debug_response)
|
|
||||||
GameState.debug_response = null
|
|
||||||
|
|
||||||
|
|
||||||
# D-061: Handle dialogue option selection → send to server
|
|
||||||
func _on_dialogue_option_selected(response_id: String, text: String) -> void:
|
|
||||||
SimBridge.send_input({
|
|
||||||
"action": InputMapper.Action.INTERACT,
|
|
||||||
"timestamp_msec": Time.get_ticks_msec(),
|
|
||||||
"action_data": {
|
|
||||||
"target_entity_id": null,
|
|
||||||
"verb": "DialogueResponse",
|
|
||||||
"response_id": response_id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
# D-063: Handle confrontation beat monologue → show on monologue display (layer 7)
|
|
||||||
# Confrontation lines are high-priority (3) and urgent — full opacity, elevated colour.
|
|
||||||
# Tick guard deduplicates if dialogue box emits the signal multiple times in one tick.
|
|
||||||
func _on_confrontation_monologue(text: String, duration: float) -> void:
|
|
||||||
if not monologue_display:
|
|
||||||
return
|
|
||||||
if GameState.current_tick == _last_confrontation_tick:
|
|
||||||
return
|
|
||||||
_last_confrontation_tick = GameState.current_tick
|
|
||||||
monologue_display.show_monologue(text, duration, 3, true)
|
|
||||||
|
|
||||||
|
|
||||||
# D-061: Auto-pause on dialogue open — routed through input recording (#507, Tyre #3)
|
|
||||||
func _on_dialogue_pause_requested() -> void:
|
|
||||||
var input := {"action": InputMapper.Action.PAUSE, "timestamp_msec": Time.get_ticks_msec()}
|
|
||||||
SimBridge.send_input(input)
|
|
||||||
_pending_record_inputs.append(input)
|
|
||||||
|
|
||||||
|
|
||||||
# D-061: Auto-unpause on dialogue close — routed through input recording (#507, Tyre #3)
|
|
||||||
func _on_dialogue_unpause_requested() -> void:
|
|
||||||
var input := {"action": InputMapper.Action.UNPAUSE, "timestamp_msec": Time.get_ticks_msec()}
|
|
||||||
SimBridge.send_input(input)
|
|
||||||
_pending_record_inputs.append(input)
|
|
||||||
|
|
||||||
|
|
||||||
# D-064: Handle walk-away → send WalkAway{npc_id} to server
|
|
||||||
func _on_dialogue_dismissed() -> void:
|
|
||||||
SimBridge.send_input({
|
|
||||||
"action": InputMapper.Action.INTERACT,
|
|
||||||
"timestamp_msec": Time.get_ticks_msec(),
|
|
||||||
"action_data": {
|
|
||||||
"target_entity_id": _last_dialogue_npc_id if _last_dialogue_npc_id >= 0 else null,
|
|
||||||
"verb": "WalkAway",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
# D-020 (#558): Coordinator handles dialogue state changes from dialogue_box.
|
|
||||||
# Synchronous signal — GameState.dialogue_active updates same frame (D-064).
|
|
||||||
func _on_dialogue_state_changed(active: bool) -> void:
|
|
||||||
GameState.dialogue_active = active
|
|
||||||
|
|
||||||
|
|
||||||
# D-020 (#558): Coordinator routes audio dip requests from dialogue_box.
|
|
||||||
func _on_audio_dip_requested(profile: String) -> void:
|
|
||||||
AudioManager.apply_dip(profile)
|
|
||||||
|
|
||||||
|
|
||||||
# D-020 (#558): Coordinator routes audio dip clear from dialogue_box.
|
|
||||||
func _on_audio_dip_cleared() -> void:
|
|
||||||
AudioManager.clear_dip()
|
|
||||||
|
|
||||||
|
|
||||||
# #496: Finalize gauntlet stats on disconnect
|
# #496: Finalize gauntlet stats on disconnect
|
||||||
func _on_connection_state_changed(old_state: SimBridge.ConnectionState, new_state: SimBridge.ConnectionState) -> void:
|
func _on_connection_state_changed(_old_state: SimBridge.ConnectionState, new_state: SimBridge.ConnectionState) -> void:
|
||||||
if new_state == SimBridge.ConnectionState.DISCONNECTED and gauntlet_hud:
|
if new_state == SimBridge.ConnectionState.DISCONNECTED and gauntlet_hud:
|
||||||
gauntlet_hud.finalize()
|
gauntlet_hud.finalize()
|
||||||
|
|
||||||
|
|
||||||
# #257: Deferred LOAD_GAME dispatch — fires once when SimBridge reaches CONNECTED.
|
# #257: Deferred LOAD_GAME dispatch — fires once when SimBridge reaches CONNECTED.
|
||||||
# pending_load_path is set by main_menu.gd before scene change.
|
|
||||||
func _on_sim_connected_for_load(_old_state: SimBridge.ConnectionState, new_state: SimBridge.ConnectionState) -> void:
|
func _on_sim_connected_for_load(_old_state: SimBridge.ConnectionState, new_state: SimBridge.ConnectionState) -> void:
|
||||||
if new_state != SimBridge.ConnectionState.CONNECTED:
|
if new_state != SimBridge.ConnectionState.CONNECTED:
|
||||||
return
|
return
|
||||||
@@ -564,19 +267,13 @@ func _dispatch_pending_load() -> void:
|
|||||||
loading_screen.hide_loading(false)
|
loading_screen.hide_loading(false)
|
||||||
|
|
||||||
|
|
||||||
# #501: Detect large position jump indicating a teleport (not normal movement).
|
# #501: Detect large position jump indicating a teleport.
|
||||||
const TELEPORT_DISTANCE_THRESHOLD: float = 5.0
|
|
||||||
|
|
||||||
func _detect_teleport(old_pos: Vector2, new_pos: Vector2) -> bool:
|
func _detect_teleport(old_pos: Vector2, new_pos: Vector2) -> bool:
|
||||||
return old_pos.distance_to(new_pos) > TELEPORT_DISTANCE_THRESHOLD
|
return old_pos.distance_to(new_pos) > TELEPORT_DISTANCE_THRESHOLD
|
||||||
|
|
||||||
|
|
||||||
# #501: Gauntlet dev teleport transition — snap camera + 0.3s fade-from-black.
|
# #501: Gauntlet dev teleport transition — snap camera + 0.3s fade-from-black.
|
||||||
# Clears dialogue/monologue/interaction state (server clears its side too).
|
|
||||||
# Scoped to Gauntlet testing only — production fast-travel uses diegetic gates.
|
|
||||||
func _teleport_transition() -> void:
|
func _teleport_transition() -> void:
|
||||||
# Set teleport flag — the camera tracking block in _process() will snap
|
|
||||||
# to the player's new position this frame (no lerp). Flag clears after snap.
|
|
||||||
_camera_anchored = true
|
_camera_anchored = true
|
||||||
_teleport_in_progress = true
|
_teleport_in_progress = true
|
||||||
|
|
||||||
@@ -584,12 +281,11 @@ func _teleport_transition() -> void:
|
|||||||
GameState.current_monologue = null
|
GameState.current_monologue = null
|
||||||
GameState.current_dialogue = null
|
GameState.current_dialogue = null
|
||||||
GameState.dialogue_active = false
|
GameState.dialogue_active = false
|
||||||
_known_recognition_ids.clear() # D-067: reset chimes for new room
|
_consumers.clear_recognition_state()
|
||||||
_known_triangle_ids.clear() # #590: reset activation chimes for new room
|
|
||||||
if dialogue_box and dialogue_box.is_dialogue_active():
|
if dialogue_box and dialogue_box.is_dialogue_active():
|
||||||
dialogue_box.hide_dialogue()
|
dialogue_box.hide_dialogue()
|
||||||
|
|
||||||
# Fade from black: instant black overlay, fades to transparent over 0.3s
|
# Fade from black
|
||||||
if _flash_rect and is_instance_valid(_flash_rect):
|
if _flash_rect and is_instance_valid(_flash_rect):
|
||||||
_flash_rect.queue_free()
|
_flash_rect.queue_free()
|
||||||
_flash_rect = ColorRect.new()
|
_flash_rect = ColorRect.new()
|
||||||
@@ -602,35 +298,17 @@ func _teleport_transition() -> void:
|
|||||||
tween.tween_callback(_flash_rect.queue_free)
|
tween.tween_callback(_flash_rect.queue_free)
|
||||||
|
|
||||||
|
|
||||||
# #174: Consume examine result — show overlay when server sends character-filtered observation.
|
# #264: Toggle journal panel.
|
||||||
# Clears after display (single-consume). Dismiss examine when dialogue opens.
|
|
||||||
func _consume_examine_result() -> void:
|
|
||||||
if GameState.current_examine_result == null or not examine_display:
|
|
||||||
return
|
|
||||||
var result: Dictionary = GameState.current_examine_result
|
|
||||||
# Dismiss existing examine result if dialogue is active (focus priority)
|
|
||||||
if dialogue_box and dialogue_box.is_dialogue_active():
|
|
||||||
if examine_display.has_method("dismiss"):
|
|
||||||
examine_display.dismiss()
|
|
||||||
else:
|
|
||||||
if examine_display.has_method("show_result"):
|
|
||||||
examine_display.show_result(result)
|
|
||||||
GameState.current_examine_result = null
|
|
||||||
|
|
||||||
|
|
||||||
# #264: Toggle journal panel. Called from input handler when J key pressed.
|
|
||||||
func _toggle_journal() -> void:
|
func _toggle_journal() -> void:
|
||||||
if not journal_panel:
|
if not journal_panel:
|
||||||
return
|
return
|
||||||
# Journal and dialogue cannot be open simultaneously (sprint briefing)
|
|
||||||
if dialogue_box and dialogue_box.is_dialogue_active():
|
if dialogue_box and dialogue_box.is_dialogue_active():
|
||||||
return
|
return
|
||||||
if journal_panel.has_method("toggle"):
|
if journal_panel.has_method("toggle"):
|
||||||
journal_panel.toggle()
|
journal_panel.toggle()
|
||||||
|
|
||||||
|
|
||||||
# #502: Full-screen color flash — fades from color to transparent over duration.
|
# #502: Full-screen color flash.
|
||||||
# Used for room reset amber flash. Creates ephemeral ColorRect on UILayer.
|
|
||||||
func _screen_flash(color: Color, duration: float) -> void:
|
func _screen_flash(color: Color, duration: float) -> void:
|
||||||
if _flash_rect and is_instance_valid(_flash_rect):
|
if _flash_rect and is_instance_valid(_flash_rect):
|
||||||
_flash_rect.queue_free()
|
_flash_rect.queue_free()
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
class_name SnapshotConsumers
|
||||||
|
## Non-dialogue snapshot consumers and event handlers extracted from main.gd (#775).
|
||||||
|
##
|
||||||
|
## Registered with SnapshotEventRouter; reads from GameState directly.
|
||||||
|
## UI node references passed via init(). All methods are zero-argument
|
||||||
|
## callables compatible with SnapshotEventRouter.
|
||||||
|
|
||||||
|
const LISTENING_FOCUS_TICKS: int = 30 # D-071: stationary ticks before ListeningFocus boost
|
||||||
|
|
||||||
|
var monologue_display: Node = null
|
||||||
|
var dialogue_box: Node = null
|
||||||
|
var examine_display: Node = null
|
||||||
|
var loading_screen: Node = null
|
||||||
|
var debug_console: Node = null
|
||||||
|
var cursor_renderer: Node = null
|
||||||
|
var interaction_list: Node = null
|
||||||
|
var interaction_prompt: Node = null
|
||||||
|
var minimap: Node = null
|
||||||
|
var star_map: Node = null
|
||||||
|
|
||||||
|
var _screen_flash_fn: Callable # Callable(color: Color, duration: float)
|
||||||
|
|
||||||
|
var _last_monologue_tick: int = -1
|
||||||
|
var _known_recognition_ids: Dictionary = {}
|
||||||
|
var _known_triangle_ids: Dictionary = {}
|
||||||
|
var _current_zone: String = ""
|
||||||
|
|
||||||
|
|
||||||
|
func init(refs: Dictionary, screen_flash: Callable) -> SnapshotConsumers:
|
||||||
|
monologue_display = refs.get("monologue_display")
|
||||||
|
dialogue_box = refs.get("dialogue_box")
|
||||||
|
examine_display = refs.get("examine_display")
|
||||||
|
loading_screen = refs.get("loading_screen")
|
||||||
|
debug_console = refs.get("debug_console")
|
||||||
|
cursor_renderer = refs.get("cursor_renderer")
|
||||||
|
interaction_list = refs.get("interaction_list")
|
||||||
|
interaction_prompt = refs.get("interaction_prompt")
|
||||||
|
minimap = refs.get("minimap")
|
||||||
|
star_map = refs.get("star_map")
|
||||||
|
_screen_flash_fn = screen_flash
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
# OQ-07 (#522): Propagate insert state to all z-layer-6 display nodes.
|
||||||
|
func propagate_insert_state() -> void:
|
||||||
|
var insert_state := GameState.insert_active
|
||||||
|
if cursor_renderer:
|
||||||
|
cursor_renderer.set_insert_active(insert_state)
|
||||||
|
if interaction_list:
|
||||||
|
interaction_list.set_insert_active(insert_state)
|
||||||
|
if interaction_prompt:
|
||||||
|
interaction_prompt.set_insert_active(insert_state)
|
||||||
|
if minimap:
|
||||||
|
minimap.set_insert_active(insert_state)
|
||||||
|
if star_map:
|
||||||
|
star_map.set_insert_active(insert_state)
|
||||||
|
|
||||||
|
|
||||||
|
# D-057: Update interaction list from game state.
|
||||||
|
# Suppress during dialogue — player is in conversation, verb list is noise.
|
||||||
|
func update_interaction_list() -> void:
|
||||||
|
if not interaction_list:
|
||||||
|
return
|
||||||
|
if dialogue_box and dialogue_box.is_dialogue_active():
|
||||||
|
if interaction_list.is_showing():
|
||||||
|
interaction_list.hide_list()
|
||||||
|
else:
|
||||||
|
interaction_list.update_from_state()
|
||||||
|
|
||||||
|
|
||||||
|
# D-018 #125: Play close-range sound events — fired once per snapshot tick.
|
||||||
|
func play_close_sound_events() -> void:
|
||||||
|
for evt in GameState.close_sound_events:
|
||||||
|
if not evt is Dictionary or not evt.has("x") or not evt.has("y"):
|
||||||
|
continue
|
||||||
|
AudioManager.play_sound_event(
|
||||||
|
evt.get("event_type", ""),
|
||||||
|
Vector2(float(evt.x), float(evt.y))
|
||||||
|
)
|
||||||
|
GameState.close_sound_events = []
|
||||||
|
|
||||||
|
|
||||||
|
# D-067: Recognition chime — fires sfx_monologue_chime when a fog entity
|
||||||
|
# enters the cognitive delay recognition queue for the first time.
|
||||||
|
func play_recognition_chimes() -> void:
|
||||||
|
for rec in GameState.pending_recognitions:
|
||||||
|
if not rec is Dictionary or not rec.has("entity_id"):
|
||||||
|
continue
|
||||||
|
var eid: int = rec.entity_id
|
||||||
|
if not _known_recognition_ids.has(eid):
|
||||||
|
_known_recognition_ids[eid] = true
|
||||||
|
AudioManager.play(AudioManager.CHIME_RECOGNITION)
|
||||||
|
|
||||||
|
|
||||||
|
# #590 D-072/D-089: Triangle activation consumer.
|
||||||
|
func handle_triangle_crisis_events() -> void:
|
||||||
|
var events: Array = GameState.current_snapshot.get("triangle_crisis_events", [])
|
||||||
|
for ev in events:
|
||||||
|
if not ev is Dictionary or not ev.has("triangle_id"):
|
||||||
|
continue
|
||||||
|
var tid: int = ev.triangle_id
|
||||||
|
if not _known_triangle_ids.has(tid):
|
||||||
|
_known_triangle_ids[tid] = true
|
||||||
|
AudioManager.play(AudioManager.CHIME_ACTIVATION, AudioManager.BUS_UI_SOUNDS)
|
||||||
|
|
||||||
|
|
||||||
|
# D-073 (#529): Zone ambient crossfade.
|
||||||
|
func update_zone() -> void:
|
||||||
|
var zone := GameState.current_zone_id
|
||||||
|
if zone != _current_zone:
|
||||||
|
_current_zone = zone
|
||||||
|
AudioManager.set_zone(zone)
|
||||||
|
|
||||||
|
|
||||||
|
# D-071 (#530): ListeningFocus boost — World SFX +2.5dB when stationary 30+ ticks.
|
||||||
|
func update_listening_focus() -> void:
|
||||||
|
var current_dip := AudioManager.get_active_dip()
|
||||||
|
var threshold_met := GameState.stationary_ticks >= LISTENING_FOCUS_TICKS
|
||||||
|
if threshold_met and current_dip == "":
|
||||||
|
AudioManager.apply_dip("listening_focus")
|
||||||
|
elif not threshold_met and current_dip == "listening_focus":
|
||||||
|
AudioManager.clear_dip()
|
||||||
|
|
||||||
|
|
||||||
|
# Consume-once per tick: show monologue text, then clear.
|
||||||
|
func consume_monologue() -> void:
|
||||||
|
if GameState.current_monologue == null or not monologue_display:
|
||||||
|
return
|
||||||
|
if GameState.current_tick == _last_monologue_tick:
|
||||||
|
return
|
||||||
|
_last_monologue_tick = GameState.current_tick
|
||||||
|
var mono: Dictionary = GameState.current_monologue
|
||||||
|
monologue_display.show_monologue(
|
||||||
|
mono.get("text", ""),
|
||||||
|
mono.get("duration_seconds", 5.0),
|
||||||
|
mono.get("priority", 2),
|
||||||
|
mono.get("is_urgent", false)
|
||||||
|
)
|
||||||
|
# #502: Amber flash on room reset
|
||||||
|
var mono_id: String = mono.get("id", "")
|
||||||
|
if mono_id.begins_with("room_reset"):
|
||||||
|
_screen_flash_fn.call(Constants.ENTITY_COLOR_POI, 0.15)
|
||||||
|
GameState.current_monologue = null
|
||||||
|
|
||||||
|
|
||||||
|
# #554/#257: Show save/load result notification; hide loading screen on load complete.
|
||||||
|
func consume_save_result() -> void:
|
||||||
|
if GameState.save_result == null:
|
||||||
|
return
|
||||||
|
var result: Dictionary = GameState.save_result
|
||||||
|
GameState.save_result = null
|
||||||
|
if loading_screen:
|
||||||
|
loading_screen.hide_loading()
|
||||||
|
var msg: String
|
||||||
|
if result.get("success", false):
|
||||||
|
if result.get("kind", "") == "save":
|
||||||
|
msg = UIStrings.get_text("notifications.save_complete")
|
||||||
|
else:
|
||||||
|
msg = UIStrings.get_text("notifications.load_complete")
|
||||||
|
else:
|
||||||
|
if result.get("kind", "") == "save":
|
||||||
|
msg = UIStrings.get_text("notifications.save_failed")
|
||||||
|
else:
|
||||||
|
msg = UIStrings.get_text("notifications.load_failed")
|
||||||
|
if monologue_display:
|
||||||
|
monologue_display.show_notification(msg)
|
||||||
|
|
||||||
|
|
||||||
|
# #581: Forward debug_response from server to the debug console.
|
||||||
|
func consume_debug_response() -> void:
|
||||||
|
if GameState.debug_response == null or not debug_console:
|
||||||
|
return
|
||||||
|
debug_console.append_response(GameState.debug_response)
|
||||||
|
GameState.debug_response = null
|
||||||
|
|
||||||
|
|
||||||
|
# #174: Consume examine result — show overlay when server sends character-filtered observation.
|
||||||
|
func consume_examine_result() -> void:
|
||||||
|
if GameState.current_examine_result == null or not examine_display:
|
||||||
|
return
|
||||||
|
var result: Dictionary = GameState.current_examine_result
|
||||||
|
if dialogue_box and dialogue_box.is_dialogue_active():
|
||||||
|
if examine_display.has_method("dismiss"):
|
||||||
|
examine_display.dismiss()
|
||||||
|
else:
|
||||||
|
if examine_display.has_method("show_result"):
|
||||||
|
examine_display.show_result(result)
|
||||||
|
GameState.current_examine_result = null
|
||||||
|
|
||||||
|
|
||||||
|
## Clear session-scoped recognition state (call on teleport / room change).
|
||||||
|
func clear_recognition_state() -> void:
|
||||||
|
_known_recognition_ids.clear()
|
||||||
|
_known_triangle_ids.clear()
|
||||||
@@ -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
|
||||||
@@ -19,7 +19,7 @@ func before_each() -> void:
|
|||||||
GameState.player_stance = "Walk"
|
GameState.player_stance = "Walk"
|
||||||
GameState.player_inventory = []
|
GameState.player_inventory = []
|
||||||
GameState.stationary_ticks = 0
|
GameState.stationary_ticks = 0
|
||||||
GameState._prev_player_position = Vector2(-1e9, -1e9)
|
SnapshotHandler._prev_player_position = Vector2(-1e9, -1e9)
|
||||||
GameState.current_zone_id = ""
|
GameState.current_zone_id = ""
|
||||||
GameState.insert_active = true
|
GameState.insert_active = true
|
||||||
|
|
||||||
@@ -67,7 +67,8 @@ func test_apply_snapshot_player_facing_missing_keeps_default() -> void:
|
|||||||
|
|
||||||
func test_apply_snapshot_sets_nearby_interactions() -> void:
|
func test_apply_snapshot_sets_nearby_interactions() -> void:
|
||||||
var interactions := [
|
var interactions := [
|
||||||
{"entity_id": 5, "entity_type": "Npc", "distance": 1.2, "verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}]},
|
{"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})
|
GameState.apply_snapshot({"tick": 1, "entities": [], "nearby_interactions": interactions})
|
||||||
assert_that(GameState.nearby_interactions.size()).is_equal(1)
|
assert_that(GameState.nearby_interactions.size()).is_equal(1)
|
||||||
@@ -83,7 +84,10 @@ func test_apply_snapshot_nearby_interactions_absent_clears_list() -> void:
|
|||||||
# -- v5: current_monologue (#414) -----------------------------------------
|
# -- v5: current_monologue (#414) -----------------------------------------
|
||||||
|
|
||||||
func test_apply_snapshot_sets_monologue() -> void:
|
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}
|
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})
|
GameState.apply_snapshot({"tick": 1, "entities": [], "current_monologue": monologue})
|
||||||
assert_that(GameState.current_monologue).is_not_null()
|
assert_that(GameState.current_monologue).is_not_null()
|
||||||
assert_that(GameState.current_monologue.get("text")).is_equal("Something is off here.")
|
assert_that(GameState.current_monologue.get("text")).is_equal("Something is off here.")
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ extends GdUnitTestSuite
|
|||||||
|
|
||||||
func before_each() -> void:
|
func before_each() -> void:
|
||||||
GameState.stationary_ticks = 0
|
GameState.stationary_ticks = 0
|
||||||
GameState._prev_player_position = Vector2(-1e9, -1e9)
|
SnapshotHandler._prev_player_position = Vector2(-1e9, -1e9)
|
||||||
GameState.current_zone_id = ""
|
GameState.current_zone_id = ""
|
||||||
GameState.player_position = Vector2.ZERO
|
GameState.player_position = Vector2.ZERO
|
||||||
GameState.visible_tiles = []
|
GameState.visible_tiles = []
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
## Smoke tests for main scene initialization
|
## Smoke tests for main scene initialization
|
||||||
## Run with: godot4 --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -a res://tests/
|
## Run with: godot4 --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd
|
||||||
|
## --ignoreHeadlessMode -a res://tests/
|
||||||
class_name TestMainScene
|
class_name TestMainScene
|
||||||
extends GdUnitTestSuite
|
extends GdUnitTestSuite
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user