From 88c7407cb6cbd73ce7831f93813d9cdfea71c6b4 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 5 Apr 2026 11:09:10 +0200 Subject: [PATCH] =?UTF-8?q?style(client):=20gdformat=20all=2052=20GDScript?= =?UTF-8?q?=20files=20=E2=80=94=20zero=20format=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- client/scripts/autoloads/audio_manager.gd | 47 ++- client/scripts/autoloads/fog_state.gd | 24 +- client/scripts/autoloads/game_state.gd | 3 +- client/scripts/autoloads/hud_groups.gd | 4 +- client/scripts/autoloads/input_mapper.gd | 144 +++++--- client/scripts/autoloads/platform_info.gd | 130 ++++--- client/scripts/autoloads/session_manager.gd | 69 ++-- client/scripts/autoloads/sim_bridge.gd | 179 +++++++--- client/scripts/autoloads/ui_strings.gd | 7 + .../scripts/checklist/checklist_evaluator.gd | 39 ++- client/scripts/constants.gd | 80 +++-- client/scripts/dialogue_coordinator.gd | 51 ++- client/scripts/main.gd | 103 ++++-- client/scripts/protocol/local_bridge.gd | 15 +- client/scripts/protocol/protocol.gd | 274 ++++++++++----- client/scripts/protocol/test_harness.gd | 283 ++++++++++----- client/scripts/rendering/character_visual.gd | 70 +++- .../rendering/character_visual_descriptor.gd | 3 +- client/scripts/rendering/cursor_renderer.gd | 103 ++++-- client/scripts/rendering/entity_renderer.gd | 37 +- client/scripts/rendering/fog_entities.gd | 40 +-- client/scripts/rendering/fog_shader.gd | 10 +- .../rendering/sound_indicator_renderer.gd | 48 ++- client/scripts/rendering/tile_renderer.gd | 13 +- client/scripts/rendering/world_renderer.gd | 2 + client/scripts/snapshot_consumers.gd | 3 +- client/scripts/snapshot_handler.gd | 39 ++- client/scripts/util/yaml_parser.gd | 18 +- client/ui/bug_report_dialog.gd | 125 +++++-- client/ui/character_creation.gd | 329 +++++++++++++----- client/ui/checklist_overlay.gd | 37 +- client/ui/debug_console.gd | 53 ++- client/ui/debug_overlay.gd | 186 +++++++--- client/ui/dialogue_box.gd | 127 ++++--- client/ui/examine_display.gd | 18 +- client/ui/gauntlet_hud.gd | 43 ++- client/ui/hardware_detector.gd | 57 ++- client/ui/hud.gd | 3 + client/ui/implant/implant_separator.gd | 7 +- client/ui/implant/implant_text_block.gd | 5 +- client/ui/interaction_list.gd | 38 +- client/ui/interaction_prompt.gd | 6 + client/ui/inventory_grid.gd | 21 +- client/ui/journal_panel.gd | 31 +- client/ui/main_menu.gd | 15 +- client/ui/minimap.gd | 48 ++- client/ui/monologue_display.gd | 112 +++--- client/ui/settings_dialog.gd | 82 +++-- client/ui/stance_indicator.gd | 17 +- client/ui/star_map.gd | 111 ++++-- client/ui/time_display.gd | 41 ++- client/ui/world_radial.gd | 34 +- 52 files changed, 2323 insertions(+), 1061 deletions(-) diff --git a/client/scripts/autoloads/audio_manager.gd b/client/scripts/autoloads/audio_manager.gd index fa09dce1c..a71ba9406 100644 --- a/client/scripts/autoloads/audio_manager.gd +++ b/client/scripts/autoloads/audio_manager.gd @@ -30,18 +30,24 @@ const BUSES := [BUS_MUSIC, BUS_AMBIENT, BUS_WORLD_SFX, BUS_PLAYER_ACTIONS, BUS_U # NOTE: listening_focus requires 30+ stationary ticks before activation (D-069/D-071). # The tick gate is the caller's responsibility — AudioManager only manages bus volumes. const DIP_SPECS := { - "dialogue": { - "ease_in": 0.3, "ease_out": 0.5, - "buses": { "Ambient": -7.0 }, + "dialogue": + { + "ease_in": 0.3, + "ease_out": 0.5, + "buses": {"Ambient": -7.0}, }, - "confrontation": { - "ease_in": 0.5, "ease_out": 1.0, - "buses": { "Ambient": -11.0, "WorldSFX": -5.0 }, + "confrontation": + { + "ease_in": 0.5, + "ease_out": 1.0, + "buses": {"Ambient": -11.0, "WorldSFX": -5.0}, "filter_hz": 800.0, }, - "listening_focus": { - "ease_in": 0.5, "ease_out": 0.5, - "buses": { "WorldSFX": 2.5 }, + "listening_focus": + { + "ease_in": 0.5, + "ease_out": 0.5, + "buses": {"WorldSFX": 2.5}, }, } @@ -59,10 +65,10 @@ const CROSSFADE_DURATION := 1.8 # D-073: 1.5-2s spec, mid-range # Note: amb_station_base (D-038 global base hum) plays globally via play_loop() # at startup — it is not zone-dependent and has no ZONE_ASSETS entry. const ZONE_ASSETS: Dictionary = { - "hub": "amb_hub_layer", + "hub": "amb_hub_layer", "workplace": "amb_hub_layer", - "bar": "amb_bar_layer", - "corridor": "amb_corridor_layer", + "bar": "amb_bar_layer", + "corridor": "amb_corridor_layer", } const PREFS_PATH := "user://audio_prefs.cfg" @@ -73,12 +79,12 @@ const PREFS_PATH := "user://audio_prefs.cfg" # Audio assets per D-038: footstep variants (walk / run), NPC murmur (D-072, #532). # Missing assets no-op gracefully (D-038 fallback pattern). const SOUND_EVENT_ASSETS: Dictionary = { - "Footstep": "sfx_footstep_metal_walk", - "FootstepWalk": "sfx_footstep_metal_walk", - "FootstepCareful":"sfx_footstep_metal_walk", # D-053: same asset until stance-differentiated audio lands + "Footstep": "sfx_footstep_metal_walk", + "FootstepWalk": "sfx_footstep_metal_walk", + "FootstepCareful": "sfx_footstep_metal_walk", # D-053: same asset until stance-differentiated audio lands "FootstepCrouch": "sfx_footstep_metal_walk", # D-053: same asset until stance-differentiated audio lands "FootstepSprint": "sfx_footstep_metal_run", - "FootstepRun": "sfx_footstep_metal_run", + "FootstepRun": "sfx_footstep_metal_run", } # Asset registry: filename stem (e.g. "amb_station_base") → AudioStream @@ -110,6 +116,7 @@ func _ready() -> void: # --- Bus setup --- + func _setup_buses() -> void: for bus_name in BUSES: if AudioServer.get_bus_index(bus_name) < 0: @@ -130,6 +137,7 @@ func _setup_buses() -> void: # --- Asset registry (D-068 directory-scan pattern) --- + func _scan_registry() -> void: _scan_dir("res://assets/audio/") print("AudioManager: %d assets registered" % _registry.size()) @@ -166,6 +174,7 @@ func get_registry_size() -> int: # --- Playback: non-spatial --- + ## Play a one-shot sound on the given bus. No-ops if asset not in registry. func play(asset_key: String, bus: String = BUS_UI_SOUNDS) -> void: var stream := _get_stream(asset_key) @@ -227,6 +236,7 @@ func play_sound_event(event_type: String, world_tile_pos: Vector2) -> void: # --- Playback: spatial (D-018 close-range) --- + ## Play a one-shot spatial sound at a world position (pixels). ## Added to current_scene for correct AudioListener2D positioning. func play_at(asset_key: String, world_position: Vector2, bus: String = BUS_WORLD_SFX) -> void: @@ -248,6 +258,7 @@ func play_at(asset_key: String, world_position: Vector2, bus: String = BUS_WORLD # --- Audio dip profiles (D-069) --- + ## Apply a dip profile. Interrupts any active dip, restoring unaffected buses. ## Profiles: "dialogue", "confrontation", "listening_focus" func apply_dip(profile: String) -> void: @@ -318,6 +329,7 @@ func get_active_dip() -> String: # --- Volume control (player sliders, D-069 proportional base) --- + ## Set the player's volume preference for a bus. During active dip, immediately ## recalculates effective volume (proportional to new base). func set_volume(bus: String, volume_db: float) -> void: @@ -341,6 +353,7 @@ func get_volume(bus: String) -> float: # --- Volume persistence (user://audio_prefs.cfg) --- + func _load_prefs() -> void: var cfg := ConfigFile.new() if cfg.load(PREFS_PATH) != OK: @@ -366,6 +379,7 @@ func _save_prefs() -> void: # --- Zone crossfade (D-073) --- + ## Handle zone transition. Server sends zone_id per tile in ObserverSnapshot. ## Hard boundary trigger with 1.5-2s audio crossfade between ambient layers. ## Interruptible — mid-crossfade zone change tweens from current position. @@ -415,6 +429,7 @@ func _kill_zone_tweens() -> void: # --- Internal helpers --- + func _get_stream(asset_key: String) -> AudioStream: return _registry.get(asset_key) as AudioStream diff --git a/client/scripts/autoloads/fog_state.gd b/client/scripts/autoloads/fog_state.gd index 70846df1e..c1bf7fefb 100644 --- a/client/scripts/autoloads/fog_state.gd +++ b/client/scripts/autoloads/fog_state.gd @@ -7,14 +7,14 @@ extends Node # Fog texture byte values — visibility and exploration layers. # Used by fog shader to distinguish visual treatment per tile. # Test assertions reference these: assert_that(byte).is_equal(FogState.VIS_FORWARD) -const VIS_HIDDEN: int = 0 # Not in LOS — fully fogged +const VIS_HIDDEN: int = 0 # Not in LOS — fully fogged const VIS_PERIPHERAL: int = 180 # DEPRECATED: peripheral sector removed in Sprint 22 (#569). # Retained — tests still reference it. -const VIS_FORWARD: int = 255 # In LOS, forward sector — clear vision +const VIS_FORWARD: int = 255 # In LOS, forward sector — clear vision -const EXP_UNEXPLORED: int = 0 # Never seen — total darkness -const EXP_EXPLORED: int = 128 # Previously seen, now out of LOS — deep fog -const EXP_VISIBLE: int = 255 # Currently in LOS — clear (written each frame) +const EXP_UNEXPLORED: int = 0 # Never seen — total darkness +const EXP_EXPLORED: int = 128 # Previously seen, now out of LOS — deep fog +const EXP_VISIBLE: int = 255 # Currently in LOS — clear (written each frame) # Zone temperature tints (D-059 + D-046, Sprint 22) — keyed by zone_id string from server. # Matches audio_manager.gd ZONE_ASSETS zone_id strings for consistent zone semantics. @@ -25,10 +25,10 @@ const EXP_VISIBLE: int = 255 # Currently in LOS — clear (written each frame # bar: #2a1f15 (warm amber-dark — social, inhabited) # corridor: #1a1a1a (neutral dark — transitional, maintenance) const ZONE_TINTS: Dictionary = { - "hub": Color(0.102, 0.122, 0.180), # #1a1f2e — cool blue-dark + "hub": Color(0.102, 0.122, 0.180), # #1a1f2e — cool blue-dark "workplace": Color(0.102, 0.122, 0.180), # same as hub - "bar": Color(0.165, 0.122, 0.082), # #2a1f15 — warm amber-dark - "corridor": Color(0.102, 0.102, 0.102), # #1a1a1a — neutral dark + "bar": Color(0.165, 0.122, 0.082), # #2a1f15 — warm amber-dark + "corridor": Color(0.102, 0.102, 0.102), # #1a1a1a — neutral dark } const ZONE_TINT_DEFAULT: Color = Color(0.102, 0.102, 0.102) # #1a1a1a neutral @@ -213,7 +213,11 @@ func update_from_state() -> void: var new_g := int(tint_color.g * 255.0) var new_b := int(tint_color.b * 255.0) # Only update if different from current (avoid spurious texture uploads) - if _tint_bytes[tint_idx] != new_r or _tint_bytes[tint_idx + 1] != new_g or _tint_bytes[tint_idx + 2] != new_b: + if ( + _tint_bytes[tint_idx] != new_r + or _tint_bytes[tint_idx + 1] != new_g + or _tint_bytes[tint_idx + 2] != new_b + ): _tint_bytes[tint_idx + 0] = new_r _tint_bytes[tint_idx + 1] = new_g _tint_bytes[tint_idx + 2] = new_b @@ -243,5 +247,3 @@ func _grow_bounds_from_positions(positions: Dictionary) -> Rect2i: if map_bounds.size.x <= 1 and map_bounds.size.y <= 1: return tile_bounds return map_bounds.merge(tile_bounds) - - diff --git a/client/scripts/autoloads/game_state.gd b/client/scripts/autoloads/game_state.gd index 14e137ce2..96cc7ef79 100644 --- a/client/scripts/autoloads/game_state.gd +++ b/client/scripts/autoloads/game_state.gd @@ -18,7 +18,7 @@ var current_tick: int = 0 var player_position: Vector2 = Vector2.ZERO var visible_entities: Array = [] var visible_tiles: Array = [] -var visible_positions: Dictionary = {} # Vector2i -> true, for fast fog lookups (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 @@ -162,6 +162,7 @@ var stationary_ticks: int = 0 # Empty string when zone_id field absent. var current_zone_id: String = "" + func apply_snapshot(snapshot: Dictionary) -> void: # 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. diff --git a/client/scripts/autoloads/hud_groups.gd b/client/scripts/autoloads/hud_groups.gd index 2eb6da5ff..87d548b81 100644 --- a/client/scripts/autoloads/hud_groups.gd +++ b/client/scripts/autoloads/hud_groups.gd @@ -28,8 +28,8 @@ extends Node ## HudGroups.close_app() # returns to gameplay ## HudGroups.toggle_app("implant/map") # open if closed, close if open -var _groups: Dictionary = {} # group_name -> Array[Control] -var _active_app: String = "" # currently open implant/* app ("" = none) +var _groups: Dictionary = {} # group_name -> Array[Control] +var _active_app: String = "" # currently open implant/* app ("" = none) func register(node: Control, group: String) -> void: diff --git a/client/scripts/autoloads/input_mapper.gd b/client/scripts/autoloads/input_mapper.gd index 60071066d..ce019f291 100644 --- a/client/scripts/autoloads/input_mapper.gd +++ b/client/scripts/autoloads/input_mapper.gd @@ -14,10 +14,21 @@ extends Node # Server cooldown is authoritative, but the client throttle prevents # flooding and gives correct movement feel in test mode. enum Action { - MOVE_NORTH, MOVE_NORTHEAST, MOVE_EAST, MOVE_SOUTHEAST, - MOVE_SOUTH, MOVE_SOUTHWEST, MOVE_WEST, MOVE_NORTHWEST, - INTERACT, USE_PERCEPTION_MODE, OPEN_MENU, PAUSE, UNPAUSE, - TOGGLE_STANCE_UP, TOGGLE_STANCE_DOWN, + MOVE_NORTH, + MOVE_NORTHEAST, + MOVE_EAST, + MOVE_SOUTHEAST, + MOVE_SOUTH, + MOVE_SOUTHWEST, + MOVE_WEST, + MOVE_NORTHWEST, + INTERACT, + USE_PERCEPTION_MODE, + OPEN_MENU, + PAUSE, + UNPAUSE, + TOGGLE_STANCE_UP, + TOGGLE_STANCE_DOWN, BUG_REPORT, # #495: F12 WRONG button — client-only, not sent to server OPEN_JOURNAL, # #264: J key — toggle knowledge journal panel, client-only SET_FACING, # D-054: facing octant update (no movement) @@ -25,18 +36,18 @@ enum Action { SAVE_GAME, # #554: F5 quicksave — sends SaveGame to server with save path LOAD_GAME, # #554: F6 quickload — sends LoadGame to server with save path DEBUG_COMMAND, # #581: debug console command dispatch — sends DebugCommandKind to server - CHANGE_SETTINGS, # #646: persist a setting to server SQLite — sends {key, value} to server - REQUEST_ALL_SETTINGS, # #646: request full settings dump from server after handshake (unit variant) - DELETE_SETTING, # #646: delete a setting by key from server SQLite (struct variant) + CHANGE_SETTINGS, # #646: persist a setting to server SQLite — sends {key, value} to server + REQUEST_ALL_SETTINGS, # #646: request full settings dump from server after handshake (unit variant) + DELETE_SETTING, # #646: delete a setting by key from server SQLite (struct variant) } # Minimum milliseconds between movement commands, per stance. # Tuned so Walk feels like walking, Sprint feels fast but readable. const MOVE_INTERVAL_MS := { - "Sprint": 200, # 5/sec — fast but trackable - "Walk": 400, # 2.5/sec — comfortable walking pace - "Careful": 600, # ~1.7/sec — deliberate, scanning - "Crouch": 800, # 1.25/sec — creeping + "Sprint": 200, # 5/sec — fast but trackable + "Walk": 400, # 2.5/sec — comfortable walking pace + "Careful": 600, # ~1.7/sec — deliberate, scanning + "Crouch": 800, # 1.25/sec — creeping } var input_queue: Array[Dictionary] = [] @@ -44,7 +55,7 @@ var input_queue: Array[Dictionary] = [] # D-054: Client-side facing angle (radians). 0=East, -PI/2=North, PI/2=South. # Updated every frame from mouse position. EntityRenderer reads this for indicator. var facing_angle: float = -PI / 2.0 # Default: North -var facing_octant: String = "North" # Derived from facing_angle +var facing_octant: String = "North" # Derived from facing_angle var _last_sent_octant: String = "North" # Track to avoid redundant sends var _last_move_msec: int = 0 @@ -63,11 +74,16 @@ func _process(_delta: float) -> void: # D-054: Send facing octant to server when it changes (even without movement) if facing_octant != _last_sent_octant: _last_sent_octant = facing_octant - input_queue.append({ - "action": Action.SET_FACING, - "timestamp_msec": Time.get_ticks_msec(), - "action_data": {"facing": facing_octant}, - }) + ( + input_queue + . append( + { + "action": Action.SET_FACING, + "timestamp_msec": Time.get_ticks_msec(), + "action_data": {"facing": facing_octant}, + } + ) + ) # Poll held WASD keys var raw_dir := Vector2i.ZERO @@ -88,10 +104,15 @@ func _process(_delta: float) -> void: # D-054: Transform WASD input relative to mouse facing var world_dir := _wasd_to_world_dir(raw_dir) var action: Action = _dir_to_action(world_dir) - input_queue.append({ - "action": action, - "timestamp_msec": now, - }) + ( + input_queue + . append( + { + "action": action, + "timestamp_msec": now, + } + ) + ) # Discrete actions: fire once on key press (not held). @@ -197,15 +218,24 @@ static func _snap_to_octant_dir(dir: Vector2) -> Vector2i: # Quantize to nearest 45° (PI/4) var octant := roundi(angle / (PI / 4.0)) match octant: - 0: return Vector2i(1, 0) # East - 1: return Vector2i(1, 1) # Southeast - 2, -6: return Vector2i(0, 1) # South - 3, -5: return Vector2i(-1, 1) # Southwest - 4, -4: return Vector2i(-1, 0) # West - -3, 5: return Vector2i(-1, -1) # Northwest - -2: return Vector2i(0, -1) # North - -1: return Vector2i(1, -1) # Northeast - _: return Vector2i.ZERO + 0: + return Vector2i(1, 0) # East + 1: + return Vector2i(1, 1) # Southeast + 2, -6: + return Vector2i(0, 1) # South + 3, -5: + return Vector2i(-1, 1) # Southwest + 4, -4: + return Vector2i(-1, 0) # West + -3, 5: + return Vector2i(-1, -1) # Northwest + -2: + return Vector2i(0, -1) # North + -1: + return Vector2i(1, -1) # Northeast + _: + return Vector2i.ZERO # D-054: Convert a facing angle (radians) to the nearest octant name. @@ -213,27 +243,45 @@ static func _snap_to_octant_dir(dir: Vector2) -> Vector2i: static func _angle_to_octant(angle: float) -> String: var octant := roundi(angle / (PI / 4.0)) match octant: - 0: return "East" - 1: return "Southeast" - 2, -6: return "South" - 3, -5: return "Southwest" - 4, -4: return "West" - -3, 5: return "Northwest" - -2: return "North" - -1: return "Northeast" - _: return "East" + 0: + return "East" + 1: + return "Southeast" + 2, -6: + return "South" + 3, -5: + return "Southwest" + 4, -4: + return "West" + -3, 5: + return "Northwest" + -2: + return "North" + -1: + return "Northeast" + _: + return "East" # Map a direction vector to the corresponding movement Action. # Handles all 8 directions via composite W+D, W+A, etc. static func _dir_to_action(dir: Vector2i) -> Action: match dir: - Vector2i(0, -1): return Action.MOVE_NORTH - Vector2i(1, -1): return Action.MOVE_NORTHEAST - Vector2i(1, 0): return Action.MOVE_EAST - Vector2i(1, 1): return Action.MOVE_SOUTHEAST - Vector2i(0, 1): return Action.MOVE_SOUTH - Vector2i(-1, 1): return Action.MOVE_SOUTHWEST - Vector2i(-1, 0): return Action.MOVE_WEST - Vector2i(-1, -1): return Action.MOVE_NORTHWEST - _: return Action.MOVE_NORTH + Vector2i(0, -1): + return Action.MOVE_NORTH + Vector2i(1, -1): + return Action.MOVE_NORTHEAST + Vector2i(1, 0): + return Action.MOVE_EAST + Vector2i(1, 1): + return Action.MOVE_SOUTHEAST + Vector2i(0, 1): + return Action.MOVE_SOUTH + Vector2i(-1, 1): + return Action.MOVE_SOUTHWEST + Vector2i(-1, 0): + return Action.MOVE_WEST + Vector2i(-1, -1): + return Action.MOVE_NORTHWEST + _: + return Action.MOVE_NORTH diff --git a/client/scripts/autoloads/platform_info.gd b/client/scripts/autoloads/platform_info.gd index 91ecc3c3a..65df526bd 100644 --- a/client/scripts/autoloads/platform_info.gd +++ b/client/scripts/autoloads/platform_info.gd @@ -16,28 +16,23 @@ extends Node ## Paths: Read-once OS path constants, populated in _ready(). ## Diag: get_diagnostics() — flat dict of all properties for bug reports. - # -- Power profile ------------------------------------------------------------ ## Emitted when the detected power profile changes. signal power_profile_changed(old_profile: int, new_profile: int) ## High-level power classification. POWER_SAVER reserved for future OS API. -enum PowerProfile { - FULL = 0, # Plugged in (charged, charging, or no battery) — no restrictions - BATTERY = 1, # On battery — AI inference should be suspended per D-138 §8 Layer 3 - POWER_SAVER = 2 # System-level power-saver mode (future: no cross-platform API yet) -} +enum PowerProfile { FULL = 0, BATTERY = 1, POWER_SAVER = 2 } # Plugged in (charged, charging, or no battery) — no restrictions # On battery — AI inference should be suspended per D-138 §8 Layer 3 # System-level power-saver mode (future: no cross-platform API yet) ## How often (seconds) to re-poll OS for power state changes. const POWER_POLL_INTERVAL := 30.0 # OS power_state integer constants (Godot 4 — same values as POWERSTATE_* enum). -const _POWER_STATE_UNKNOWN := 0 +const _POWER_STATE_UNKNOWN := 0 const _POWER_STATE_ON_BATTERY := 1 const _POWER_STATE_NO_BATTERY := 2 -const _POWER_STATE_CHARGING := 3 -const _POWER_STATE_CHARGED := 4 +const _POWER_STATE_CHARGING := 3 +const _POWER_STATE_CHARGED := 4 ## Current power profile. Updated by the 30-second poll timer. var power_profile: PowerProfile = PowerProfile.FULL @@ -48,7 +43,6 @@ var raw_power_state: int = 0 ## Battery charge percentage (0–100). -1 if not available or not on battery. var battery_percent: int = -1 - # -- Memory ------------------------------------------------------------------- ## Free physical RAM in megabytes. Call refresh_memory() before reading if staleness matters. @@ -57,7 +51,6 @@ var free_memory_mb: float = 0.0 ## Total physical RAM in megabytes. Populated once in _ready(); doesn't change at runtime. var total_memory_mb: float = 0.0 - # -- CPU ---------------------------------------------------------------------- ## Human-readable processor name (e.g. "Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz"). @@ -66,7 +59,6 @@ var cpu_name: String = "" ## Number of logical CPU cores reported by the OS. var cpu_logical_cores: int = 0 - # -- GPU ---------------------------------------------------------------------- ## GPU display name (e.g. "NVIDIA GeForce RTX 3080"). @@ -84,7 +76,6 @@ var gpu_type: String = "other" ## Low-level driver info strings from the OS (may be empty on some platforms). var gpu_driver_info: PackedStringArray = PackedStringArray() - # -- Platform identity -------------------------------------------------------- ## OS name reported by Godot (e.g. "Linux", "Windows", "macOS"). @@ -99,7 +90,6 @@ var distribution: String = "" ## True if the application is running inside a sandbox (Flatpak, Snap, macOS sandbox, etc.). var is_sandboxed: bool = false - # -- Display ------------------------------------------------------------------ ## Number of connected screens. @@ -117,7 +107,6 @@ var screen_refresh_rate: float = 60.0 ## UI scale factor. Uses OS-reported scale on macOS; falls back to dpi / 96.0 elsewhere. var display_scale: float = 1.0 - # -- Locale ------------------------------------------------------------------- ## Full locale string (e.g. "en_US"). @@ -126,7 +115,6 @@ var locale: String = "" ## Language portion of the locale (e.g. "en"). var locale_language: String = "" - # -- File paths --------------------------------------------------------------- ## Base user data directory (user://). @@ -150,9 +138,9 @@ var cache_dir: String = "" ## Directory where the bundled Gemma 2 model lives (derived from install_dir). var model_dir: String = "" - # -- Lifecycle ---------------------------------------------------------------- + func _ready() -> void: _init_paths() _init_hardware() @@ -167,12 +155,12 @@ func _ready() -> void: func _init_paths() -> void: - user_data_dir = OS.get_user_data_dir() - config_dir = user_data_dir + user_data_dir = OS.get_user_data_dir() + config_dir = user_data_dir benchmark_cache_path = "user://ai-dialogue-config.json" - executable_path = OS.get_executable_path() - install_dir = executable_path.get_base_dir() - model_dir = install_dir + "/models" + executable_path = OS.get_executable_path() + install_dir = executable_path.get_base_dir() + model_dir = install_dir + "/models" # get_cache_dir() may not exist on all Godot builds — use callv for safety. var os_cache: String = "" if OS.has_method("get_cache_dir"): @@ -184,12 +172,12 @@ func _init_paths() -> void: func _init_hardware() -> void: # -- CPU - cpu_name = OS.get_processor_name() + cpu_name = OS.get_processor_name() cpu_logical_cores = OS.get_processor_count() # -- GPU (guarded: RenderingServer methods may return empty in headless mode) - gpu_name = RenderingServer.get_video_adapter_name() - gpu_vendor = RenderingServer.get_video_adapter_vendor() + gpu_name = RenderingServer.get_video_adapter_name() + gpu_vendor = RenderingServer.get_video_adapter_vendor() gpu_api_version = RenderingServer.get_video_adapter_api_version() gpu_driver_info = OS.get_video_adapter_driver_info() if RenderingServer.has_method("get_video_adapter_type"): @@ -198,7 +186,7 @@ func _init_hardware() -> void: # -- Platform identity platform_name = OS.get_name() - os_version = OS.get_version() + os_version = OS.get_version() if OS.has_method("get_distribution_name"): var dist: Variant = OS.callv("get_distribution_name", []) if dist is String: @@ -207,14 +195,14 @@ func _init_hardware() -> void: # -- Display screen_count = DisplayServer.get_screen_count() - screen_size = DisplayServer.screen_get_size() - screen_dpi = DisplayServer.screen_get_dpi() + screen_size = DisplayServer.screen_get_size() + screen_dpi = DisplayServer.screen_get_dpi() var raw_rate: float = DisplayServer.screen_get_refresh_rate() screen_refresh_rate = raw_rate if raw_rate >= 0.0 else 60.0 display_scale = _compute_display_scale() # -- Locale - locale = OS.get_locale() + locale = OS.get_locale() locale_language = OS.get_locale_language() @@ -222,11 +210,16 @@ func _init_hardware() -> void: ## Values: 0=OTHER, 1=INTEGRATED, 2=DISCRETE, 3=VIRTUAL, 4=CPU. static func _map_adapter_type(adapter_type: int) -> String: match adapter_type: - 1: return "integrated" - 2: return "discrete" - 3: return "virtual" - 4: return "cpu" - _: return "other" + 1: + return "integrated" + 2: + return "discrete" + 3: + return "virtual" + 4: + return "cpu" + _: + return "other" ## Compute display scale for the primary screen. @@ -241,17 +234,19 @@ func _compute_display_scale() -> float: # -- Memory API --------------------------------------------------------------- + ## Refresh free/total memory readings from the OS. Call before read if freshness matters. func refresh_memory() -> void: var mem := OS.get_memory_info() # "available" = Linux MemAvailable / Windows ullAvailPhys (includes reclaimable pages). # Fall back to "free" on platforms that don't provide "available". - free_memory_mb = float(mem.get("available", mem.get("free", 0))) / (1024.0 * 1024.0) + free_memory_mb = float(mem.get("available", mem.get("free", 0))) / (1024.0 * 1024.0) total_memory_mb = float(mem.get("physical", 0)) / (1024.0 * 1024.0) # -- Power API ---------------------------------------------------------------- + ## Classify an OS power_state integer into a human-readable string. ## 1 (ON_BATTERY) → "battery", 2/3/4 (plugged) → "plugged", 0 → "unknown". static func classify_power_state(power_state: int) -> String: @@ -293,9 +288,9 @@ func _poll_power() -> void: if raw_power_state == _POWER_STATE_ON_BATTERY: battery_percent = int(info.get("percent", -1)) - var new_profile: PowerProfile = PowerProfile.BATTERY \ - if raw_power_state == _POWER_STATE_ON_BATTERY \ - else PowerProfile.FULL + var new_profile: PowerProfile = ( + PowerProfile.BATTERY if raw_power_state == _POWER_STATE_ON_BATTERY else PowerProfile.FULL + ) if new_profile != power_profile: var old_profile := power_profile power_profile = new_profile @@ -304,45 +299,46 @@ func _poll_power() -> void: # -- Diagnostics -------------------------------------------------------------- + ## Returns all platform properties as a flat dictionary for inclusion in bug reports. func get_diagnostics() -> Dictionary: return { # Power - "power_profile": int(power_profile), - "raw_power_state": raw_power_state, - "battery_percent": battery_percent, + "power_profile": int(power_profile), + "raw_power_state": raw_power_state, + "battery_percent": battery_percent, # Memory - "free_memory_mb": free_memory_mb, - "total_memory_mb": total_memory_mb, + "free_memory_mb": free_memory_mb, + "total_memory_mb": total_memory_mb, # CPU - "cpu_name": cpu_name, - "cpu_logical_cores": cpu_logical_cores, + "cpu_name": cpu_name, + "cpu_logical_cores": cpu_logical_cores, # GPU - "gpu_name": gpu_name, - "gpu_vendor": gpu_vendor, - "gpu_api_version": gpu_api_version, - "gpu_type": gpu_type, - "gpu_driver_info": Array(gpu_driver_info), + "gpu_name": gpu_name, + "gpu_vendor": gpu_vendor, + "gpu_api_version": gpu_api_version, + "gpu_type": gpu_type, + "gpu_driver_info": Array(gpu_driver_info), # Platform - "platform_name": platform_name, - "os_version": os_version, - "distribution": distribution, - "is_sandboxed": is_sandboxed, + "platform_name": platform_name, + "os_version": os_version, + "distribution": distribution, + "is_sandboxed": is_sandboxed, # Display - "screen_count": screen_count, - "screen_size": {"x": screen_size.x, "y": screen_size.y}, - "screen_dpi": screen_dpi, + "screen_count": screen_count, + "screen_size": {"x": screen_size.x, "y": screen_size.y}, + "screen_dpi": screen_dpi, "screen_refresh_rate": screen_refresh_rate, - "display_scale": display_scale, + "display_scale": display_scale, # Locale - "locale": locale, - "locale_language": locale_language, + "locale": locale, + "locale_language": locale_language, # Paths - "user_data_dir": user_data_dir, - "config_dir": config_dir, - "benchmark_cache_path": benchmark_cache_path, - "install_dir": install_dir, - "executable_path": executable_path, - "cache_dir": cache_dir, - "model_dir": model_dir, + "user_data_dir": user_data_dir, + "config_dir": config_dir, + "benchmark_cache_path": benchmark_cache_path, + "install_dir": install_dir, + "executable_path": executable_path, + "cache_dir": cache_dir, + "model_dir": model_dir, } diff --git a/client/scripts/autoloads/session_manager.gd b/client/scripts/autoloads/session_manager.gd index 14e1ddd8e..996ca12c0 100644 --- a/client/scripts/autoloads/session_manager.gd +++ b/client/scripts/autoloads/session_manager.gd @@ -17,18 +17,26 @@ var _quit_dialog: ConfirmationDialog = null ## Returns the new game-id string. func new_game() -> String: var now := Time.get_datetime_dict_from_system() - var timestamp := "%04d%02d%02d-%02d%02d%02d" % [ - now.year, now.month, now.day, - now.hour, now.minute, now.second, - ] + var timestamp := ( + "%04d%02d%02d-%02d%02d%02d" + % [ + now.year, + now.month, + now.day, + now.hour, + now.minute, + now.second, + ] + ) var rng := RandomNumberGenerator.new() var hex_seed := "%06x" % (rng.randi() & 0xFFFFFF) var game_id := "%s-%s" % [timestamp, hex_seed] var save_path := SAVES_DIR + game_id + "/" var err := DirAccess.make_dir_recursive_absolute(save_path) if err != OK: - push_error("SessionManager: failed to create save dir %s: %s" % [ - save_path, error_string(err)]) + push_error( + "SessionManager: failed to create save dir %s: %s" % [save_path, error_string(err)] + ) return "" GameState.current_game_id = game_id @@ -71,15 +79,21 @@ func list_game_dirs() -> Array: var mtime: int = 0 if newest_save != "": mtime = FileAccess.get_modified_time(dir_path + newest_save) - results.append({ - "game_id": entry, - "modified_time": mtime, - "newest_save": newest_save, - }) + ( + results + . append( + { + "game_id": entry, + "modified_time": mtime, + "newest_save": newest_save, + } + ) + ) entry = dir.get_next() dir.list_dir_end() - results.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: - return a.modified_time > b.modified_time) + results.sort_custom( + func(a: Dictionary, b: Dictionary) -> bool: return a.modified_time > b.modified_time + ) return results @@ -105,11 +119,16 @@ func _do_quit_to_menu() -> void: # SimBridge._process() flushes the outbound buffer before teardown. if not GameState.current_game_id.is_empty(): var path := "user://saves/" + GameState.current_game_id + "/quicksave.sav" - SimBridge.send_input({ - "action": InputMapper.Action.SAVE_GAME, - "timestamp_msec": Time.get_ticks_msec(), - "action_data": {"path": path}, - }) + ( + SimBridge + . send_input( + { + "action": InputMapper.Action.SAVE_GAME, + "timestamp_msec": Time.get_ticks_msec(), + "action_data": {"path": path}, + } + ) + ) GameState.current_game_id = "" _navigate_to_menu.call_deferred() else: @@ -131,7 +150,12 @@ func _cleanup_quit_dialog() -> void: func _write_seed_file(save_path: String, seed: int) -> void: var file := FileAccess.open(save_path + "world_seed", FileAccess.WRITE) if file == null: - push_error("SessionManager: failed to write seed file: %s" % error_string(FileAccess.get_open_error())) + push_error( + ( + "SessionManager: failed to write seed file: %s" + % error_string(FileAccess.get_open_error()) + ) + ) return file.store_64(seed) @@ -152,7 +176,12 @@ func save_character_archetype(game_id: String, archetype: String) -> void: var save_path := SAVES_DIR + game_id + "/" var file := FileAccess.open(save_path + "character.txt", FileAccess.WRITE) if file == null: - push_error("SessionManager: failed to write character.txt: %s" % error_string(FileAccess.get_open_error())) + push_error( + ( + "SessionManager: failed to write character.txt: %s" + % error_string(FileAccess.get_open_error()) + ) + ) return file.store_string(archetype) diff --git a/client/scripts/autoloads/sim_bridge.gd b/client/scripts/autoloads/sim_bridge.gd index d8081039e..6efbbe5ff 100644 --- a/client/scripts/autoloads/sim_bridge.gd +++ b/client/scripts/autoloads/sim_bridge.gd @@ -34,37 +34,50 @@ var _retry_timer: float = 0.0 var _handshake_start_usec: int = 0 var _test_tick: int: - get: return harness.tick if harness else 0 + get: + return harness.tick if harness else 0 set(v): - if harness: harness.tick = v + if harness: + harness.tick = v var _test_player_pos: Vector2i: - get: return harness.player_pos if harness else Vector2i.ZERO + get: + return harness.player_pos if harness else Vector2i.ZERO set(v): - if harness: harness.player_pos = v + if harness: + harness.player_pos = v var _test_facing: String: - get: return harness.facing if harness else "North" + get: + return harness.facing if harness else "North" set(v): - if harness: harness.facing = v + if harness: + harness.facing = v var _test_in_dialogue: bool: - get: return harness.in_dialogue if harness else false + get: + return harness.in_dialogue if harness else false set(v): - if harness: harness.in_dialogue = v + if harness: + harness.in_dialogue = v var _test_gauntlet_mode: bool: - get: return harness.gauntlet_mode if harness else false + get: + return harness.gauntlet_mode if harness else false set(v): - if harness: harness.gauntlet_mode = v + if harness: + harness.gauntlet_mode = v var _test_npc_relationship: String: - get: return harness.npc_relationship if harness else "Unknown" + get: + return harness.npc_relationship if harness else "Unknown" set(v): - if harness: harness.npc_relationship = v + if harness: + harness.npc_relationship = v var _test_input_queue: Array: - get: return harness.input_queue if harness else [] + get: + return harness.input_queue if harness else [] func _ready() -> void: @@ -75,18 +88,23 @@ func _ready() -> void: # -- Test mode proxy API (backward compat for 13+ test files) ------------------ + func reset_test_state() -> void: - if harness: harness.reset() + if harness: + harness.reset() + func _test_snapshot() -> Dictionary: return harness.snapshot() + func _test_has_los(from: Vector2i, to: Vector2i) -> bool: return harness.has_los(from, to) # -- Connection lifecycle ------------------------------------------------------ + # Change connection state and emit signal func _set_state(new_state: ConnectionState) -> void: if state != new_state: @@ -94,6 +112,7 @@ func _set_state(new_state: ConnectionState) -> void: state = new_state connection_state_changed.emit(old_state, new_state) + # Connect to simulation server. # In test mode, immediately transitions to CONNECTED. # In live mode, spawns server subprocess and defers TCP connection to _process() @@ -125,6 +144,7 @@ func connect_to_sim() -> void: _retry_timer = 0.0 _bridge = null + # Disconnect from simulation server func disconnect_from_sim() -> void: if _bridge != null: @@ -136,15 +156,21 @@ func disconnect_from_sim() -> void: _connect_retries = 0 _set_state(ConnectionState.DISCONNECTED) + # Attempt TCP connection. Called from _process() during CONNECTING state. func _try_connect() -> void: _bridge = LocalBridge.new() var err := _bridge.connect_to_server("127.0.0.1", server_port) if err != OK: - push_warning("SimBridge: TCP connect attempt %d/%d failed: %s" % [ - _connect_retries + 1, MAX_CONNECT_RETRIES, error_string(err)]) + push_warning( + ( + "SimBridge: TCP connect attempt %d/%d failed: %s" + % [_connect_retries + 1, MAX_CONNECT_RETRIES, error_string(err)] + ) + ) _bridge = null + # Poll transport layer every frame (non-test mode only) func _process(delta: float) -> void: # gdlint:disable=max-returns if test_mode: @@ -158,7 +184,9 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns _retry_timer = 0.0 _connect_retries += 1 if _connect_retries > MAX_CONNECT_RETRIES: - push_error("SimBridge: TCP connection failed after %d retries" % MAX_CONNECT_RETRIES) + push_error( + "SimBridge: TCP connection failed after %d retries" % MAX_CONNECT_RETRIES + ) _set_state(ConnectionState.ERROR) return _try_connect() @@ -176,7 +204,9 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns # Connection attempt failed — retry _bridge = null if _connect_retries >= MAX_CONNECT_RETRIES: - push_error("SimBridge: TCP connection failed after %d retries" % MAX_CONNECT_RETRIES) + push_error( + "SimBridge: TCP connection failed after %d retries" % MAX_CONNECT_RETRIES + ) _set_state(ConnectionState.ERROR) StreamPeerTCP.STATUS_NONE: _bridge = null # Reset and retry @@ -191,7 +221,10 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns # Check connection dropped during handshake var bridge_status := _bridge.get_status() - if bridge_status == StreamPeerTCP.STATUS_ERROR or bridge_status == StreamPeerTCP.STATUS_NONE: + if ( + bridge_status == StreamPeerTCP.STATUS_ERROR + or bridge_status == StreamPeerTCP.STATUS_NONE + ): var reason := "Connection dropped during handshake" push_error("SimBridge: %s" % reason) handshake_failed.emit(reason) @@ -215,8 +248,11 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns # Decode HandshakeMessage: { "protocol_version": N } var decoded: Variant = Messagepack.decode(msg) - if decoded.status != null or not (decoded.value is Dictionary) \ - or not decoded.value.has("protocol_version"): + if ( + decoded.status != null + or not (decoded.value is Dictionary) + or not decoded.value.has("protocol_version") + ): var reason := "Handshake decode failed: malformed HandshakeMessage" push_error("SimBridge: %s" % reason) handshake_failed.emit(reason) @@ -226,8 +262,10 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns var server_version: int = decoded.value["protocol_version"] if server_version != Protocol.PROTOCOL_VERSION: - var reason := "Protocol version mismatch: server=%d, client=%d" % [ - server_version, Protocol.PROTOCOL_VERSION] + var reason := ( + "Protocol version mismatch: server=%d, client=%d" + % [server_version, Protocol.PROTOCOL_VERSION] + ) push_error("SimBridge: %s" % reason) handshake_failed.emit(reason) _bridge.disconnect_from_server() @@ -237,7 +275,10 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns # Send startup message with world_seed and character appearance (#175, D-010/D-029, #718). # Server blocks waiting for this before entering the tick loop. var startup_bytes := Protocol.encode_startup_message( - GameState.world_seed, GameState.character_archetype, GameState.character_visual_descriptor) + GameState.world_seed, + GameState.character_archetype, + GameState.character_visual_descriptor + ) if startup_bytes.size() > 0: var send_err := _bridge.send_message(startup_bytes) if send_err != OK: @@ -259,10 +300,15 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns _set_state(ConnectionState.CONNECTED) # #646: Request full settings dump on connect — hydrates GameState.ai_enhanced_dialogue_enabled # from server SQLite so the client reflects the authoritative persisted state (D-138). - _outbound_buffer.append({ - "tick": 0, - "action_name": "RequestAllSettings", - }) + ( + _outbound_buffer + . append( + { + "tick": 0, + "action_name": "RequestAllSettings", + } + ) + ) return if _bridge == null: @@ -289,7 +335,9 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns if err != OK: push_error("SimBridge: failed to send message: %s" % error_string(err)) else: - push_error("SimBridge: failed to batch-encode %d inputs (dropped)" % outbound.size()) + push_error( + "SimBridge: failed to batch-encode %d inputs (dropped)" % outbound.size() + ) StreamPeerTCP.STATUS_CONNECTING: pass # Should not happen in CONNECTED state StreamPeerTCP.STATUS_ERROR: @@ -304,6 +352,7 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns # -- Input / snapshot ---------------------------------------------------------- + # Send input to simulation server. # player_input: Dictionary with "action" (int from InputMapper.Action enum) and "timestamp_msec". # In test mode, inputs are delegated to the test harness. @@ -330,13 +379,14 @@ func send_input(player_input: Dictionary) -> Error: if action_name.is_empty(): return ERR_INVALID_PARAMETER var tick: int = GameState.current_tick - var entry: Dictionary = { "tick": tick, "action_name": action_name } + var entry: Dictionary = {"tick": tick, "action_name": action_name} var action_data: Variant = player_input.get("action_data") if action_data != null: entry["action_data"] = action_data _outbound_buffer.append(entry) return OK + # Poll for snapshot from simulation. # In test mode delegates to test harness. In live mode, returns the last decoded snapshot. func poll_snapshot() -> Variant: @@ -356,6 +406,7 @@ func poll_snapshot() -> Variant: return null + # Called by transport layer when raw bytes arrive from the server. # Latest-wins for positional state (stale frames are worthless), but one-shot # events (monologue, dialogue) are carried forward from overwritten snapshots @@ -367,12 +418,21 @@ func receive_bytes(bytes: PackedByteArray) -> void: return if _last_snapshot != null: # Carry forward one-shot events the client hasn't consumed yet. - if snapshot.get("current_monologue") == null and _last_snapshot.get("current_monologue") != null: + if ( + snapshot.get("current_monologue") == null + and _last_snapshot.get("current_monologue") != null + ): snapshot["current_monologue"] = _last_snapshot["current_monologue"] - if snapshot.get("current_dialogue") == null and _last_snapshot.get("current_dialogue") != null: + if ( + snapshot.get("current_dialogue") == null + and _last_snapshot.get("current_dialogue") != null + ): snapshot["current_dialogue"] = _last_snapshot["current_dialogue"] # #535: Carry forward one-shot dialogue events (arrays merge, scalar falls through) - if snapshot.get("dialogue_response") == null and _last_snapshot.get("dialogue_response") != null: + if ( + snapshot.get("dialogue_response") == null + and _last_snapshot.get("dialogue_response") != null + ): snapshot["dialogue_response"] = _last_snapshot["dialogue_response"] var old_conv_events: Array = _last_snapshot.get("conversation_events", []) if old_conv_events.size() > 0: @@ -386,10 +446,14 @@ func receive_bytes(bytes: PackedByteArray) -> void: if snapshot.get("save_result") == null and _last_snapshot.get("save_result") != null: snapshot["save_result"] = _last_snapshot["save_result"] # #646: Carry forward settings_response (one-shot, consumed by game_state apply_snapshot) - if snapshot.get("settings_response") == null and _last_snapshot.get("settings_response") != null: + if ( + snapshot.get("settings_response") == null + and _last_snapshot.get("settings_response") != null + ): snapshot["settings_response"] = _last_snapshot["settings_response"] _last_snapshot = snapshot + # Drain the outbound buffer. Returns raw input entries for batch encoding. func drain_outbound() -> Array[Dictionary]: var inputs = _outbound_buffer.duplicate() @@ -399,24 +463,39 @@ func drain_outbound() -> Array[Dictionary]: # -- Wire protocol mapping ----------------------------------------------------- + # Map InputMapper.Action enum values to wire-format action names (matching Rust PlayerAction). # OPEN_MENU is client-only — no Rust equivalent, not sent over the wire. static func action_enum_to_wire(action: int) -> String: match action: - InputMapper.Action.MOVE_NORTH: return "MoveNorth" - InputMapper.Action.MOVE_NORTHEAST: return "MoveNortheast" - InputMapper.Action.MOVE_EAST: return "MoveEast" - InputMapper.Action.MOVE_SOUTHEAST: return "MoveSoutheast" - InputMapper.Action.MOVE_SOUTH: return "MoveSouth" - InputMapper.Action.MOVE_SOUTHWEST: return "MoveSouthwest" - InputMapper.Action.MOVE_WEST: return "MoveWest" - InputMapper.Action.MOVE_NORTHWEST: return "MoveNorthwest" - InputMapper.Action.INTERACT: return "Interact" - InputMapper.Action.USE_PERCEPTION_MODE: return "UsePerceptionMode" - InputMapper.Action.PAUSE: return "Pause" - InputMapper.Action.UNPAUSE: return "Unpause" - InputMapper.Action.TOGGLE_STANCE_UP: return "ToggleStanceUp" - InputMapper.Action.TOGGLE_STANCE_DOWN: return "ToggleStanceDown" + InputMapper.Action.MOVE_NORTH: + return "MoveNorth" + InputMapper.Action.MOVE_NORTHEAST: + return "MoveNortheast" + InputMapper.Action.MOVE_EAST: + return "MoveEast" + InputMapper.Action.MOVE_SOUTHEAST: + return "MoveSoutheast" + InputMapper.Action.MOVE_SOUTH: + return "MoveSouth" + InputMapper.Action.MOVE_SOUTHWEST: + return "MoveSouthwest" + InputMapper.Action.MOVE_WEST: + return "MoveWest" + InputMapper.Action.MOVE_NORTHWEST: + return "MoveNorthwest" + InputMapper.Action.INTERACT: + return "Interact" + InputMapper.Action.USE_PERCEPTION_MODE: + return "UsePerceptionMode" + InputMapper.Action.PAUSE: + return "Pause" + InputMapper.Action.UNPAUSE: + return "Unpause" + InputMapper.Action.TOGGLE_STANCE_UP: + return "ToggleStanceUp" + InputMapper.Action.TOGGLE_STANCE_DOWN: + return "ToggleStanceDown" InputMapper.Action.OPEN_MENU: return "" # Client-only action, not part of wire protocol InputMapper.Action.BUG_REPORT: @@ -432,11 +511,11 @@ static func action_enum_to_wire(action: int) -> String: InputMapper.Action.DEBUG_COMMAND: return "DebugCommand" # #581: debug console command dispatch InputMapper.Action.CHANGE_SETTINGS: - return "ChangeSettings" # #646: persist setting to server SQLite (D-138) + return "ChangeSettings" # #646: persist setting to server SQLite (D-138) InputMapper.Action.REQUEST_ALL_SETTINGS: return "RequestAllSettings" # #646: unit variant — server sends full settings dump InputMapper.Action.DELETE_SETTING: - return "DeleteSetting" # #646: struct variant — delete setting by key + return "DeleteSetting" # #646: struct variant — delete setting by key _: push_warning("SimBridge: unknown action enum %s" % action) return "" diff --git a/client/scripts/autoloads/ui_strings.gd b/client/scripts/autoloads/ui_strings.gd index 00da195a0..c9bfbe200 100644 --- a/client/scripts/autoloads/ui_strings.gd +++ b/client/scripts/autoloads/ui_strings.gd @@ -9,9 +9,11 @@ const STRINGS_PATH: String = "res://data/ui-strings.yaml" var _strings: Dictionary = {} var _loaded: bool = false + func _ready() -> void: _load_strings() + func _load_strings() -> void: if not FileAccess.file_exists(STRINGS_PATH): push_warning("UIStrings: file not found: %s" % STRINGS_PATH) @@ -26,6 +28,7 @@ func _load_strings() -> void: _loaded = true print("UIStrings: loaded %d strings from %s" % [_strings.size(), STRINGS_PATH]) + ## Get a UI string by dotted key. Returns the key itself if not found. func get_text(key: String) -> String: if _strings.has(key): @@ -33,20 +36,24 @@ func get_text(key: String) -> String: push_warning("UIStrings: missing key '%s'" % key) return key + ## Check if a key exists. func has_key(key: String) -> bool: return _strings.has(key) + ## Get all keys. func get_all_keys() -> PackedStringArray: return PackedStringArray(_strings.keys()) + ## Reload from disk (useful for hot-reload during development). func reload() -> void: _strings.clear() _loaded = false _load_strings() + ## Parse YAML with arbitrary nesting depth. ## Returns flat Dictionary with dotted keys: { "section.sub.key": "value" }. ## Delegates to YamlParser.parse_flat() (#560). diff --git a/client/scripts/checklist/checklist_evaluator.gd b/client/scripts/checklist/checklist_evaluator.gd index 507041a3c..50fd23cd4 100644 --- a/client/scripts/checklist/checklist_evaluator.gd +++ b/client/scripts/checklist/checklist_evaluator.gd @@ -15,11 +15,11 @@ extends RefCounted ## ## Spec ref: D-030 (testability), checklist.schema.json (#497). -var _room_conditions: Array = [] # Conditions from per-room checklist -var _cross_conditions: Array = [] # Conditions from cross_room_checks.yaml -var _latched: Dictionary = {} # condition_id -> true (once met, stays met) +var _room_conditions: Array = [] # Conditions from per-room checklist +var _cross_conditions: Array = [] # Conditions from cross_room_checks.yaml +var _latched: Dictionary = {} # condition_id -> true (once met, stays met) var _current_room_id: String = "" -var _content_base: String = "" # Absolute path to content/ directory +var _content_base: String = "" # Absolute path to content/ directory var _loaded: bool = false @@ -54,8 +54,7 @@ func load_room(room_id: String) -> void: _latched = kept # Load per-room checklist - var room_path := _content_base.path_join( - "gauntlet/rooms/%s/checklist.yaml" % room_id) + var room_path := _content_base.path_join("gauntlet/rooms/%s/checklist.yaml" % room_id) var room_data := _load_checklist_file(room_path) if room_data.has("conditions"): _room_conditions = room_data["conditions"] @@ -90,12 +89,17 @@ func get_results() -> Array: var cid: String = cond.get("id", "") if cid.is_empty(): continue - results.append({ - "id": cid, - "description": cond.get("description", ""), - "condition_type": cond.get("condition_type", ""), - "met": _latched.has(cid), - }) + ( + results + . append( + { + "id": cid, + "description": cond.get("description", ""), + "condition_type": cond.get("condition_type", ""), + "met": _latched.has(cid), + } + ) + ) return results @@ -136,12 +140,17 @@ static func _warn_empty_ids(conditions: Array, path: String) -> void: for i in conditions.size(): if conditions[i].get("id", "").is_empty(): push_warning( - "ChecklistEvaluator: condition at index %d in %s has empty id — will be excluded from results" % [i, path]) + ( + "ChecklistEvaluator: condition at index %d in %s has empty id — will be excluded from results" + % [i, path] + ) + ) # -- Condition evaluation ------------------------------------------------------ -func _evaluate_condition(cond: Dictionary) -> bool: # gdlint:disable=max-returns + +func _evaluate_condition(cond: Dictionary) -> bool: # gdlint:disable=max-returns match cond.get("condition_type", ""): "player_near": return _eval_player_near(cond) @@ -225,6 +234,7 @@ func _eval_expected_interaction_verb(cond: Dictionary) -> bool: # -- Helpers ------------------------------------------------------------------- + func _find_entity(entity_id: int) -> bool: for entity in GameState.visible_entities: if not entity is Dictionary: @@ -236,6 +246,7 @@ func _find_entity(entity_id: int) -> bool: # -- YAML parsing -------------------------------------------------------------- + func _load_checklist_file(path: String) -> Dictionary: if not FileAccess.file_exists(path): return {} diff --git a/client/scripts/constants.gd b/client/scripts/constants.gd index a1302b4bf..e7cfdf978 100644 --- a/client/scripts/constants.gd +++ b/client/scripts/constants.gd @@ -12,35 +12,35 @@ const TILE_SIZE: int = 32 # All world content composited into one texture, then fog drawn over it. # Y-sort contract: all children of YSortGroup MUST use z_index = 0. # z_index is PRIMARY sort, y-position is SECONDARY (Godot #62715). -const Z_FLOOR: int = 0 # Floor tiles — ground plane -const Z_FLOOR_OBJECTS: int = 10 # Floor objects — cosmetic, ground shadows +const Z_FLOOR: int = 0 # Floor tiles — ground plane +const Z_FLOOR_OBJECTS: int = 10 # Floor objects — cosmetic, ground shadows # z:20-99 reserved: liquid surface (z:110), surface effects -const Z_YSORT: int = 100 # YSortGroup — furniture + entities + walls, all z:0 relative +const Z_YSORT: int = 100 # YSortGroup — furniture + entities + walls, all z:0 relative # z:110 reserved: liquid surface occlusion (alpha by depth) # z:150-199 reserved: ground VFX (smoke origins, gas pools, sparks) -const Z_AIRBORNE: int = 200 # Projectiles, low-flying objects (scale ~1.0) +const Z_AIRBORNE: int = 200 # Projectiles, low-flying objects (scale ~1.0) # z:250-299 reserved: mid-air VFX (rising smoke, floating particles) -const Z_OVERHEAD: int = 300 # Ceiling edges, upper structure, semi-transparent +const Z_OVERHEAD: int = 300 # Ceiling edges, upper structure, semi-transparent # z:325-349 reserved: ceiling VFX (smoke through ceiling) -const Z_HIGH_AIRBORNE: int = 350 # Above-ceiling flying (scale 1.03-1.30, alpha fades) -const Z_UPPER_CONTENT: int = 400 # Upper-floor entities (rare with fixed camera) +const Z_HIGH_AIRBORNE: int = 350 # Above-ceiling flying (scale 1.03-1.30, alpha fades) +const Z_UPPER_CONTENT: int = 400 # Upper-floor entities (rare with fixed camera) # z:500-899 reserved: edge cases -const Z_FOG: int = 900 # FogOverlay — OUTSIDE FogGroup, fog shader -const Z_FOG_ENTITIES: int = 950 # FogEntities — cognitive delay blobs/pings (D-059/D-060) +const Z_FOG: int = 900 # FogOverlay — OUTSIDE FogGroup, fog shader +const Z_FOG_ENTITIES: int = 950 # FogEntities — cognitive delay blobs/pings (D-059/D-060) # Lower floors: z:-100 per floor (floor-1: z:-100 to z:-1, floor-2: z:-200 to z:-101) # z:-75 to z:-51 reserved: lower floor VFX # # INSERT SCOPE (CanvasLayer 10) # Bloom-rendered, not affected by fog or camera transform. -const CANVAS_INSERT: int = 10 # CanvasLayer number for InsertOverlay +const CANVAS_INSERT: int = 10 # CanvasLayer number for InsertOverlay # # UI SCOPE (CanvasLayer 20) # HUD, monologue, cursor — always visible. -const CANVAS_UI: int = 20 # CanvasLayer number for UILayer +const CANVAS_UI: int = 20 # CanvasLayer number for UILayer # # MODAL SCOPE (CanvasLayer 30) # Full-screen overlays: pause, inventory modal, death screen. -const CANVAS_MODAL: int = 30 # CanvasLayer number for ModalLayer +const CANVAS_MODAL: int = 30 # CanvasLayer number for ModalLayer # # Rendering ceiling: 10 floors (25m) above current floor. # Above this: no sprites, ground shadows + environmental effects only. @@ -52,18 +52,18 @@ const VISIBLE_FLOOR_DEPTH: int = 2 # Color represents the player's RELATIONSHIP to the entity, not an objective property. # Phase 1: default colors mapped by entity kind (Player/Npc/Object/Terrain). # Phase 2 (#361): colors derived from RelationshipState via the knowledge graph. -const ENTITY_COLOR_UNKNOWN: Color = Color("#4a9ebb") # Unknown/Neutral — cool teal +const ENTITY_COLOR_UNKNOWN: Color = Color("#4a9ebb") # Unknown/Neutral — cool teal const ENTITY_COLOR_FRIENDLY: Color = Color("#6bc9a6") # Known/Friendly — soft green -const ENTITY_COLOR_POI: Color = Color("#e8c547") # Person of Interest — warm amber -const ENTITY_COLOR_HOSTILE: Color = Color("#d45d5d") # Hostile/Dangerous — muted red -const ENTITY_COLOR_OBJECT: Color = Color("#8b8ba0") # Static objects — muted grey -const ENTITY_COLOR_PLAYER: Color = Color("#e0e8ff") # Player character (detective) +const ENTITY_COLOR_POI: Color = Color("#e8c547") # Person of Interest — warm amber +const ENTITY_COLOR_HOSTILE: Color = Color("#d45d5d") # Hostile/Dangerous — muted red +const ENTITY_COLOR_OBJECT: Color = Color("#8b8ba0") # Static objects — muted grey +const ENTITY_COLOR_PLAYER: Color = Color("#e0e8ff") # Player character (detective) # D-048/D-056: Insert-styled UI color palette # Used by dialogue box, interaction list, radial menu, and other diegetic insert UI. -const INSERT_COLOR_TEXT: Color = Color("#c8d0e0") # Default insert text — white-blue -const INSERT_COLOR_HOVER: Color = Color("#e8c547") # Hover/highlight — amber POI -const INSERT_COLOR_ACTIVE: Color = Color("#6bc9a6") # Active/pressed — friendly green +const INSERT_COLOR_TEXT: Color = Color("#c8d0e0") # Default insert text — white-blue +const INSERT_COLOR_HOVER: Color = Color("#e8c547") # Hover/highlight — amber POI +const INSERT_COLOR_ACTIVE: Color = Color("#6bc9a6") # Active/pressed — friendly green # D-015: Peripheral vision dimming const PERIPHERAL_ALPHA: float = 0.5 @@ -87,29 +87,41 @@ const CAMERA_DEFAULT_ZOOM: Vector2 = Vector2(2.0, 2.0) const CAMERA_SMOOTHING_SPEED: float = 8.0 # #517: Implant UI font color grading — avoid pure white, project through a lens -const IMPLANT_TEXT_COLOR: Color = Color("#E0F7FA") # Cyan-white — primary text -const IMPLANT_TEXT_DIM: Color = Color("#9EBFC4") # Dimmed variant — secondary text -const IMPLANT_PULSE_MIN: float = 0.85 # Alpha pulse floor -const IMPLANT_PULSE_MAX: float = 1.0 # Alpha pulse ceiling -const IMPLANT_PULSE_PERIOD: float = 2.5 # Seconds per pulse cycle +const IMPLANT_TEXT_COLOR: Color = Color("#E0F7FA") # Cyan-white — primary text +const IMPLANT_TEXT_DIM: Color = Color("#9EBFC4") # Dimmed variant — secondary text +const IMPLANT_PULSE_MIN: float = 0.85 # Alpha pulse floor +const IMPLANT_PULSE_MAX: float = 1.0 # Alpha pulse ceiling +const IMPLANT_PULSE_PERIOD: float = 2.5 # Seconds per pulse cycle + # D-033 color lookup by relationship string (#521) static func color_for_relationship(relationship: String) -> Color: match relationship: - "Friendly": return ENTITY_COLOR_FRIENDLY - "PersonOfInterest": return ENTITY_COLOR_POI - "Hostile": return ENTITY_COLOR_HOSTILE - "Unknown": return ENTITY_COLOR_UNKNOWN - _: return ENTITY_COLOR_UNKNOWN + "Friendly": + return ENTITY_COLOR_FRIENDLY + "PersonOfInterest": + return ENTITY_COLOR_POI + "Hostile": + return ENTITY_COLOR_HOSTILE + "Unknown": + return ENTITY_COLOR_UNKNOWN + _: + return ENTITY_COLOR_UNKNOWN + # D-033 color lookup by entity data — uses relationship for NPCs (#521) static func color_for_entity_kind(entity_data: Dictionary) -> Color: var kind_variant: String = entity_data.get("kind", {}).get("variant", "") match kind_variant: - "Player": return ENTITY_COLOR_PLAYER - "Object", "Terrain": return ENTITY_COLOR_OBJECT - "Npc": return color_for_relationship(entity_data.get("relationship", "Unknown")) - _: return ENTITY_COLOR_OBJECT + "Player": + return ENTITY_COLOR_PLAYER + "Object", "Terrain": + return ENTITY_COLOR_OBJECT + "Npc": + return color_for_relationship(entity_data.get("relationship", "Unknown")) + _: + return ENTITY_COLOR_OBJECT + # D-031: Format game-minutes (0..1439) as station local time string "HH:MM". static func format_game_time(time_of_day: int) -> String: diff --git a/client/scripts/dialogue_coordinator.gd b/client/scripts/dialogue_coordinator.gd index f092c8b61..e06c8a229 100644 --- a/client/scripts/dialogue_coordinator.gd +++ b/client/scripts/dialogue_coordinator.gd @@ -13,7 +13,7 @@ var journal_panel: Node = null # 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_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 @@ -44,6 +44,7 @@ func connect_signals() -> void: # -- 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: @@ -102,17 +103,24 @@ func consume_dialogue_response() -> void: # -- 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, - }, - }) + ( + 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) @@ -141,14 +149,21 @@ func on_dialogue_unpause_requested() -> void: # 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", - }, - }) + ( + 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. diff --git a/client/scripts/main.gd b/client/scripts/main.gd index cdbafbce2..968fcd2ab 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -5,10 +5,10 @@ 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 +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 fog_entities = $World/FogEntities # D-059/D-060: cognitive delay fog visualization @@ -30,11 +30,11 @@ var _dialogue: DialogueCoordinator # #775: dialogue consumers + signal han @onready var journal_panel = $InsertOverlay/JournalPanel # #264: knowledge journal (D-041) @onready var debug_overlay = $UILayer/DebugOverlay # #511: F3 debug overlay @onready var bug_report_dialog = $ModalLayer/BugReportDialog # #495: F12 WRONG button -@onready var settings_dialog = $ModalLayer/SettingsDialog # #528: audio settings (ESC/OPEN_MENU) -@onready var loading_screen = $ModalLayer/LoadingScreen # #257: blocking overlay during load -@onready var debug_console = $ModalLayer/DebugConsole # #581: tilde debug console -@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 settings_dialog = $ModalLayer/SettingsDialog # #528: audio settings (ESC/OPEN_MENU) +@onready var loading_screen = $ModalLayer/LoadingScreen # #257: blocking overlay during load +@onready var debug_console = $ModalLayer/DebugConsole # #581: tilde debug console +@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) func _ready() -> void: @@ -64,31 +64,53 @@ func _ready() -> void: # D-170: Register HUD nodes into visibility groups # Gameplay group — hidden when implant panels are open - for node in [hud, minimap, stance_indicator, - interaction_prompt, interaction_list, inventory_grid, news_ticker, - examine_display, world_radial]: + for node in [ + hud, + minimap, + stance_indicator, + interaction_prompt, + interaction_list, + inventory_grid, + news_ticker, + examine_display, + world_radial + ]: if node and node is Control: HudGroups.register(node, "gameplay") # #775: Initialize extracted components - _consumers = SnapshotConsumers.new().init({ - "monologue_display": monologue_display, - "dialogue_box": dialogue_box, - "examine_display": examine_display, - "loading_screen": loading_screen, - "debug_console": debug_console, - "cursor_renderer": cursor_renderer, - "interaction_list": interaction_list, - "interaction_prompt": interaction_prompt, - "minimap": minimap, - "star_map": star_map, - }, _screen_flash) + _consumers = ( + SnapshotConsumers + . new() + . init( + { + "monologue_display": monologue_display, + "dialogue_box": dialogue_box, + "examine_display": examine_display, + "loading_screen": loading_screen, + "debug_console": debug_console, + "cursor_renderer": cursor_renderer, + "interaction_list": interaction_list, + "interaction_prompt": interaction_prompt, + "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 = ( + 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 @@ -245,13 +267,17 @@ func _process(delta: float) -> void: # #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: gauntlet_hud.finalize() # #257: Deferred LOAD_GAME dispatch — fires once when SimBridge reaches CONNECTED. -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: return if SimBridge.connection_state_changed.is_connected(_on_sim_connected_for_load): @@ -264,11 +290,16 @@ func _dispatch_pending_load() -> void: if load_path.is_empty(): return GameState.pending_load_path = "" - var err := SimBridge.send_input({ - "action": InputMapper.Action.LOAD_GAME, - "timestamp_msec": Time.get_ticks_msec(), - "action_data": {"path": load_path}, - }) + var err := ( + SimBridge + . send_input( + { + "action": InputMapper.Action.LOAD_GAME, + "timestamp_msec": Time.get_ticks_msec(), + "action_data": {"path": load_path}, + } + ) + ) if err != OK: push_error("main.gd: failed to send LOAD_GAME after connection — %s" % error_string(err)) if loading_screen: diff --git a/client/scripts/protocol/local_bridge.gd b/client/scripts/protocol/local_bridge.gd index 601e0d6d5..0eb52cbce 100644 --- a/client/scripts/protocol/local_bridge.gd +++ b/client/scripts/protocol/local_bridge.gd @@ -86,15 +86,23 @@ func _try_extract_message() -> PackedByteArray: if _pending_length < 0: if _read_buffer.size() < 4: return PackedByteArray() - _pending_length = (_read_buffer[0] << 24) | (_read_buffer[1] << 16) | \ - (_read_buffer[2] << 8) | _read_buffer[3] + _pending_length = ( + (_read_buffer[0] << 24) + | (_read_buffer[1] << 16) + | (_read_buffer[2] << 8) + | _read_buffer[3] + ) _read_buffer = _read_buffer.slice(4) if _pending_length > MAX_MESSAGE_SIZE: # Stream is corrupt — we can't find the next valid frame boundary. # Disconnect rather than silently discarding valid buffered data. push_error( - "LocalBridge: incoming message too large: %d bytes (max %d) — disconnecting" % [_pending_length, MAX_MESSAGE_SIZE]) + ( + "LocalBridge: incoming message too large: %d bytes (max %d) — disconnecting" + % [_pending_length, MAX_MESSAGE_SIZE] + ) + ) _corrupt = true _pending_length = -1 _read_buffer.clear() @@ -129,6 +137,7 @@ func reset() -> void: # These exist for unit tests that verify framing logic without a live TCP # connection. Not used in production code paths. + ## Encode a payload into a framed byte array: [4-byte BE length][payload]. static func frame_encode(payload: PackedByteArray) -> PackedByteArray: var len := payload.size() diff --git a/client/scripts/protocol/protocol.gd b/client/scripts/protocol/protocol.gd index 203a412d6..d3d14db4a 100644 --- a/client/scripts/protocol/protocol.gd +++ b/client/scripts/protocol/protocol.gd @@ -14,9 +14,9 @@ extends Node ## v20: adds settings_response field to ObserverSnapshot (#627, D-138). const PROTOCOL_VERSION: int = 20 - # -- Decode: bytes from server → GDScript types -------------------------------- + ## Decode an ObserverSnapshot from MessagePack bytes. ## Returns decoded snapshot Dictionary or null on error. ## v2 fields (version, game_time, player_facing, visible_tiles) default to null/empty @@ -36,7 +36,11 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: var version: Variant = raw.get("version") if version != PROTOCOL_VERSION: push_error( - "Protocol: version mismatch (got %s, expected %s). Server and client are out of sync." % [version, PROTOCOL_VERSION]) + ( + "Protocol: version mismatch (got %s, expected %s). Server and client are out of sync." + % [version, PROTOCOL_VERSION] + ) + ) return null var entities: Array[Dictionary] = [] @@ -51,7 +55,11 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: if dropped > 0: push_error( - "Protocol: %d/%d entities failed to decode (D-010 information boundary violation)" % [dropped, raw_entities.size()]) + ( + "Protocol: %d/%d entities failed to decode (D-010 information boundary violation)" + % [dropped, raw_entities.size()] + ) + ) # GDScript int is signed 64-bit. Rust tick is u64 but will not exceed 2^63 # in any realistic scenario (would require ~29 billion years at 10 ticks/game-minute per D-031). @@ -71,7 +79,12 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: var raw_vtiles: Variant = raw.get("visible_tiles") if raw_vtiles is Array: for raw_tile in raw_vtiles: - if raw_tile is Dictionary and raw_tile.has("x") and raw_tile.has("y") and raw_tile.has("z"): + if ( + raw_tile is Dictionary + and raw_tile.has("x") + and raw_tile.has("y") + and raw_tile.has("z") + ): var tile_entry := { "x": int(raw_tile["x"]), "y": int(raw_tile["y"]), @@ -119,11 +132,16 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: if raw_inventory is Array: for raw_item in raw_inventory: if raw_item is Dictionary and raw_item.has("item_id") and raw_item.has("name"): - player_inventory.append({ - "item_id": int(raw_item["item_id"]), - "name": str(raw_item["name"]), - "slot": int(raw_item.get("slot", 0)), - }) + ( + player_inventory + . append( + { + "item_id": int(raw_item["item_id"]), + "name": str(raw_item["name"]), + "slot": int(raw_item.get("slot", 0)), + } + ) + ) # v7: pending_recognitions (#431, D-059/D-060) — cognitive delay fog entities # Bounded: server sends at most ~50 pending recognitions per snapshot (practical limit @@ -135,17 +153,32 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: var count := 0 for raw_pr in raw_recognitions: if count >= MAX_PENDING_RECOGNITIONS: - push_warning("Protocol: pending_recognitions truncated at %d entries" % MAX_PENDING_RECOGNITIONS) + push_warning( + ( + "Protocol: pending_recognitions truncated at %d entries" + % MAX_PENDING_RECOGNITIONS + ) + ) break - if raw_pr is Dictionary and raw_pr.has("entity_id") and raw_pr.has("x") and raw_pr.has("y"): - pending_recognitions.append({ - "entity_id": int(raw_pr["entity_id"]), - "x": float(raw_pr["x"]), - "y": float(raw_pr["y"]), - "z": int(raw_pr.get("z", 0)), - "remaining_ticks": int(raw_pr.get("remaining_ticks", 0)), - "total_delay_ticks": int(raw_pr.get("total_delay_ticks", 1)), - }) + if ( + raw_pr is Dictionary + and raw_pr.has("entity_id") + and raw_pr.has("x") + and raw_pr.has("y") + ): + ( + pending_recognitions + . append( + { + "entity_id": int(raw_pr["entity_id"]), + "x": float(raw_pr["x"]), + "y": float(raw_pr["y"]), + "z": int(raw_pr.get("z", 0)), + "remaining_ticks": int(raw_pr.get("remaining_ticks", 0)), + "total_delay_ticks": int(raw_pr.get("total_delay_ticks", 1)), + } + ) + ) count += 1 # v7: current_dialogue (#435, D-061/D-062) — NPC speech + player response options @@ -159,12 +192,17 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: if raw_options is Array: for raw_opt in raw_options: if raw_opt is Dictionary and raw_opt.has("text"): - dialogue_options.append({ - "text": str(raw_opt["text"]), - "response_id": str(raw_opt.get("response_id", "")), - "priority": int(raw_opt.get("priority", 0)), - "confrontation": bool(raw_opt.get("confrontation", false)), - }) + ( + dialogue_options + . append( + { + "text": str(raw_opt["text"]), + "response_id": str(raw_opt.get("response_id", "")), + "priority": int(raw_opt.get("priority", 0)), + "confrontation": bool(raw_opt.get("confrontation", false)), + } + ) + ) current_dialogue = { "npc_name": str(raw_dialogue.get("npc_name", "")), "npc_entity_id": int(raw_dialogue.get("npc_entity_id", -1)), @@ -190,13 +228,18 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: if raw_conv_events is Array: for raw_ce in raw_conv_events: if raw_ce is Dictionary and raw_ce.has("occluded_line"): - conversation_events.append({ - "speaker_id": int(raw_ce.get("speaker_id", 0)), - "target_id": int(raw_ce.get("target_id", 0)), - "speaker_name": str(raw_ce.get("speaker_name", "")), - "target_name": str(raw_ce.get("target_name", "")), - "occluded_line": str(raw_ce["occluded_line"]), - }) + ( + conversation_events + . append( + { + "speaker_id": int(raw_ce.get("speaker_id", 0)), + "target_id": int(raw_ce.get("target_id", 0)), + "speaker_name": str(raw_ce.get("speaker_name", "")), + "target_name": str(raw_ce.get("target_name", "")), + "occluded_line": str(raw_ce["occluded_line"]), + } + ) + ) # v9: conversation_ended (#535, D-078) — pairs whose conversation ended this tick. var conversation_ended: Array = [] @@ -204,10 +247,15 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: if raw_conv_ended is Array: for raw_end in raw_conv_ended: if raw_end is Dictionary: - conversation_ended.append({ - "speaker_id": int(raw_end.get("speaker_id", 0)), - "target_id": int(raw_end.get("target_id", 0)), - }) + ( + conversation_ended + . append( + { + "speaker_id": int(raw_end.get("speaker_id", 0)), + "target_id": int(raw_end.get("target_id", 0)), + } + ) + ) # v14: poi_list (#151) — discovered POIs for minimap rendering. # Each entry: {poi_id, name, x, y, z, poi_category}. Positions in sim tile coords. @@ -215,15 +263,26 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: var raw_pois: Variant = raw.get("poi_list") if raw_pois is Array: for raw_poi in raw_pois: - if raw_poi is Dictionary and raw_poi.has("poi_id") and raw_poi.has("x") and raw_poi.has("y"): - poi_list.append({ - "poi_id": str(raw_poi["poi_id"]), - "name": str(raw_poi.get("name", "")), - "x": int(raw_poi["x"]), - "y": int(raw_poi["y"]), - "z": int(raw_poi.get("z", 0)), - "poi_category": str(raw_poi.get("poi_category", raw_poi.get("category", "Location"))), - }) + if ( + raw_poi is Dictionary + and raw_poi.has("poi_id") + and raw_poi.has("x") + and raw_poi.has("y") + ): + ( + poi_list + . append( + { + "poi_id": str(raw_poi["poi_id"]), + "name": str(raw_poi.get("name", "")), + "x": int(raw_poi["x"]), + "y": int(raw_poi["y"]), + "z": int(raw_poi.get("z", 0)), + "poi_category": + str(raw_poi.get("poi_category", raw_poi.get("category", "Location"))), + } + ) + ) # v14: examine_result (#174, #242) — character-filtered observation text. # {entity_id, text, confidence} or null. Auto-dismisses on client after 4-6 seconds. @@ -268,9 +327,14 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: if raw_tce is Array: for raw_ev in raw_tce: if raw_ev is Dictionary and raw_ev.has("triangle_id"): - triangle_crisis_events.append({ - "triangle_id": int(raw_ev["triangle_id"]), - }) + ( + triangle_crisis_events + . append( + { + "triangle_id": int(raw_ev["triangle_id"]), + } + ) + ) # v20: settings_response (#627, D-138) — server ack after ChangeSettings / full settings dump # after RequestAllSettings. kind = "full"|"ack". "full": settings array [{key, value}]. @@ -285,10 +349,15 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: if raw_sr_settings is Array: for raw_s in raw_sr_settings: if raw_s is Dictionary and raw_s.has("key"): - sr_settings.append({ - "key": str(raw_s["key"]), - "value": raw_s.get("value"), - }) + ( + sr_settings + . append( + { + "key": str(raw_s["key"]), + "value": raw_s.get("value"), + } + ) + ) settings_response = {"kind": "full", "settings": sr_settings} elif sr_kind == "ack": settings_response = { @@ -338,27 +407,37 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: if raw_kg_entities is Array: for raw_ke in raw_kg_entities: if raw_ke is Dictionary and raw_ke.has("entity_id"): - kg_entities.append({ - "entity_id": int(raw_ke["entity_id"]), - "name": str(raw_ke.get("name", "Unknown")), - "confidence": str(raw_ke.get("confidence", "Suspects")), - "source": str(raw_ke.get("source", "")), - "state": str(raw_ke.get("state", "Active")), - "relationship": str(raw_ke.get("relationship", "Unknown")), - "last_observed_tick": int(raw_ke.get("last_observed_tick", 0)), - }) + ( + kg_entities + . append( + { + "entity_id": int(raw_ke["entity_id"]), + "name": str(raw_ke.get("name", "Unknown")), + "confidence": str(raw_ke.get("confidence", "Suspects")), + "source": str(raw_ke.get("source", "")), + "state": str(raw_ke.get("state", "Active")), + "relationship": str(raw_ke.get("relationship", "Unknown")), + "last_observed_tick": int(raw_ke.get("last_observed_tick", 0)), + } + ) + ) var kg_facts: Array = [] var raw_kg_facts: Variant = raw_pk.get("facts") if raw_kg_facts is Array: for raw_kf in raw_kg_facts: if raw_kf is Dictionary and raw_kf.has("fact_id"): - kg_facts.append({ - "fact_id": str(raw_kf["fact_id"]), - "confidence": str(raw_kf.get("confidence", "Suspects")), - "source": str(raw_kf.get("source", "")), - "state": str(raw_kf.get("state", "Active")), - "acquired_tick": int(raw_kf.get("acquired_tick", 0)), - }) + ( + kg_facts + . append( + { + "fact_id": str(raw_kf["fact_id"]), + "confidence": str(raw_kf.get("confidence", "Suspects")), + "source": str(raw_kf.get("source", "")), + "state": str(raw_kf.get("state", "Active")), + "acquired_tick": int(raw_kf.get("acquired_tick", 0)), + } + ) + ) player_knowledge = { "entities": kg_entities, "facts": kg_facts, @@ -396,8 +475,13 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: ## Decode a single VisibleEntity from a raw msgpack map. static func _decode_entity(raw: Dictionary) -> Variant: - if not raw.has("entity_id") or not raw.has("x") or not raw.has("y") \ - or not raw.has("z") or not raw.has("kind"): + if ( + not raw.has("entity_id") + or not raw.has("x") + or not raw.has("y") + or not raw.has("z") + or not raw.has("kind") + ): push_warning("Protocol: entity missing required fields: %s" % str(raw.keys())) return null @@ -485,23 +569,25 @@ static func _decode_verb_option(raw) -> Variant: ## Returns { "variant": String, "data": Variant } in both cases. static func _decode_enum_variant(raw) -> Dictionary: if raw is String: - return { "variant": raw, "data": null } + return {"variant": raw, "data": null} if raw is Dictionary and raw.size() == 1: var variant_name: String = raw.keys()[0] - return { "variant": variant_name, "data": raw[variant_name] } + return {"variant": variant_name, "data": raw[variant_name]} push_warning("Protocol: unexpected enum encoding: %s" % str(raw)) - return { "variant": "Unknown", "data": raw } + return {"variant": "Unknown", "data": raw} # -- Encode: GDScript types → bytes to server ---------------------------------- + ## Encode a StartupMessage to MessagePack bytes (#175, #588, #718). ## Sent by the client immediately after handshake validation. ## Server reads this to initialize SimRng (D-010, D-029) and select monologue pool (D-032). ## character_archetype: "detective" → "Detective", "smuggler" → "Smuggler" (server enum variant). ## character_visual: optional CharacterVisualDescriptor — included as "character_visual_descriptor" dict. static func encode_startup_message( - world_seed: int, character_archetype: String = "detective", character_visual: Variant = null) -> PackedByteArray: + world_seed: int, character_archetype: String = "detective", character_visual: Variant = null +) -> PackedByteArray: # Map client lowercase archetype string to server PascalCase enum variant. # Explicit match prevents unknown strings silently reaching the server as # garbage enum values — fail loudly and fall back to "Detective". @@ -512,7 +598,12 @@ static func encode_startup_message( "smuggler": archetype_variant = "Smuggler" _: - push_error("Protocol: unknown character_archetype '%s' — defaulting to 'Detective'" % character_archetype) + push_error( + ( + "Protocol: unknown character_archetype '%s' — defaulting to 'Detective'" + % character_archetype + ) + ) archetype_variant = "Detective" var msg := { "world_seed": world_seed, @@ -531,7 +622,9 @@ static func encode_startup_message( ## action_name: one of "MoveNorth", "MoveSouth", "MoveEast", "MoveWest", ## "Interact", "UsePerceptionMode", "Pause", "Unpause" ## action_data: null for unit variants, String for UsePerceptionMode -static func encode_player_input(tick: int, action_name: String, action_data: Variant = null) -> PackedByteArray: +static func encode_player_input( + tick: int, action_name: String, action_data: Variant = null +) -> PackedByteArray: var action_value: Variant = _encode_action(action_name, action_data) var input := { @@ -555,10 +648,15 @@ static func encode_player_inputs(inputs: Array) -> PackedByteArray: for input in inputs: var action_name: String = input["action_name"] var action_data: Variant = input.get("action_data") - wire_inputs.append({ - "tick": input["tick"], - "action": _encode_action(action_name, action_data), - }) + ( + wire_inputs + . append( + { + "tick": input["tick"], + "action": _encode_action(action_name, action_data), + } + ) + ) var result = Messagepack.encode(wire_inputs) if result.status != null: @@ -573,10 +671,10 @@ static func encode_player_inputs(inputs: Array) -> PackedByteArray: ## Unit variants (MoveNorth, Pause, etc.) encode as bare strings. static func _encode_action(action_name: String, action_data: Variant) -> Variant: if action_data != null: - return { action_name: action_data } + return {action_name: action_data} # Interact is a struct variant — server expects named fields, not a bare string if action_name == "Interact": - return { "Interact": { "target_entity_id": null, "verb": null } } + return {"Interact": {"target_entity_id": null, "verb": null}} return action_name @@ -586,11 +684,13 @@ static func _encode_action(action_name: String, action_data: Variant) -> Variant ## queuing via SimBridge._outbound_buffer. ## The server receives this as a PlayerAction::ChangeSettings variant. static func encode_change_settings(enabled: bool) -> PackedByteArray: - var entries: Array = [{ - "tick": 0, - "action_name": "ChangeSettings", - "action_data": {"ai_enhanced_dialogue": enabled}, - }] + var entries: Array = [ + { + "tick": 0, + "action_name": "ChangeSettings", + "action_data": {"ai_enhanced_dialogue": enabled}, + } + ] var result = Messagepack.encode(entries) if result.status != null: push_error("Protocol: encode_change_settings failed: %s" % result.status) diff --git a/client/scripts/protocol/test_harness.gd b/client/scripts/protocol/test_harness.gd index 8808f8cb9..cf6cbffdf 100644 --- a/client/scripts/protocol/test_harness.gd +++ b/client/scripts/protocol/test_harness.gd @@ -7,14 +7,34 @@ extends RefCounted const _WALLS: Array = [ # Room walls (8x8 room from (7,7) to (14,14)) - Vector2i(7,7), Vector2i(8,7), Vector2i(9,7), Vector2i(10,7), - Vector2i(11,7), Vector2i(12,7), Vector2i(13,7), Vector2i(14,7), - Vector2i(7,14), Vector2i(8,14), Vector2i(9,14), Vector2i(10,14), - Vector2i(11,14), Vector2i(12,14), Vector2i(13,14), Vector2i(14,14), - Vector2i(7,8), Vector2i(7,9), Vector2i(7,10), Vector2i(7,11), - Vector2i(7,12), Vector2i(7,13), - Vector2i(14,8), Vector2i(14,9), Vector2i(14,10), Vector2i(14,11), - Vector2i(14,12), Vector2i(14,13), + Vector2i(7, 7), + Vector2i(8, 7), + Vector2i(9, 7), + Vector2i(10, 7), + Vector2i(11, 7), + Vector2i(12, 7), + Vector2i(13, 7), + Vector2i(14, 7), + Vector2i(7, 14), + Vector2i(8, 14), + Vector2i(9, 14), + Vector2i(10, 14), + Vector2i(11, 14), + Vector2i(12, 14), + Vector2i(13, 14), + Vector2i(14, 14), + Vector2i(7, 8), + Vector2i(7, 9), + Vector2i(7, 10), + Vector2i(7, 11), + Vector2i(7, 12), + Vector2i(7, 13), + Vector2i(14, 8), + Vector2i(14, 9), + Vector2i(14, 10), + Vector2i(14, 11), + Vector2i(14, 12), + Vector2i(14, 13), # Interior wall blocking NPC Vector2i(12, 10), ] @@ -48,6 +68,7 @@ func process_facing(new_facing: String) -> void: # -- Snapshot generation ------------------------------------------------------- + func snapshot() -> Dictionary: tick += 1 @@ -76,42 +97,60 @@ func snapshot() -> Dictionary: var py := player_pos.y # Build entities — player always visible - var entities: Array = [{ - "entity_id": 1, - "x": float(px), - "y": float(py), - "z": 0, - "kind": { "variant": "Player", "data": null }, - "visibility": "Forward", - }] + var entities: Array = [ + { + "entity_id": 1, + "x": float(px), + "y": float(py), + "z": 0, + "kind": {"variant": "Player", "data": null}, + "visibility": "Forward", + } + ] # NPC at (12, 9) — visible if within range and not blocked by wall at (12, 10) var npc_pos := Vector2i(12, 9) var npc_dist := absi(px - npc_pos.x) + absi(py - npc_pos.y) if npc_dist <= 4 and has_los(Vector2i(px, py), npc_pos): var sector: String = "Forward" if npc_pos.y <= py else "Peripheral" - entities.append({ - "entity_id": 2, - "x": float(npc_pos.x), - "y": float(npc_pos.y), - "z": 0, - "kind": { "variant": "Npc", "data": null }, - "visibility": sector, - "relationship": npc_relationship, - }) + ( + entities + . append( + { + "entity_id": 2, + "x": float(npc_pos.x), + "y": float(npc_pos.y), + "z": 0, + "kind": {"variant": "Npc", "data": null}, + "visibility": sector, + "relationship": npc_relationship, + } + ) + ) # v4: nearby_interactions when NPC is nearby and visible (#404/#405) var nearby: Array = [] if npc_dist <= 2 and has_los(Vector2i(px, py), npc_pos): - nearby.append({ - "entity_id": 2, - "entity_type": "Npc", - "distance": npc_dist, - "verbs": [ - {"kind": "Talk", "label": "Talk", "priority": 1, "available": true}, - {"kind": "ExamineNpc", "label": "Observe", "priority": 2, "available": true}, - ], - }) + ( + nearby + . append( + { + "entity_id": 2, + "entity_type": "Npc", + "distance": npc_dist, + "verbs": + [ + {"kind": "Talk", "label": "Talk", "priority": 1, "available": true}, + { + "kind": "ExamineNpc", + "label": "Observe", + "priority": 2, + "available": true + }, + ], + } + ) + ) # v5: monologue on first tick (#414) var monologue: Variant = null @@ -128,11 +167,28 @@ func snapshot() -> Dictionary: dialogue = { "npc_name": "Kael", "npc_entity_id": 2, - "speech": "Haven't seen you around the transit hub before. You new to Sova, or just passing through?", - "options": [ - {"text": "Just arrived. Still getting my bearings.", "response_id": "kael_greet_01", "priority": 1, "confrontation": false}, # gdlint:ignore = max-line-length - {"text": "Passing through. Know where I can find work?", "response_id": "kael_greet_02", "priority": 2, "confrontation": false}, # gdlint:ignore = max-line-length - {"text": "I saw you near the cargo bay last night.", "response_id": "kael_confront_01", "priority": 3, "confrontation": true}, # gdlint:ignore = max-line-length + "speech": + "Haven't seen you around the transit hub before. You new to Sova, or just passing through?", + "options": + [ + { + "text": "Just arrived. Still getting my bearings.", + "response_id": "kael_greet_01", + "priority": 1, + "confrontation": false + }, # gdlint:ignore = max-line-length + { + "text": "Passing through. Know where I can find work?", + "response_id": "kael_greet_02", + "priority": 2, + "confrontation": false + }, # gdlint:ignore = max-line-length + { + "text": "I saw you near the cargo bay last night.", + "response_id": "kael_confront_01", + "priority": 3, + "confrontation": true + }, # gdlint:ignore = max-line-length ], } @@ -142,26 +198,55 @@ func snapshot() -> Dictionary: if cycle_pos < 6: var total_delay := 6 var remaining := total_delay - cycle_pos - pending_recs.append({ - "entity_id": 100, - "x": 13.5, - "y": 12.5, - "z": 0, - "remaining_ticks": remaining, - "total_delay_ticks": total_delay, - }) + ( + pending_recs + . append( + { + "entity_id": 100, + "x": 13.5, + "y": 12.5, + "z": 0, + "remaining_ticks": remaining, + "total_delay_ticks": total_delay, + } + ) + ) # #535: Mock overheard NPC-NPC conversation (D-078) var conv_events: Array = [] var conv_ended: Array = [] var conv_start := 3 var conv_lines := [ - {"speaker": "Mira", "target": "Soren", "line": "The cargo manifests don't add up. Three containers unaccounted for."}, - {"speaker": "Soren", "target": "Mira", "line": "Could be a logging error. Happens every... cycle."}, - {"speaker": "Mira", "target": "Soren", "line": "Not like this. Someone moved them after... check."}, - {"speaker": "Soren", "target": "Mira", "line": "You're reading too much into it. The docks are... these days."}, - {"speaker": "Mira", "target": "Soren", "line": "Then explain the weight discrepancy. Two hundred kilos... just gone."}, # gdlint:ignore = max-line-length - {"speaker": "Soren", "target": "Mira", "line": "Fine. I'll pull the bay... tonight. But keep this between us."}, + { + "speaker": "Mira", + "target": "Soren", + "line": "The cargo manifests don't add up. Three containers unaccounted for." + }, + { + "speaker": "Soren", + "target": "Mira", + "line": "Could be a logging error. Happens every... cycle." + }, + { + "speaker": "Mira", + "target": "Soren", + "line": "Not like this. Someone moved them after... check." + }, + { + "speaker": "Soren", + "target": "Mira", + "line": "You're reading too much into it. The docks are... these days." + }, + { + "speaker": "Mira", + "target": "Soren", + "line": "Then explain the weight discrepancy. Two hundred kilos... just gone." + }, # gdlint:ignore = max-line-length + { + "speaker": "Soren", + "target": "Mira", + "line": "Fine. I'll pull the bay... tonight. But keep this between us." + }, ] var conv_tick_interval := 5 var conv_total_ticks := conv_lines.size() * conv_tick_interval @@ -170,20 +255,26 @@ func snapshot() -> Dictionary: var within_tick := (tick - conv_start) % conv_tick_interval if within_tick == 0 and conv_index < conv_lines.size(): var cl: Dictionary = conv_lines[conv_index] - conv_events.append({ - "speaker_id": 10, - "target_id": 11, - "speaker_name": cl.speaker, - "target_name": cl.target, - "occluded_line": cl.line, - }) + ( + conv_events + . append( + { + "speaker_id": 10, + "target_id": 11, + "speaker_name": cl.speaker, + "target_name": cl.target, + "occluded_line": cl.line, + } + ) + ) elif tick == conv_start + conv_total_ticks: conv_ended.append({"speaker_id": 10, "target_id": 11}) return { "tick": tick, "version": Protocol.PROTOCOL_VERSION, - "game_time": { + "game_time": + { "day": 0, "time_of_day": tick * 10, "day_phase": "Morning", @@ -207,6 +298,7 @@ func snapshot() -> Dictionary: # -- Map generation ------------------------------------------------------------ + func _tiles() -> Array: var tiles: Array = [] var room_x := 7 @@ -216,8 +308,9 @@ func _tiles() -> Array: for x in range(room_x, room_x + room_w): for y in range(room_y, room_y + room_h): - var is_edge := (x == room_x or x == room_x + room_w - 1 - or y == room_y or y == room_y + room_h - 1) + var is_edge := ( + x == room_x or x == room_x + room_w - 1 or y == room_y or y == room_y + room_h - 1 + ) var tile_type: String if is_edge: if y == room_y + room_h - 1 and x == room_x + room_w / 2: @@ -253,7 +346,9 @@ func _visible_tiles() -> Array: if dist <= radius: if x >= room_x and x < room_x + room_w and y >= room_y and y < room_y + room_h: var sector: String = "Forward" if y <= py else "Peripheral" - vtiles.append({"x": x, "y": y, "z": 0, "visibility": sector, "type": _get_tile_type(x, y)}) + vtiles.append( + {"x": x, "y": y, "z": 0, "visibility": sector, "type": _get_tile_type(x, y)} + ) return vtiles @@ -262,8 +357,9 @@ func _get_tile_type(x: int, y: int) -> String: var room_y := 7 var room_w := 8 var room_h := 8 - var is_edge := (x == room_x or x == room_x + room_w - 1 - or y == room_y or y == room_y + room_h - 1) + var is_edge := ( + x == room_x or x == room_x + room_w - 1 or y == room_y or y == room_y + room_h - 1 + ) if is_edge: if y == room_y + room_h - 1 and x == room_x + room_w / 2: return "door" @@ -273,6 +369,7 @@ func _get_tile_type(x: int, y: int) -> String: # -- Spatial helpers ----------------------------------------------------------- + func _is_walkable(pos: Vector2i) -> bool: return not _WALLS.has(pos) @@ -302,25 +399,43 @@ func has_los(from: Vector2i, to: Vector2i) -> bool: static func action_to_delta(action_name: String) -> Vector2i: match action_name: - "MoveNorth": return Vector2i(0, -1) - "MoveNortheast": return Vector2i(1, -1) - "MoveEast": return Vector2i(1, 0) - "MoveSoutheast": return Vector2i(1, 1) - "MoveSouth": return Vector2i(0, 1) - "MoveSouthwest": return Vector2i(-1, 1) - "MoveWest": return Vector2i(-1, 0) - "MoveNorthwest": return Vector2i(-1, -1) - _: return Vector2i.ZERO + "MoveNorth": + return Vector2i(0, -1) + "MoveNortheast": + return Vector2i(1, -1) + "MoveEast": + return Vector2i(1, 0) + "MoveSoutheast": + return Vector2i(1, 1) + "MoveSouth": + return Vector2i(0, 1) + "MoveSouthwest": + return Vector2i(-1, 1) + "MoveWest": + return Vector2i(-1, 0) + "MoveNorthwest": + return Vector2i(-1, -1) + _: + return Vector2i.ZERO static func delta_to_facing(delta: Vector2i) -> String: match delta: - Vector2i(0, -1): return "North" - Vector2i(1, -1): return "Northeast" - Vector2i(1, 0): return "East" - Vector2i(1, 1): return "Southeast" - Vector2i(0, 1): return "South" - Vector2i(-1, 1): return "Southwest" - Vector2i(-1, 0): return "West" - Vector2i(-1, -1): return "Northwest" - _: return "North" + Vector2i(0, -1): + return "North" + Vector2i(1, -1): + return "Northeast" + Vector2i(1, 0): + return "East" + Vector2i(1, 1): + return "Southeast" + Vector2i(0, 1): + return "South" + Vector2i(-1, 1): + return "Southwest" + Vector2i(-1, 0): + return "West" + Vector2i(-1, -1): + return "Northwest" + _: + return "North" diff --git a/client/scripts/rendering/character_visual.gd b/client/scripts/rendering/character_visual.gd index 0ed800c76..641664df0 100644 --- a/client/scripts/rendering/character_visual.gd +++ b/client/scripts/rendering/character_visual.gd @@ -40,12 +40,25 @@ const OUTLINE_SHADER_PATH := BASE_PATH + "shaders/outline.gdshader" ## All body segment names in assembly order. D-160: 14 base + 2 swappable torso + 2 face = 18. ## torso_upper is loaded and hidden by default; clothing coverage reveals it. const ALL_SEGMENTS: Array[String] = [ - "head", "neck", "torso_upper", "torso", "hips", - "arm_upper_l", "arm_upper_r", "arm_lower_l", "arm_lower_r", - "hand_l", "hand_r", - "leg_upper_l", "leg_upper_r", "leg_lower_l", "leg_lower_r", - "foot_l", "foot_r", - "eyes", "eyebrows", + "head", + "neck", + "torso_upper", + "torso", + "hips", + "arm_upper_l", + "arm_upper_r", + "arm_lower_l", + "arm_lower_r", + "hand_l", + "hand_r", + "leg_upper_l", + "leg_upper_r", + "leg_lower_l", + "leg_lower_r", + "foot_l", + "foot_r", + "eyes", + "eyebrows", ] ## Segments that do NOT receive the skin tone shader — preserve original embedded material. @@ -107,14 +120,14 @@ var _overhead_attachment: BoneAttachment3D = null # Inspectable state for tests var _active_torso_variant: String = "full" -var _active_coverages: Dictionary = {} # item_id -> coverage dict -var _loaded_slots: Array[String] = [] # "hair", "facial_hair", etc. +var _active_coverages: Dictionary = {} # item_id -> coverage dict +var _loaded_slots: Array[String] = [] # "hair", "facial_hair", etc. var _toon_shader: Shader = null var _toon_masked_shader: Shader = null var _outline_shader: Shader = null var _white_mask: ImageTexture = null # 1x1 white pixel — forces full-body tinting -var _iris_mask: Texture2D = null # iris-only mask from T_Eye_Split.png +var _iris_mask: Texture2D = null # iris-only mask from T_Eye_Split.png func _ready() -> void: @@ -140,6 +153,7 @@ func _load_shaders() -> void: # Public API # ============================================================================= + ## Rebuild the full character from a CharacterVisualDescriptor. func load_descriptor(descriptor: CharacterVisualDescriptor) -> void: _clear() @@ -168,8 +182,14 @@ func set_facing(direction: Variant) -> void: rotation.y = -atan2((direction as Vector2).x, (direction as Vector2).y) elif direction is String: var angles: Dictionary = { - "south": 0.0, "southwest": 45.0, "west": 90.0, "northwest": 135.0, - "north": 180.0, "northeast": -135.0, "east": -90.0, "southeast": -45.0, + "south": 0.0, + "southwest": 45.0, + "west": 90.0, + "northwest": 135.0, + "north": 180.0, + "northeast": -135.0, + "east": -90.0, + "southeast": -45.0, } rotation_degrees.y = angles.get((direction as String).to_lower(), 0.0) @@ -232,6 +252,7 @@ func get_overhead_anchor() -> Marker3D: # Internal — teardown # ============================================================================= + func _clear() -> void: # Outline nodes are duplicates of body/clothing meshes — must be freed FIRST, # before the source meshes are removed. _rebuild_outlines() is not called here @@ -287,6 +308,7 @@ func _clear() -> void: # Internal — skeleton # ============================================================================= + func _load_skeleton() -> void: if not ResourceLoader.exists(SKELETON_PATH): push_error("CharacterVisual: skeleton not found at %s" % SKELETON_PATH) @@ -317,7 +339,12 @@ func _validate_slot_bones() -> void: for slot: String in SLOT_TO_BONE: var bone_name: String = SLOT_TO_BONE[slot] if _skeleton.find_bone(bone_name) == -1: - push_warning("CharacterVisual: SLOT_TO_BONE['%s'] = '%s' — bone not found in skeleton" % [slot, bone_name]) + push_warning( + ( + "CharacterVisual: SLOT_TO_BONE['%s'] = '%s' — bone not found in skeleton" + % [slot, bone_name] + ) + ) ## #712: Create a Marker3D anchored ~0.3m above the Head bone via BoneAttachment3D. @@ -344,6 +371,7 @@ func _create_overhead_anchor() -> void: # Internal — body segments (D-160) # ============================================================================= + func _load_body_segments(desc: CharacterVisualDescriptor) -> void: var body_dir := BASE_PATH + "bodies/%s/" % desc.body_type_key() var tone := SKIN_TONES[clampi(desc.skin_tone, 0, SKIN_TONES.size() - 1)] @@ -420,6 +448,7 @@ func _apply_body_shader(mi: MeshInstance3D, seg_name: String, tone: Dictionary) # Internal — head, hair, facial hair, eyebrows (BoneAttachment3D) (D-161) # ============================================================================= + func _load_head(desc: CharacterVisualDescriptor) -> void: if desc.head_id.is_empty(): return @@ -452,7 +481,8 @@ func _load_eyebrows(_desc: CharacterVisualDescriptor) -> void: func _attach_to_bone( - path: String, bone_name: String, tint: Color = Color.WHITE, _render_priority: int = 0) -> BoneAttachment3D: + path: String, bone_name: String, tint: Color = Color.WHITE, _render_priority: int = 0 +) -> BoneAttachment3D: if _skeleton == null: return null if not ResourceLoader.exists(path): @@ -517,6 +547,7 @@ func _attach_to_bone( # Internal — clothing (D-162): coverage.json → segment hiding + torso variant # ============================================================================= + func _load_clothing(desc: CharacterVisualDescriptor) -> void: if _skeleton == null or desc.clothing_slots.is_empty(): return @@ -566,7 +597,6 @@ func _load_clothing(desc: CharacterVisualDescriptor) -> void: # Hiding is reserved for amputation/prosthetics via the debug tab or game logic. - func _read_coverage(path: String) -> Dictionary: if not FileAccess.file_exists(path): return {} @@ -608,6 +638,7 @@ func _apply_clothing_shader(mi: MeshInstance3D, tints: Array, mask_tex: Texture2 # Internal — accessories (BoneAttachment3D per slot→bone) # ============================================================================= + func _load_accessories(desc: CharacterVisualDescriptor) -> void: for slot: String in desc.accessory_slots: var item_id: String = desc.accessory_slots[slot] @@ -629,8 +660,10 @@ func _load_accessories(desc: CharacterVisualDescriptor) -> void: # Internal — tinting (hair, accessories, bone-attached assets with tint) # ============================================================================= + func _apply_tinted_shader( - mi: MeshInstance3D, tint: Color, mask_tex: Texture2D = null, _render_priority: int = 0) -> void: + mi: MeshInstance3D, tint: Color, mask_tex: Texture2D = null, _render_priority: int = 0 +) -> void: if mi.mesh == null: return for surf in range(mi.mesh.get_surface_count()): @@ -657,6 +690,7 @@ func _apply_tinted_shader( # Internal — outline pass (inverted hull, cull_front) # ============================================================================= + func _rebuild_outlines() -> void: for node in _outline_nodes: if is_instance_valid(node): @@ -693,6 +727,7 @@ func _rebuild_outlines() -> void: # Animation # ============================================================================= + func _load_animations() -> void: if _skeleton == null: return @@ -764,6 +799,7 @@ func stop_animation() -> void: # Static helpers # ============================================================================= + static func _find_skeleton(root: Node) -> Skeleton3D: if root is Skeleton3D: return root as Skeleton3D @@ -791,7 +827,9 @@ static func _get_albedo_texture(mat: Material) -> Texture2D: if mat is BaseMaterial3D: return (mat as BaseMaterial3D).albedo_texture if mat is ShaderMaterial: - for p: String in ["albedo_tex", "albedo_texture", "texture_albedo", "Hair_Texture", "BaseColor"]: + for p: String in [ + "albedo_tex", "albedo_texture", "texture_albedo", "Hair_Texture", "BaseColor" + ]: var val: Variant = (mat as ShaderMaterial).get_shader_parameter(p) if val is Texture2D: return val as Texture2D diff --git a/client/scripts/rendering/character_visual_descriptor.gd b/client/scripts/rendering/character_visual_descriptor.gd index a6a828506..d248fc525 100644 --- a/client/scripts/rendering/character_visual_descriptor.gd +++ b/client/scripts/rendering/character_visual_descriptor.gd @@ -166,6 +166,7 @@ func to_dict() -> Dictionary: # -- Color serialization helpers -- # Wire format: [r, g, b, a] float array (rmp_serde serializes Color as tuple). + static func _decode_color(value: Variant, fallback: Color) -> Color: if value is Array and value.size() >= 3: return Color(value[0], value[1], value[2], value[3] if value.size() >= 4 else 1.0) @@ -198,5 +199,3 @@ static func _encode_tint_map(data: Dictionary) -> Dictionary: encoded.append(_encode_color(c)) result[key] = encoded return result - - diff --git a/client/scripts/rendering/cursor_renderer.gd b/client/scripts/rendering/cursor_renderer.gd index 7b75e1ed7..36236f97e 100644 --- a/client/scripts/rendering/cursor_renderer.gd +++ b/client/scripts/rendering/cursor_renderer.gd @@ -84,6 +84,7 @@ func _process(delta: float) -> void: # --- Hover detection (automatic, from GameState.visible_entities) --- + func _detect_hover() -> void: var prev_id := hovered_entity_id @@ -115,7 +116,7 @@ func _find_nearest_entity() -> Dictionary: var mouse_world: Vector2 = xform.affine_inverse() * mouse_screen var best_dist := INF - var result := { id = -1, kind = "", color = COLOR_DEFAULT, offset = Vector2.ZERO } + var result := {id = -1, kind = "", color = COLOR_DEFAULT, offset = Vector2.ZERO} for entity in GameState.visible_entities: if not entity.has("entity_id") or not entity.has("x") or not entity.has("y"): @@ -138,6 +139,7 @@ func _find_nearest_entity() -> Dictionary: # --- State transitions --- + func _set_target(new_state: State) -> void: if new_state == _target: return @@ -154,29 +156,75 @@ func _set_target(new_state: State) -> void: func _snapshot() -> Dictionary: - return { gap = _gap, len = _len, rot = _rot, thick = _thick, - color = _color, bloom = _bloom, bracket_a = _bracket_a, alpha = _alpha } + return { + gap = _gap, + len = _len, + rot = _rot, + thick = _thick, + color = _color, + bloom = _bloom, + bracket_a = _bracket_a, + alpha = _alpha + } func _params_for(state: State) -> Dictionary: match state: State.ENTITY_HOVER: - return { gap = 10.0, len = 6.0, rot = 0.0, thick = 1.0, - color = _hover_color, bloom = 0.5, bracket_a = 1.0, alpha = 1.0 } + return { + gap = 10.0, + len = 6.0, + rot = 0.0, + thick = 1.0, + color = _hover_color, + bloom = 0.5, + bracket_a = 1.0, + alpha = 1.0 + } State.OBJECT_HOVER: - return { gap = 4.0, len = 6.0, rot = PI / 4.0, thick = 1.0, - color = _hover_color, bloom = 0.3, bracket_a = 0.0, alpha = 0.8 } + return { + gap = 4.0, + len = 6.0, + rot = PI / 4.0, + thick = 1.0, + color = _hover_color, + bloom = 0.3, + bracket_a = 0.0, + alpha = 0.8 + } State.WEAPON_AIM: - return { gap = 12.0, len = 9.0, rot = 0.0, thick = 2.0, - color = COLOR_WEAPON, bloom = 0.0, bracket_a = 0.0, alpha = 1.0 } + return { + gap = 12.0, + len = 9.0, + rot = 0.0, + thick = 2.0, + color = COLOR_WEAPON, + bloom = 0.0, + bracket_a = 0.0, + alpha = 1.0 + } _: - return { gap = 4.0, len = 6.0, rot = 0.0, thick = 1.0, - color = COLOR_DEFAULT, bloom = 0.4, bracket_a = 0.0, alpha = 0.45 } + return { + gap = 4.0, + len = 6.0, + rot = 0.0, + thick = 1.0, + color = COLOR_DEFAULT, + bloom = 0.4, + bracket_a = 0.0, + alpha = 0.45 + } func _apply_params(p: Dictionary) -> void: - _gap = p.gap; _len = p.len; _rot = p.rot; _thick = p.thick - _color = p.color; _bloom = p.bloom; _bracket_a = p.bracket_a; _alpha = p.alpha + _gap = p.gap + _len = p.len + _rot = p.rot + _thick = p.thick + _color = p.color + _bloom = p.bloom + _bracket_a = p.bracket_a + _alpha = p.alpha func _interpolate() -> void: @@ -193,6 +241,7 @@ func _interpolate() -> void: # --- Drawing --- + func _draw() -> void: # _hover_offset is computed in _detect_hover() during _process(), not recalculated # here. This is safe because Camera2D (tree sibling, earlier in scene order) applies @@ -233,13 +282,19 @@ func _draw_brackets(offset: Vector2, color: Color) -> void: # --- Test-friendly API (expected by test_cursor_states.gd) --- + func get_state() -> String: match current_state: - State.DEFAULT: return "Default" - State.ENTITY_HOVER: return "EntityHover" - State.OBJECT_HOVER: return "ObjectHover" - State.WEAPON_AIM: return "WeaponAim" - _: return "Default" + State.DEFAULT: + return "Default" + State.ENTITY_HOVER: + return "EntityHover" + State.OBJECT_HOVER: + return "ObjectHover" + State.WEAPON_AIM: + return "WeaponAim" + _: + return "Default" func set_hover_target(data: Dictionary) -> void: @@ -252,10 +307,14 @@ func set_hover_target(data: Dictionary) -> void: if kind == "Npc": var rel: String = data.get("relationship", "Unknown") match rel: - "Friendly": _hover_color = Constants.ENTITY_COLOR_FRIENDLY - "PersonOfInterest": _hover_color = Constants.ENTITY_COLOR_POI - "Hostile": _hover_color = Constants.ENTITY_COLOR_HOSTILE - _: _hover_color = Constants.ENTITY_COLOR_UNKNOWN + "Friendly": + _hover_color = Constants.ENTITY_COLOR_FRIENDLY + "PersonOfInterest": + _hover_color = Constants.ENTITY_COLOR_POI + "Hostile": + _hover_color = Constants.ENTITY_COLOR_HOSTILE + _: + _hover_color = Constants.ENTITY_COLOR_UNKNOWN _target = State.ENTITY_HOVER _t = 1.0 _apply_params(_params_for(State.ENTITY_HOVER)) diff --git a/client/scripts/rendering/entity_renderer.gd b/client/scripts/rendering/entity_renderer.gd index dc6376127..e67894535 100644 --- a/client/scripts/rendering/entity_renderer.gd +++ b/client/scripts/rendering/entity_renderer.gd @@ -18,7 +18,7 @@ const TILE_SIZE: int = Constants.TILE_SIZE # D-044: 24x32 entity footprint within 32x32 visual tile (64x64 source at 0.5 scale = 32px runtime) const ENTITY_WIDTH: int = 24 const ENTITY_HEIGHT: int = 32 -const ENTITY_OFFSET_X: float = 0.0 # sprite fills tile width at 0.5 scale +const ENTITY_OFFSET_X: float = 0.0 # sprite fills tile width at 0.5 scale const ENTITY_OFFSET_Y: float = TILE_SIZE - ENTITY_HEIGHT # feet-anchored for correct y-sort with D-019 tilt # Lerp speed — framerate-independent exponential smoothing. @@ -28,12 +28,13 @@ const LERP_SPEED: float = 12.0 # #521: Color transition duration in seconds (D-033: "0.5s fade") const COLOR_FADE_DURATION: float = 0.5 -var entity_nodes: Dictionary = {} # entity_id -> Node2D -var _entity_targets: Dictionary = {} # entity_id -> Vector2 (target pixel position) +var entity_nodes: Dictionary = {} # entity_id -> Node2D +var _entity_targets: Dictionary = {} # entity_id -> Vector2 (target pixel position) var _entity_relationships: Dictionary = {} # #521: entity_id -> String (last relationship) var _entity_tweens: Dictionary = {} # #521: entity_id -> {from: Color, target: Color, elapsed: float} var _entity_facing: Dictionary = {} # #540: entity_id -> String ("north"/"east"/"south"/"west") + func _ready() -> void: print("EntityRenderer: Initialized") @@ -111,7 +112,11 @@ func _create_entity_node(entity_id: int, entity_data: Dictionary) -> void: var tex := _load_sprite_texture(direction) if tex == null: push_error( - "EntityRenderer: no texture for entity %d direction '%s' — entity will be invisible" % [entity_id, direction]) + ( + "EntityRenderer: no texture for entity %d direction '%s' — entity will be invisible" + % [entity_id, direction] + ) + ) entity_node.texture = tex # D-033: self_modulate for relationship tinting; modulate.a is reserved for D-015 dimming. @@ -220,10 +225,14 @@ func _entity_direction(entity_id: int, _entity_data: Dictionary) -> String: # N/NW → north, NE/E → east, SE/S → south, SW/W → west static func _octant_to_direction(octant: String) -> String: match octant: - "North", "Northwest": return "north" - "Northeast", "East": return "east" - "Southeast", "South": return "south" - "Southwest", "West": return "west" + "North", "Northwest": + return "north" + "Northeast", "East": + return "east" + "Southeast", "South": + return "south" + "Southwest", "West": + return "west" _: push_warning("EntityRenderer: unrecognised octant '%s' — defaulting to south" % octant) return "south" @@ -247,11 +256,13 @@ func _add_facing_indicator(parent_node: Node2D) -> void: var s := Constants.FACING_INDICATOR_SIZE var offset := Constants.FACING_INDICATOR_OFFSET # Triangle pointing up (North), offset from center. Rotates around (0,0). - indicator.polygon = PackedVector2Array([ - Vector2(0, -offset - s), - Vector2(-s * 0.6, -offset + s * 0.4), - Vector2(s * 0.6, -offset + s * 0.4), - ]) + indicator.polygon = PackedVector2Array( + [ + Vector2(0, -offset - s), + Vector2(-s * 0.6, -offset + s * 0.4), + Vector2(s * 0.6, -offset + s * 0.4), + ] + ) indicator.color = Constants.ENTITY_COLOR_PLAYER # Sprite2D local space: 64px texture at scale 0.5 → center of visible sprite at (32,32). # Indicator rotates around this point to track player facing direction. diff --git a/client/scripts/rendering/fog_entities.gd b/client/scripts/rendering/fog_entities.gd index f8755dfc0..ec931a53f 100644 --- a/client/scripts/rendering/fog_entities.gd +++ b/client/scripts/rendering/fog_entities.gd @@ -17,25 +17,25 @@ extends Node2D const TILE_SIZE: int = Constants.TILE_SIZE # D-059: Fog entity colors -const COLOR_UNRECOGNIZED := Color("#555566") # Neutral grey blob +const COLOR_UNRECOGNIZED := Color("#555566") # Neutral grey blob # Intentionally matches Constants.INSERT_COLOR_TEXT / cursor default (#c8d0e0). # Sonar pings are insert-generated — same visual language as cursor and HUD overlays. -const COLOR_PING := Color("#c8d0e0") # Insert white-blue (D-048/D-056) +const COLOR_PING := Color("#c8d0e0") # Insert white-blue (D-048/D-056) # Animation timing -const PULSE_PERIOD: float = 0.8 # Breathing pulse cycle (seconds) -const PULSE_MIN_ALPHA: float = 0.4 # Min alpha during breathing -const PULSE_MAX_ALPHA: float = 0.8 # Max alpha during breathing -const PING_DURATION: float = 1.5 # Sound ping expand + fade (seconds) -const PING_MAX_RADIUS: float = 24.0 # Max ring expand radius (pixels) -const PING_RING_COUNT: int = 3 # Concentric rings per ping -const PING_RING_WIDTH: float = 1.5 # Ring line width (pixels) -const DRIFT_RANGE: float = 0.5 # ±0.5 tile position drift (D-059) -const DRIFT_PERIOD: float = 2.0 # Seconds per drift wander +const PULSE_PERIOD: float = 0.8 # Breathing pulse cycle (seconds) +const PULSE_MIN_ALPHA: float = 0.4 # Min alpha during breathing +const PULSE_MAX_ALPHA: float = 0.8 # Max alpha during breathing +const PING_DURATION: float = 1.5 # Sound ping expand + fade (seconds) +const PING_MAX_RADIUS: float = 24.0 # Max ring expand radius (pixels) +const PING_RING_COUNT: int = 3 # Concentric rings per ping +const PING_RING_WIDTH: float = 1.5 # Ring line width (pixels) +const DRIFT_RANGE: float = 0.5 # ±0.5 tile position drift (D-059) +const DRIFT_PERIOD: float = 2.0 # Seconds per drift wander # Entity blob rendering const BLOB_RADIUS: float = 10.0 -const SILHOUETTE_SCALE: float = 1.3 # Silhouette slightly larger than blob +const SILHOUETTE_SCALE: float = 1.3 # Silhouette slightly larger than blob # Transition thresholds: recognition progress mapped from remaining_ticks/total_delay_ticks. # Color transition starts at 50% to compress the visual change into ~0.3s (D-060). const COLOR_TRANSITION_START: float = 0.5 @@ -45,8 +45,8 @@ const SILHOUETTE_APPEAR_THRESHOLD: float = 0.3 const SILHOUETTE_SIZE := Vector2(6.0, 10.0) # Internal state — no child nodes, pure data + _draw() -var _entities: Dictionary = {} # entity_id -> {pos, drift_offset, drift_target, drift_timer, progress} -var _pings: Array = [] # [{pos: Vector2, elapsed: float}] +var _entities: Dictionary = {} # entity_id -> {pos, drift_offset, drift_target, drift_timer, progress} +var _pings: Array = [] # [{pos: Vector2, elapsed: float}] var _time: float = 0.0 @@ -121,8 +121,7 @@ func update_from_state() -> void: func _draw() -> void: # Breathing pulse — shared across all entities var pulse_t := fmod(_time, PULSE_PERIOD) / PULSE_PERIOD - var pulse_alpha := lerpf(PULSE_MIN_ALPHA, PULSE_MAX_ALPHA, - 0.5 + 0.5 * sin(pulse_t * TAU)) + var pulse_alpha := lerpf(PULSE_MIN_ALPHA, PULSE_MAX_ALPHA, 0.5 + 0.5 * sin(pulse_t * TAU)) # Draw entity blobs for eid in _entities.keys(): @@ -132,7 +131,9 @@ func _draw() -> void: # Color transition: grey → D-033 teal based on recognition progress # Transition begins at 50% progress (D-060: ~0.3s visual transition within delay window) var progress: float = e.progress - var color_t: float = clampf((progress - COLOR_TRANSITION_START) / (1.0 - COLOR_TRANSITION_START), 0.0, 1.0) + var color_t: float = clampf( + (progress - COLOR_TRANSITION_START) / (1.0 - COLOR_TRANSITION_START), 0.0, 1.0 + ) var blob_color: Color = COLOR_UNRECOGNIZED.lerp(Constants.ENTITY_COLOR_UNKNOWN, color_t) blob_color.a = pulse_alpha @@ -172,7 +173,4 @@ func _draw() -> void: func _random_drift() -> Vector2: - return Vector2( - randf_range(-DRIFT_RANGE, DRIFT_RANGE), - randf_range(-DRIFT_RANGE, DRIFT_RANGE) - ) + return Vector2(randf_range(-DRIFT_RANGE, DRIFT_RANGE), randf_range(-DRIFT_RANGE, DRIFT_RANGE)) diff --git a/client/scripts/rendering/fog_shader.gd b/client/scripts/rendering/fog_shader.gd index ba3ad87cd..34dc68090 100644 --- a/client/scripts/rendering/fog_shader.gd +++ b/client/scripts/rendering/fog_shader.gd @@ -41,7 +41,11 @@ func _ready() -> void: noise_tex.width = 256 noise_tex.height = 256 noise_tex.seamless = true - noise_tex.changed.connect(func(): _noise_ready = true; fog_noise_ready.emit()) + noise_tex.changed.connect( + func(): + _noise_ready = true + fog_noise_ready.emit() + ) _shader_mat.set_shader_parameter("noise_tex", noise_tex) _shader_mat.set_shader_parameter("tile_size", TILE_SIZE) @@ -74,6 +78,8 @@ func update_fog() -> void: _shader_mat.set_shader_parameter("rect_sz", _fog_rect.size) _shader_mat.set_shader_parameter("map_offset", Vector2(FogState.map_bounds.position)) _shader_mat.set_shader_parameter("map_size", Vector2(FogState.map_bounds.size)) - var t: float = FogState.override_time if FogState.override_time >= 0.0 else Time.get_ticks_msec() / 1000.0 + var t: float = ( + FogState.override_time if FogState.override_time >= 0.0 else Time.get_ticks_msec() / 1000.0 + ) _shader_mat.set_shader_parameter("time", t) _shader_mat.set_shader_parameter("debug_exploration", FogState.debug_exploration) diff --git a/client/scripts/rendering/sound_indicator_renderer.gd b/client/scripts/rendering/sound_indicator_renderer.gd index 7d00f32b0..3c3ec7092 100644 --- a/client/scripts/rendering/sound_indicator_renderer.gd +++ b/client/scripts/rendering/sound_indicator_renderer.gd @@ -20,20 +20,21 @@ extends Node2D const TILE_SIZE: int = Constants.TILE_SIZE # Visual parameters -const INDICATOR_LIFETIME: float = 3.5 # Total seconds visible -const FADE_DURATION: float = 0.6 # Fade-out window at end -const EDGE_INSET: float = 20.0 # Pixels inward from viewport edge -const ARROW_HALF: float = 7.0 # Half-width of arrowhead base -const ARROW_LEN: float = 12.0 # Length from tip to base +const INDICATOR_LIFETIME: float = 3.5 # Total seconds visible +const FADE_DURATION: float = 0.6 # Fade-out window at end +const EDGE_INSET: float = 20.0 # Pixels inward from viewport edge +const ARROW_HALF: float = 7.0 # Half-width of arrowhead base +const ARROW_LEN: float = 12.0 # Length from tip to base # D-018/D-069 colors — sourced from Constants to prevent palette drift -const COLOR_NEUTRAL: Color = Constants.INSERT_COLOR_TEXT # Generic / footstep -const COLOR_VOICE: Color = Constants.ENTITY_COLOR_POI # Speech / conversation +const COLOR_NEUTRAL: Color = Constants.INSERT_COLOR_TEXT # Generic / footstep +const COLOR_VOICE: Color = Constants.ENTITY_COLOR_POI # Speech / conversation const COLOR_DANGER: Color = Constants.ENTITY_COLOR_HOSTILE # Alert / threat / gunshot # Indicators: [{x, y, event_type, elapsed}] var _indicators: Array = [] + func _process(delta: float) -> void: if _indicators.is_empty(): return @@ -68,12 +69,17 @@ func update_sound_events(events: Array) -> void: found = true break if not found: - _indicators.append({ - "x": ex, - "y": ey, - "event_type": evt.get("event_type", ""), - "elapsed": 0.0, - }) + ( + _indicators + . append( + { + "x": ex, + "y": ey, + "event_type": evt.get("event_type", ""), + "elapsed": 0.0, + } + ) + ) if not _indicators.is_empty(): queue_redraw() @@ -144,9 +150,19 @@ func _draw_arrow(pos: Vector2, dir: Vector2, color: Color) -> void: ## Map event type string → D-018 color category. func color_for_type(event_type: String) -> Color: var et := event_type.to_lower() - if et.contains("voice") or et.contains("speech") or et.contains("convers") or et.contains("talk"): + if ( + et.contains("voice") + or et.contains("speech") + or et.contains("convers") + or et.contains("talk") + ): return COLOR_VOICE - if et.contains("danger") or et.contains("gunshot") or et.contains("explosion") \ - or et.contains("alert") or et.contains("threat"): + if ( + et.contains("danger") + or et.contains("gunshot") + or et.contains("explosion") + or et.contains("alert") + or et.contains("threat") + ): return COLOR_DANGER return COLOR_NEUTRAL diff --git a/client/scripts/rendering/tile_renderer.gd b/client/scripts/rendering/tile_renderer.gd index cf19a8f41..8d5def1f1 100644 --- a/client/scripts/rendering/tile_renderer.gd +++ b/client/scripts/rendering/tile_renderer.gd @@ -27,11 +27,13 @@ const TILE_TYPE_MAP: Dictionary = { var _initialized: bool = false + func _ready() -> void: _setup_tileset() _initialized = true print("TileRenderer: Initialized") + # Build a programmatic TileSet with colored placeholder tiles func _setup_tileset() -> void: var ts := TileSet.new() @@ -63,6 +65,7 @@ func _setup_tileset() -> void: var source_id := ts.add_source(source) tile_set = ts + # Update tiles from snapshot data # tiles: Array of {x: int, y: int, z: int, type: String, visibility: String (optional)} # z here is the server-side FLOOR LEVEL (0 = ground, 1 = first floor, etc.), @@ -93,19 +96,25 @@ func update_tiles(tiles: Array) -> void: var tile_type_str: String = tile_data.type if not TILE_TYPE_MAP.has(tile_type_str): - push_warning("TileRenderer: unknown tile type '%s' at (%d, %d)" % [ - tile_type_str, tile_data.x, tile_data.y]) + push_warning( + ( + "TileRenderer: unknown tile type '%s' at (%d, %d)" + % [tile_type_str, tile_data.x, tile_data.y] + ) + ) continue var atlas_x: int = TILE_TYPE_MAP[tile_type_str] var coords := Vector2i(tile_data.x, tile_data.y) set_cell(coords, 0, Vector2i(atlas_x, 0)) + # Fill a tile region with a solid color func _fill_tile(img: Image, tile_index: int, color: Color) -> void: var rect := Rect2i(tile_index * TILE_SIZE, 0, TILE_SIZE, TILE_SIZE) img.fill_rect(rect, color) + # Fill a tile region with a color and a 1px border func _fill_tile_with_border(img: Image, tile_index: int, fill: Color, border: Color) -> void: var x_offset := tile_index * TILE_SIZE diff --git a/client/scripts/rendering/world_renderer.gd b/client/scripts/rendering/world_renderer.gd index 7350bb08f..ddeaeac20 100644 --- a/client/scripts/rendering/world_renderer.gd +++ b/client/scripts/rendering/world_renderer.gd @@ -20,9 +20,11 @@ var _last_tick: int = -1 @onready var entity_renderer = $FogGroup/YSortGroup/Entities @onready var sound_indicator_renderer = $SoundIndicators # #126 D-018 medium-range indicators + func _ready() -> void: print("WorldRenderer: Initialized (D-049 z-stack)") + # Called each frame to update visuals from game state. # Uses tick-based invalidation — re-renders all layers when a new snapshot arrives. func update_from_state() -> void: diff --git a/client/scripts/snapshot_consumers.gd b/client/scripts/snapshot_consumers.gd index 59bad3e52..8cd91334b 100644 --- a/client/scripts/snapshot_consumers.gd +++ b/client/scripts/snapshot_consumers.gd @@ -74,8 +74,7 @@ func play_close_sound_events() -> void: 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)) + evt.get("event_type", ""), Vector2(float(evt.x), float(evt.y)) ) GameState.close_sound_events = [] diff --git a/client/scripts/snapshot_handler.gd b/client/scripts/snapshot_handler.gd index 97bb44514..589d79c66 100644 --- a/client/scripts/snapshot_handler.gd +++ b/client/scripts/snapshot_handler.gd @@ -20,15 +20,23 @@ static func apply(snapshot: Dictionary) -> void: 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": + 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()]) + 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: @@ -44,7 +52,11 @@ static func apply(snapshot: Dictionary) -> void: # 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: + 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") @@ -195,12 +207,17 @@ static func apply(snapshot: Dictionary) -> void: 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) + 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: + 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) @@ -220,12 +237,18 @@ static func apply(snapshot: Dictionary) -> void: 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_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: + 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: diff --git a/client/scripts/util/yaml_parser.gd b/client/scripts/util/yaml_parser.gd index cafec640e..395661dd4 100644 --- a/client/scripts/util/yaml_parser.gd +++ b/client/scripts/util/yaml_parser.gd @@ -19,9 +19,9 @@ static func parse(text: String) -> Dictionary: # Stack: [{indent: int, key: String}] — path of open section headers var stack: Array = [] # Array state - var current_array: Variant = null # Array being built, or null - var current_item: Variant = null # Dict being built for current array item, or null - var array_parent_indent: int = -1 # indent of the "key:" line that owns the array + var current_array: Variant = null # Array being built, or null + var current_item: Variant = null # Dict being built for current array item, or null + var array_parent_indent: int = -1 # indent of the "key:" line that owns the array for raw_line in text.split("\n"): var stripped := raw_line.strip_edges(false, true) @@ -126,19 +126,21 @@ static func _parse_value(val: String) -> Variant: if val.is_empty(): return "" # Strip inline comment outside quotes - if not val.begins_with("\""): + if not val.begins_with('"'): var comment_pos: int = val.find(" #") if comment_pos >= 0: val = val.substr(0, comment_pos).strip_edges() # Quoted string — extract content between quotes - if val.begins_with("\""): - var end_quote: int = val.find("\"", 1) + if val.begins_with('"'): + var end_quote: int = val.find('"', 1) if end_quote > 0: return val.substr(1, end_quote - 1) return val.substr(1) # Boolean - if val == "true": return true - if val == "false": return false + if val == "true": + return true + if val == "false": + return false # Float (must have decimal point) if val.contains(".") and val.is_valid_float(): return val.to_float() diff --git a/client/ui/bug_report_dialog.gd b/client/ui/bug_report_dialog.gd index bffaf1913..b9110b1b8 100644 --- a/client/ui/bug_report_dialog.gd +++ b/client/ui/bug_report_dialog.gd @@ -46,11 +46,11 @@ var _captured_screenshot: Image = null # Memory ceiling: 60 snapshot JSON strings (each ~2-8KB depending on entity count) # + 60 input arrays (negligible). Worst case ~480KB resident. Acceptable for a # debug tool that is always active during Gauntlet play. -var _input_ring: Array = [] # Array[Array] — each slot: Array of {tick, action} dicts -var _input_head: int = 0 # Next write index (0..RING_SIZE-1) -var _input_count: int = 0 # Filled slot count (0..RING_SIZE) +var _input_ring: Array = [] # Array[Array] — each slot: Array of {tick, action} dicts +var _input_head: int = 0 # Next write index (0..RING_SIZE-1) +var _input_count: int = 0 # Filled slot count (0..RING_SIZE) -var _snapshot_ring: Array = [] # Array[String] — each slot: JSON-serialized ObserverSnapshot +var _snapshot_ring: Array = [] # Array[String] — each slot: JSON-serialized ObserverSnapshot var _snapshot_head: int = 0 var _snapshot_count: int = 0 @@ -74,6 +74,7 @@ func _ready() -> void: # Public API for main.gd: record one tick's data # --------------------------------------------------------------------------- + ## Record one tick. Called from main.gd on every server tick (snapshot arrival). ## - tick: current server tick number ## - snapshot_json: JSON.stringify(GameState.current_snapshot) @@ -180,18 +181,23 @@ func _to_replay_format(input: Dictionary, tick: int) -> Dictionary: # Test-accessible accessors (ring buffer introspection) # --------------------------------------------------------------------------- + func _get_buffer_capacity() -> int: return RING_SIZE + func _get_snapshot_buffer_capacity() -> int: return RING_SIZE + func _get_input_buffer() -> Array: return _input_ring + func _get_filled_input_count() -> int: return _input_count + func _get_filled_snapshot_count() -> int: return _snapshot_count @@ -200,6 +206,7 @@ func _get_filled_snapshot_count() -> int: # UI / capture flow # --------------------------------------------------------------------------- + func start_capture() -> void: if _active: return @@ -209,10 +216,15 @@ func start_capture() -> void: visible = true # Pause the simulation - SimBridge.send_input({ - "action": InputMapper.Action.PAUSE, - "timestamp_msec": Time.get_ticks_msec(), - }) + ( + SimBridge + . send_input( + { + "action": InputMapper.Action.PAUSE, + "timestamp_msec": Time.get_ticks_msec(), + } + ) + ) # Create the LineEdit dynamically _line_edit = LineEdit.new() @@ -254,10 +266,15 @@ func _close() -> void: _captured_screenshot = null # Unpause the simulation - SimBridge.send_input({ - "action": InputMapper.Action.UNPAUSE, - "timestamp_msec": Time.get_ticks_msec(), - }) + ( + SimBridge + . send_input( + { + "action": InputMapper.Action.UNPAUSE, + "timestamp_msec": Time.get_ticks_msec(), + } + ) + ) func _save_report(description: String) -> void: @@ -301,7 +318,9 @@ func _save_report(description: String) -> void: if desc_file: desc_file.store_string("Description: %s\n" % description) desc_file.store_string("Tick: %d\n" % tick) - desc_file.store_string("Room: %s\n" % str(GameState.room_id if GameState.room_id else "none")) + desc_file.store_string( + "Room: %s\n" % str(GameState.room_id if GameState.room_id else "none") + ) desc_file.store_string("Stance: %s\n" % GameState.player_stance) desc_file.store_string("Facing: %s\n" % GameState.player_facing) desc_file.store_string("Position: %s\n" % str(GameState.player_position)) @@ -343,8 +362,11 @@ func _save_report(description: String) -> void: var seed_val: Variant = _get_current_seed() seed_file.store_string(str(seed_val) + "\n") if seed_val == "unavailable": - seed_file.store_string( - "# Server protocol change required: add 'rng_seed' (u64) field to ObserverSnapshot.\n" + ( + seed_file + . store_string( + "# Server protocol change required: add 'rng_seed' (u64) field to ObserverSnapshot.\n" + ) ) seed_file.close() files_saved += 1 @@ -360,8 +382,12 @@ func _save_report(description: String) -> void: else: push_error("BugReport: failed to write %s (error %d)" % [screenshot_path, img_err]) - print("BugReport: saved %d/7 files to %s (ring: %d ticks)" % [ - files_saved, base_path, _input_count]) + print( + ( + "BugReport: saved %d/7 files to %s (ring: %d ticks)" + % [files_saved, base_path, _input_count] + ) + ) ## Simplified client-side text render of the current snapshot. @@ -369,14 +395,24 @@ func _save_report(description: String) -> void: func _render_snapshot_text() -> String: var lines: PackedStringArray = [] lines.append("=== Snapshot t%d ===" % GameState.current_tick) - lines.append("Player: %s facing %s (%s)" % [ - str(GameState.player_position), GameState.player_facing, GameState.player_stance]) + lines.append( + ( + "Player: %s facing %s (%s)" + % [str(GameState.player_position), GameState.player_facing, GameState.player_stance] + ) + ) if GameState.game_time.size() > 0: - lines.append("Time: day %s, %s, %s" % [ - str(GameState.game_time.get("day", "?")), - str(GameState.game_time.get("day_phase", "?")), - str(GameState.game_time.get("tick_rate", "?"))]) + lines.append( + ( + "Time: day %s, %s, %s" + % [ + str(GameState.game_time.get("day", "?")), + str(GameState.game_time.get("day_phase", "?")), + str(GameState.game_time.get("tick_rate", "?")) + ] + ) + ) lines.append("") lines.append("Entities (%d):" % GameState.visible_entities.size()) @@ -387,12 +423,18 @@ func _render_snapshot_text() -> String: elif entity.has("kind") and entity.kind is String: kind_str = entity.kind var vis: String = entity.get("visibility", "?") - lines.append(" #%s %s at (%s, %s) [%s]" % [ - str(entity.get("entity_id", "?")), - kind_str, - str(entity.get("x", "?")), - str(entity.get("y", "?")), - vis]) + lines.append( + ( + " #%s %s at (%s, %s) [%s]" + % [ + str(entity.get("entity_id", "?")), + kind_str, + str(entity.get("x", "?")), + str(entity.get("y", "?")), + vis + ] + ) + ) lines.append("") lines.append("Visible tiles: %d" % GameState.visible_tiles.size()) @@ -400,9 +442,15 @@ func _render_snapshot_text() -> String: if GameState.current_monologue != null: lines.append("Monologue: %s" % str(GameState.current_monologue.get("text", ""))) if GameState.current_dialogue != null: - lines.append("Dialogue: %s says '%s'" % [ - str(GameState.current_dialogue.get("npc_name", "?")), - str(GameState.current_dialogue.get("speech", ""))]) + lines.append( + ( + "Dialogue: %s says '%s'" + % [ + str(GameState.current_dialogue.get("npc_name", "?")), + str(GameState.current_dialogue.get("speech", "")) + ] + ) + ) return "\n".join(lines) @@ -416,8 +464,7 @@ func _draw() -> void: # Center box var box_pos := Vector2( - (viewport_size.x - BOX_WIDTH) / 2.0, - (viewport_size.y - BOX_HEIGHT) / 2.0 + (viewport_size.x - BOX_WIDTH) / 2.0, (viewport_size.y - BOX_HEIGHT) / 2.0 ) var box_rect := Rect2(box_pos, Vector2(BOX_WIDTH, BOX_HEIGHT)) draw_rect(box_rect, Color(0.08, 0.08, 0.12, 0.95)) @@ -425,15 +472,21 @@ func _draw() -> void: # Title var font := ThemeDB.fallback_font - draw_string(font, + draw_string( + font, box_pos + Vector2(PADDING, 24), "WRONG — Describe the issue (Enter to save, Esc to cancel):", - HORIZONTAL_ALIGNMENT_LEFT, -1, LABEL_FONT_SIZE, TEXT_COLOR) + HORIZONTAL_ALIGNMENT_LEFT, + -1, + LABEL_FONT_SIZE, + TEXT_COLOR + ) # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- + func is_active() -> bool: return _active diff --git a/client/ui/character_creation.gd b/client/ui/character_creation.gd index ce10a018b..56c8829a5 100644 --- a/client/ui/character_creation.gd +++ b/client/ui/character_creation.gd @@ -16,10 +16,10 @@ signal creation_cancelled const BG_COLOR := Color(0.03, 0.03, 0.06, 1.0) const PANEL_BG := Color(0.05, 0.05, 0.08, 1.0) const BORDER_COLOR := Color(0.12, 0.15, 0.20, 1.0) -const TEXT_COLOR := Color(0.784, 0.816, 0.878, 1.0) # INSERT_COLOR_TEXT +const TEXT_COLOR := Color(0.784, 0.816, 0.878, 1.0) # INSERT_COLOR_TEXT const TEXT_DIM := Color(0.53, 0.56, 0.63, 1.0) const HIGHLIGHT_COLOR := Color(0.906, 0.773, 0.278, 1.0) # INSERT_COLOR_HOVER -const ACTIVE_COLOR := Color(0.42, 0.79, 0.65, 1.0) # INSERT_COLOR_ACTIVE +const ACTIVE_COLOR := Color(0.42, 0.79, 0.65, 1.0) # INSERT_COLOR_ACTIVE const ITEM_NORMAL_BG := Color(0.07, 0.07, 0.10, 1.0) const ITEM_SELECTED_BG := Color(0.10, 0.13, 0.18, 1.0) const ITEM_SELECTED_BORDER := Color(0.906, 0.773, 0.278, 1.0) # INSERT_COLOR_HOVER — selection border @@ -27,7 +27,7 @@ const ITEM_SELECTED_BORDER := Color(0.906, 0.773, 0.278, 1.0) # INSERT_COLOR_HO # --- Camera presets (D-158) --- # Pitch is degrees below horizontal: 5=near eye-level, 30=steeper, 80=near top-down. # Negative values = tilt downward (camera looks toward ground). -const CAM_PITCHES: Array[float] = [-5.0, -30.0, -80.0] # frontal, dramatic, overhead +const CAM_PITCHES: Array[float] = [-5.0, -30.0, -80.0] # frontal, dramatic, overhead const CAM_PITCH_NAMES := ["Frontal", "Dramatic", "Overhead"] const CAM_DISTANCE: float = 3.8 const CAM_TARGET_HEIGHT: float = 0.85 @@ -79,35 +79,111 @@ const CLOTHING_SLOT_LABELS := ["Torso", "Legs", "Feet", "Hands"] # --- Accessory slots (no held_l/held_r — play-time decisions per wireframe spec) --- const ACCESSORY_SLOTS := [ - "hat", "goggles", "mask", "backpack", "belt", - "wrist_l", "wrist_r", "earring_l", "earring_r", "necklace", + "hat", + "goggles", + "mask", + "backpack", + "belt", + "wrist_l", + "wrist_r", + "earring_l", + "earring_r", + "necklace", ] const ACCESSORY_SLOT_LABELS := [ - "Hat", "Goggles", "Mask", "Backpack", "Belt", - "Wrist L", "Wrist R", "Earring L", "Earring R", "Necklace", + "Hat", + "Goggles", + "Mask", + "Backpack", + "Belt", + "Wrist L", + "Wrist R", + "Earring L", + "Earring R", + "Necklace", ] # --- D-165 palette (5 chromatic rows + 1 neutral row, 9 cols each) --- # Hardcoded hex per D-165 spec (HSL L=18→76% per chromatic row, S fixed per row). const PALETTE_COLORS: Array[Array] = [ # Row 1: Reds H=0° S=35% - ["#3d1d1d", "#522727", "#673131", "#7f3d3d", "#974848", "#af5959", "#bc7575", "#c99090", "#d7acac"], + [ + "#3d1d1d", + "#522727", + "#673131", + "#7f3d3d", + "#974848", + "#af5959", + "#bc7575", + "#c99090", + "#d7acac" + ], # Row 2: Greens H=150° S=28% - ["#213a2d", "#2c4e3d", "#37614c", "#43785e", "#508f70", "#62a684", "#7cb599", "#96c4ad", "#b0d2c1"], + [ + "#213a2d", + "#2c4e3d", + "#37614c", + "#43785e", + "#508f70", + "#62a684", + "#7cb599", + "#96c4ad", + "#b0d2c1" + ], # Row 3: Blues H=215° S=35% - ["#1d2b3d", "#273952", "#314867", "#3d587f", "#486997", "#597daf", "#7593bc", "#90a8c9", "#acbed7"], + [ + "#1d2b3d", + "#273952", + "#314867", + "#3d587f", + "#486997", + "#597daf", + "#7593bc", + "#90a8c9", + "#acbed7" + ], # Row 4: Purples H=275° S=28% - ["#30213a", "#402c4e", "#503761", "#624378", "#75508f", "#8a62a6", "#9d7cb5", "#b196c4", "#c4b0d2"], + [ + "#30213a", + "#402c4e", + "#503761", + "#624378", + "#75508f", + "#8a62a6", + "#9d7cb5", + "#b196c4", + "#c4b0d2" + ], # Row 5: Browns H=28° S=35% - ["#3d2c1d", "#523b27", "#674a31", "#7f5c3d", "#976d48", "#af8159", "#bc9675", "#c9ab90", "#d7c0ac"], + [ + "#3d2c1d", + "#523b27", + "#674a31", + "#7f5c3d", + "#976d48", + "#af8159", + "#bc9675", + "#c9ab90", + "#d7c0ac" + ], # Row 6: Neutral grays - ["#000000", "#1f1f1f", "#3f3f3f", "#5f5f5f", "#7f7f7f", "#9f9f9f", "#bfbfbf", "#dfdfdf", "#ffffff"], + [ + "#000000", + "#1f1f1f", + "#3f3f3f", + "#5f5f5f", + "#7f7f7f", + "#9f9f9f", + "#bfbfbf", + "#dfdfdf", + "#ffffff" + ], ] const PALETTE_COLS := 9 const RECENT_SLOTS := 9 -const CAM_ZOOM_MIN: float = 0.25 # closest zoom (face detail) -const CAM_ZOOM_MAX: float = 3.0 # farthest zoom (crowd level) +const CAM_ZOOM_MIN: float = 0.25 # closest zoom (face detail) +const CAM_ZOOM_MAX: float = 3.0 # farthest zoom (crowd level) const CAM_ZOOM_STEP: float = 0.12 const MANIFEST_PATH := "res://assets/characters/manifest.json" @@ -118,7 +194,7 @@ const CARDINAL_NAMES: Array[String] = ["south", "east", "north", "west"] # --- Descriptor and preview state --- var _descriptor: CharacterVisualDescriptor var _char_visual: CharacterVisual = null -var _facing_idx: int = 0 # index into CARDINAL_DIRS (0 = south, default face-forward) +var _facing_idx: int = 0 # index into CARDINAL_DIRS (0 = south, default face-forward) var _cam_pitch_idx: int = 0 # 0=frontal(-5°), 1=dramatic(-30°), 2=overhead(-80°) var _cam_zoom: float = 1.0 # 1.0 = default distance, <1.0 = zoomed in var _cam_zoom_offset: Vector3 = Vector3.ZERO # camera offset toward cursor when zoomed @@ -128,42 +204,42 @@ var _active_clothing_slot: String = "torso" var _active_accessory_slot: String = "hat" # --- Body/skin tone buttons shared across Body + Head tabs --- -var _body_type_btns: Dictionary = {} # BodyType int -> Button +var _body_type_btns: Dictionary = {} # BodyType int -> Button var _body_skin_btns: Array[Button] = [] # 9 buttons in Body tab skin dock var _head_skin_btns: Array[Button] = [] # 9 buttons in Head tab skin dock # --- Head / hair buttons --- -var _head_item_btns: Dictionary = {} # head_id -> Button -var _hair_item_btns: Dictionary = {} # hair_id -> Button +var _head_item_btns: Dictionary = {} # head_id -> Button +var _hair_item_btns: Dictionary = {} # hair_id -> Button var _facial_hair_btns: Array[Button] = [] var _eyebrow_btns: Array[Button] = [] # --- Clothing/accessory grid content per slot --- -var _clothing_grids: Dictionary = {} # slot -> GridContainer +var _clothing_grids: Dictionary = {} # slot -> GridContainer var _clothing_slot_btns: Array[Button] = [] -var _clothing_item_btns: Dictionary = {} # slot -> Dictionary {item_id -> Button} -var _accessory_grids: Dictionary = {} # slot -> GridContainer +var _clothing_item_btns: Dictionary = {} # slot -> Dictionary {item_id -> Button} +var _accessory_grids: Dictionary = {} # slot -> GridContainer var _accessory_slot_btns: Array[Button] = [] -var _accessory_item_btns: Dictionary = {} # slot -> Dictionary {item_id -> Button} +var _accessory_item_btns: Dictionary = {} # slot -> Dictionary {item_id -> Button} # --- Color dock swatch references --- var _hair_primary_swatch: Button = null var _hair_highlight_swatch: Button = null var _eyebrow_tint_swatch: Button = null var _facial_hair_tint_swatch: Button = null -var _clothing_primary_swatches: Dictionary = {} # slot -> Button -var _clothing_secondary_swatches: Dictionary = {} # slot -> Button -var _clothing_accent_swatches: Dictionary = {} # slot -> Button +var _clothing_primary_swatches: Dictionary = {} # slot -> Button +var _clothing_secondary_swatches: Dictionary = {} # slot -> Button +var _clothing_accent_swatches: Dictionary = {} # slot -> Button var _accessory_primary_swatches: Dictionary = {} # slot -> Button -var _accessory_secondary_swatches: Dictionary = {} # slot -> Button +var _accessory_secondary_swatches: Dictionary = {} # slot -> Button # --- Auto-derive flags (true = auto-derive from hair/clothing primary) --- # Note: hair highlight is always auto-derived (display-only swatch, no override). var _eyebrow_tint_auto: bool = true var _facial_hair_tint_auto: bool = true -var _clothing_secondary_auto: Dictionary = {} # slot -> bool -var _clothing_accent_auto: Dictionary = {} # slot -> bool -var _accessory_secondary_auto: Dictionary = {} # slot -> bool +var _clothing_secondary_auto: Dictionary = {} # slot -> bool +var _clothing_accent_auto: Dictionary = {} # slot -> bool +var _accessory_secondary_auto: Dictionary = {} # slot -> bool # --- Color picker modal state --- var _modal_callback: Callable @@ -171,7 +247,7 @@ var _modal_original_color: Color = Color.WHITE var _modal_preview_swatch: Button = null # the swatch button being edited var _modal_hex_input: LineEdit = null var _modal_swatch_btns: Array[Button] = [] # 54 palette swatches -var _recent_colors: Array[Color] = [] # persisted recent custom colors +var _recent_colors: Array[Color] = [] # persisted recent custom colors var _modal_recent_btns: Array[Button] = [] # --- Dock container references (set during tab build; avoid fragile child-index traversal) --- @@ -207,25 +283,31 @@ var _tab_container: TabContainer = null # --- @onready references to .tscn nodes --- @onready var _viewport: SubViewport = $Layout/PreviewPanel/SubViewportContainer/SubViewport -@onready var _char_anchor: Node3D = $Layout/PreviewPanel/SubViewportContainer/SubViewport/CharacterVisualAnchor -@onready var _preview_camera: Camera3D = $Layout/PreviewPanel/SubViewportContainer/SubViewport/PreviewCamera -@onready var _rotate_left_btn: Button = $Layout/PreviewPanel/PreviewOverlay/RotateButtons/RotateLeftBtn -@onready var _rotate_right_btn: Button = $Layout/PreviewPanel/PreviewOverlay/RotateButtons/RotateRightBtn +@onready +var _char_anchor: Node3D = $Layout/PreviewPanel/SubViewportContainer/SubViewport/CharacterVisualAnchor +@onready +var _preview_camera: Camera3D = $Layout/PreviewPanel/SubViewportContainer/SubViewport/PreviewCamera +@onready +var _rotate_left_btn: Button = $Layout/PreviewPanel/PreviewOverlay/RotateButtons/RotateLeftBtn +@onready +var _rotate_right_btn: Button = $Layout/PreviewPanel/PreviewOverlay/RotateButtons/RotateRightBtn @onready var _cam_angle_btn: Button = $Layout/PreviewPanel/PreviewOverlay/CamAngleBtn @onready var _footer_back: Button = $Footer/BackBtn @onready var _footer_randomize: Button = $Footer/RandomizeBtn @onready var _footer_start: Button = $Footer/StartBtn @onready var _modal_root: Control = $ColorPickerModal - # ============================================================================= # Lifecycle # ============================================================================= + func _load_manifest() -> void: var file := FileAccess.open(MANIFEST_PATH, FileAccess.READ) if file == null: - push_warning("CharacterCreation: manifest not found at %s — using empty defaults" % MANIFEST_PATH) + push_warning( + "CharacterCreation: manifest not found at %s — using empty defaults" % MANIFEST_PATH + ) _manifest = {} return var parsed: Variant = JSON.parse_string(file.get_as_text()) @@ -277,12 +359,16 @@ func _ready() -> void: if has_hair: tab_builders.append({"name": "Hair", "build": _build_hair_tab, "always": false}) var clothing_data: Variant = _manifest.get("clothing", {}) - var has_clothing: bool = clothing_data is Dictionary and not (clothing_data as Dictionary).is_empty() + var has_clothing: bool = ( + clothing_data is Dictionary and not (clothing_data as Dictionary).is_empty() + ) if has_clothing: tab_builders.append({"name": "Clothing", "build": _build_clothing_tab, "always": false}) var has_accessories := not _manifest_array("accessories").is_empty() if has_accessories: - tab_builders.append({"name": "Accessories", "build": _build_accessories_tab, "always": false}) + tab_builders.append( + {"name": "Accessories", "build": _build_accessories_tab, "always": false} + ) tab_builders.append({"name": "Debug", "build": _build_debug_tab, "always": true}) for tb in tab_builders: @@ -337,6 +423,7 @@ func _ready() -> void: # Camera management (D-158) # ============================================================================= + func _update_camera_angle() -> void: var pitch_deg := CAM_PITCHES[_cam_pitch_idx] var pitch_rad := deg_to_rad(-pitch_deg) # negative = looking downward @@ -359,8 +446,10 @@ func _cam_zoom_toward_cursor(_screen_pos: Vector2, zoom_delta: float) -> void: if _char_visual and _char_visual._skeleton: var head_idx := _char_visual._skeleton.find_bone("Head") if head_idx >= 0: - var head_pos := _char_visual._skeleton.global_transform \ - * _char_visual._skeleton.get_bone_global_pose(head_idx).origin + var head_pos := ( + _char_visual._skeleton.global_transform + * _char_visual._skeleton.get_bone_global_pose(head_idx).origin + ) # Blend from body center toward head as zoom increases var blend := 1.0 - _cam_zoom # 0 at default, 0.75 at max zoom _cam_zoom_offset.y = (head_pos.y - CAM_TARGET_HEIGHT) * blend @@ -384,6 +473,7 @@ func _on_cam_angle_toggle() -> void: # Rotation (D-155: cardinal only, Q=CCW, E=CW) # ============================================================================= + func _on_rotate_left() -> void: _facing_idx = (_facing_idx + 1) % CARDINAL_DIRS.size() _char_visual.set_facing(CARDINAL_DIRS[_facing_idx]) @@ -398,6 +488,7 @@ func _on_rotate_right() -> void: # Preview refresh # ============================================================================= + func _refresh_preview() -> void: if _char_visual and is_instance_valid(_char_visual): _char_visual.load_descriptor(_descriptor) @@ -408,6 +499,7 @@ func _refresh_preview() -> void: # Tab: Body (Task #2) # ============================================================================= + func _build_body_tab(tab: Control) -> void: var vbox := _make_tab_vbox(tab) @@ -475,10 +567,13 @@ func _build_body_tab(tab: Control) -> void: var eye_row := HBoxContainer.new() eye_row.add_theme_constant_override("separation", 8) vbox.add_child(eye_row) - var eye_swatch := _make_color_swatch(_descriptor.eye_color, "Iris", + var eye_swatch := _make_color_swatch( + _descriptor.eye_color, + "Iris", func(c: Color) -> void: _descriptor.eye_color = c - _refresh_preview()) + _refresh_preview() + ) eye_row.add_child(eye_swatch) _update_body_type_btns() @@ -527,6 +622,7 @@ func _update_body_type_btns() -> void: # Tab: Head (Task #3) # ============================================================================= + func _build_head_tab(tab: Control) -> void: var vbox := _make_tab_vbox(tab) @@ -564,10 +660,13 @@ func _build_head_tab(tab: Control) -> void: var eye_row := HBoxContainer.new() eye_row.add_theme_constant_override("separation", 8) vbox.add_child(eye_row) - var eye_swatch := _make_color_swatch(_descriptor.eye_color, "Iris", + var eye_swatch := _make_color_swatch( + _descriptor.eye_color, + "Iris", func(c: Color) -> void: _descriptor.eye_color = c - _refresh_preview()) + _refresh_preview() + ) eye_row.add_child(eye_swatch) _update_head_btns() @@ -589,6 +688,7 @@ func _update_head_btns() -> void: # Tab: Hair (Task #4) # ============================================================================= + func _build_hair_tab(tab: Control) -> void: var vbox := _make_tab_vbox(tab) @@ -669,7 +769,9 @@ func _update_hair_btns() -> void: for hid: String in _hair_item_btns: _set_item_selected(_hair_item_btns[hid], hid == _descriptor.hair_id) for i in FACIAL_HAIR_OPTIONS.size(): - _set_item_selected(_facial_hair_btns[i], FACIAL_HAIR_OPTIONS[i] == _descriptor.facial_hair_id) + _set_item_selected( + _facial_hair_btns[i], FACIAL_HAIR_OPTIONS[i] == _descriptor.facial_hair_id + ) # Eyebrow buttons removed — eyebrows come from body segment only @@ -684,21 +786,25 @@ func _build_hair_color_dock() -> Control: row.add_theme_constant_override("separation", 8) dock.add_child(row) - _hair_primary_swatch = _make_color_swatch(Color(0.55, 0.35, 0.20), "Primary", - func(c): _on_hair_primary_changed(c)) + _hair_primary_swatch = _make_color_swatch( + Color(0.55, 0.35, 0.20), "Primary", func(c): _on_hair_primary_changed(c) + ) row.add_child(_hair_primary_swatch) # #719 (Option B): highlight is auto-derived from primary — display-only, not editable. _hair_highlight_swatch = _make_display_swatch( - _derive_hair_highlight(_descriptor.hair_tint), "Highlight") + _derive_hair_highlight(_descriptor.hair_tint), "Highlight" + ) row.add_child(_hair_highlight_swatch) - _eyebrow_tint_swatch = _make_color_swatch(_descriptor.hair_tint, "Brows ●", - func(c): _on_eyebrow_tint_changed(c)) + _eyebrow_tint_swatch = _make_color_swatch( + _descriptor.hair_tint, "Brows ●", func(c): _on_eyebrow_tint_changed(c) + ) row.add_child(_eyebrow_tint_swatch) - _facial_hair_tint_swatch = _make_color_swatch(_descriptor.hair_tint, "Facial ●", - func(c): _on_facial_hair_tint_changed(c)) + _facial_hair_tint_swatch = _make_color_swatch( + _descriptor.hair_tint, "Facial ●", func(c): _on_facial_hair_tint_changed(c) + ) row.add_child(_facial_hair_tint_swatch) return dock @@ -743,6 +849,7 @@ func _on_facial_hair_tint_changed(color: Color) -> void: # Tab: Clothing (Task #5) # ============================================================================= + func _build_clothing_tab(tab: Control) -> void: var vbox := _make_tab_vbox(tab) @@ -851,23 +958,25 @@ func _rebuild_clothing_color_dock(container: Control) -> void: container.add_child(row) var item_id: String = str(_descriptor.clothing_slots.get(_active_clothing_slot, "")) - var tints: Array = _descriptor.clothing_tints.get(item_id, []) as Array if not item_id.is_empty() else [] as Array + var tints: Array = ( + _descriptor.clothing_tints.get(item_id, []) as Array if not item_id.is_empty() else [] + as Array + ) var primary: Color = tints[0] as Color if tints.size() > 0 else Color(0.7, 0.65, 0.6) var secondary: Color = tints[1] as Color if tints.size() > 1 else _derive_secondary(primary) var accent: Color = tints[2] as Color if tints.size() > 2 else _derive_accent(primary) - var p_swatch := _make_color_swatch(primary, "Primary", - func(c): _on_clothing_primary_changed(c)) + var p_swatch := _make_color_swatch(primary, "Primary", func(c): _on_clothing_primary_changed(c)) row.add_child(p_swatch) _clothing_primary_swatches[_active_clothing_slot] = p_swatch - var s_swatch := _make_color_swatch(secondary, "Secondary ●", - func(c): _on_clothing_secondary_changed(c)) + var s_swatch := _make_color_swatch( + secondary, "Secondary ●", func(c): _on_clothing_secondary_changed(c) + ) row.add_child(s_swatch) _clothing_secondary_swatches[_active_clothing_slot] = s_swatch - var a_swatch := _make_color_swatch(accent, "Accent ●", - func(c): _on_clothing_accent_changed(c)) + var a_swatch := _make_color_swatch(accent, "Accent ●", func(c): _on_clothing_accent_changed(c)) row.add_child(a_swatch) _clothing_accent_swatches[_active_clothing_slot] = a_swatch @@ -902,7 +1011,9 @@ func _on_clothing_primary_changed(color: Color) -> void: tints[0] = color _descriptor.clothing_tints[item_id] = tints if _clothing_secondary_auto.get(_active_clothing_slot, true): - _set_swatch_color(_clothing_secondary_swatches.get(_active_clothing_slot), _derive_secondary(color)) + _set_swatch_color( + _clothing_secondary_swatches.get(_active_clothing_slot), _derive_secondary(color) + ) _refresh_preview() @@ -953,6 +1064,7 @@ func _update_clothing_item_btns() -> void: # Tab: Accessories (Task #6) # ============================================================================= + func _build_accessories_tab(tab: Control) -> void: var vbox := _make_tab_vbox(tab) @@ -1032,17 +1144,22 @@ func _rebuild_accessory_color_dock(container: Control) -> void: container.add_child(row) var item_id: String = str(_descriptor.accessory_slots.get(_active_accessory_slot, "")) - var tints_arr: Array = _descriptor.accessory_tints.get(item_id, []) as Array if not item_id.is_empty() else [] as Array + var tints_arr: Array = ( + _descriptor.accessory_tints.get(item_id, []) as Array if not item_id.is_empty() else [] + as Array + ) var primary: Color = tints_arr[0] as Color if not tints_arr.is_empty() else Color.WHITE var secondary: Color = _derive_secondary(primary) - var p_swatch := _make_color_swatch(primary, "Primary", - func(c): _on_accessory_primary_changed(c)) + var p_swatch := _make_color_swatch( + primary, "Primary", func(c): _on_accessory_primary_changed(c) + ) row.add_child(p_swatch) _accessory_primary_swatches[_active_accessory_slot] = p_swatch - var s_swatch := _make_color_swatch(secondary, "Secondary ●", - func(c): _on_accessory_secondary_changed(c)) + var s_swatch := _make_color_swatch( + secondary, "Secondary ●", func(c): _on_accessory_secondary_changed(c) + ) row.add_child(s_swatch) _accessory_secondary_swatches[_active_accessory_slot] = s_swatch @@ -1103,6 +1220,7 @@ func _update_accessory_item_btns() -> void: # Screenshot & automated testing # ============================================================================= + func _schedule_screenshot() -> void: _screenshot_pending = true _screenshot_frame_count = 0 @@ -1125,10 +1243,10 @@ func _take_screenshot(suffix: String = "") -> void: _char_visual.set_facing(CARDINAL_DIRS[_screenshot_cardinal_idx]) suffix = dir_name - var filename := "charcreator_%s_%s.png" % [ - _descriptor.body_type_key(), - suffix if not suffix.is_empty() else "default" - ] + var filename := ( + "charcreator_%s_%s.png" + % [_descriptor.body_type_key(), suffix if not suffix.is_empty() else "default"] + ) var path := SCREENSHOT_DIR + filename var img := get_viewport().get_texture().get_image() img.save_png(path) @@ -1212,6 +1330,7 @@ func _load_test_config() -> void: # Debug tab — segment visibility toggles # ============================================================================= + func _build_debug_tab(tab: Control) -> void: var vbox := _make_tab_vbox(tab) @@ -1308,6 +1427,7 @@ func _on_debug_toggle_outlines(pressed: bool) -> void: # Color picker modal (Task #7) — D-165 palette # ============================================================================= + func _build_color_picker_modal() -> void: var modal := _modal_root @@ -1496,6 +1616,7 @@ func _update_modal_recent_btns() -> void: # Input handling (Task #8 keyboard nav) # ============================================================================= + func _input(event: InputEvent) -> void: if not visible: return @@ -1547,7 +1668,9 @@ func _input(event: InputEvent) -> void: KEY_TAB: var count := _tab_container.get_tab_count() if key.shift_pressed: - _tab_container.current_tab = ((_tab_container.current_tab - 1) % count + count) % count + _tab_container.current_tab = ( + ((_tab_container.current_tab - 1) % count + count) % count + ) else: _tab_container.current_tab = (_tab_container.current_tab + 1) % count get_viewport().set_input_as_handled() @@ -1557,6 +1680,7 @@ func _input(event: InputEvent) -> void: # Game flow (Task #8) # ============================================================================= + func _on_back() -> void: creation_cancelled.emit() @@ -1569,6 +1693,7 @@ func _on_start() -> void: # Randomize # ============================================================================= + func _on_randomize() -> void: # Body type — pick from manifest only var available_types: Array = _manifest_array("body_types") @@ -1595,7 +1720,9 @@ func _on_randomize() -> void: _descriptor.facial_hair_id = "" # Eye color — random natural tones - _descriptor.eye_color = Color.from_hsv(randf() * 0.15 + 0.05, 0.3 + randf() * 0.5, 0.2 + randf() * 0.5) + _descriptor.eye_color = Color.from_hsv( + randf() * 0.15 + 0.05, 0.3 + randf() * 0.5, 0.2 + randf() * 0.5 + ) # Sync auto tints _eyebrow_tint_auto = true @@ -1615,6 +1742,7 @@ func _on_randomize() -> void: # Shared helpers — skin tone dock # ============================================================================= + func _build_skin_tone_dock() -> HBoxContainer: var row := HBoxContainer.new() row.add_theme_constant_override("separation", 4) @@ -1659,6 +1787,7 @@ func _update_skin_tone_btns() -> void: # Shared helpers — color swatches # ============================================================================= + ## Get the current Color displayed in a swatch button (VBoxContainer → ColorRect). func _get_swatch_color(btn: Button) -> Color: for child in btn.get_children(): @@ -1698,9 +1827,7 @@ func _make_color_swatch(color: Color, label_text: String, callback: Callable) -> vbox.add_child(lbl) # Use _get_swatch_color to read current color at click time (avoids stale closure). - btn.pressed.connect(func(): - _open_color_picker(_get_swatch_color(btn), btn, callback) - ) + btn.pressed.connect(func(): _open_color_picker(_get_swatch_color(btn), btn, callback)) return btn @@ -1754,6 +1881,7 @@ func _set_swatch_color(btn: Button, color: Color) -> void: # Shared helpers — layout builders # ============================================================================= + func _make_tab_vbox(tab: Control) -> VBoxContainer: var margin := MarginContainer.new() margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) @@ -1772,15 +1900,16 @@ func _make_search_bar(tab_idx: int) -> LineEdit: search.placeholder_text = UIStrings.get_text("character_creation.search_placeholder") search.add_theme_color_override("font_color", TEXT_COLOR) search.add_theme_font_size_override("font_size", 12) - search.text_changed.connect(func(q: String): - _tab_search[tab_idx] = q - # Tabs 3/4 filter the active slot's grid; others use _tab_grids by index - if tab_idx == 3 and _clothing_grids.has(_active_clothing_slot): - _apply_search_filter(_clothing_grids[_active_clothing_slot], q) - elif tab_idx == 4 and _accessory_grids.has(_active_accessory_slot): - _apply_search_filter(_accessory_grids[_active_accessory_slot], q) - elif tab_idx < _tab_grids.size() and _tab_grids[tab_idx] != null: - _apply_search_filter(_tab_grids[tab_idx], q) + search.text_changed.connect( + func(q: String): + _tab_search[tab_idx] = q + # Tabs 3/4 filter the active slot's grid; others use _tab_grids by index + if tab_idx == 3 and _clothing_grids.has(_active_clothing_slot): + _apply_search_filter(_clothing_grids[_active_clothing_slot], q) + elif tab_idx == 4 and _accessory_grids.has(_active_accessory_slot): + _apply_search_filter(_accessory_grids[_active_accessory_slot], q) + elif tab_idx < _tab_grids.size() and _tab_grids[tab_idx] != null: + _apply_search_filter(_tab_grids[tab_idx], q) ) return search @@ -1849,7 +1978,9 @@ func _apply_search_filter(grid: GridContainer, query: String) -> void: var lower_q := query.to_lower() for child in grid.get_children(): if child is Button: - child.visible = lower_q.is_empty() or (child as Button).text.to_lower().contains(lower_q) + child.visible = ( + lower_q.is_empty() or (child as Button).text.to_lower().contains(lower_q) + ) func _get_clothing_ids_for_slot(slot: String) -> Array: @@ -1872,21 +2003,31 @@ func _get_accessory_ids_for_slot(slot: String) -> Array: # Accessory items from manifest, filtered by slot name prefix convention. var all_ids: Array = _manifest_array("accessories") match slot: - "hat": return all_ids.filter(func(id: String) -> bool: return id.begins_with("hat")) - "goggles": return all_ids.filter(func(id: String) -> bool: return id.begins_with("goggle")) - "mask": return all_ids.filter(func(id: String) -> bool: return id.begins_with("mask")) - "backpack": return all_ids.filter(func(id: String) -> bool: return id.begins_with("backpack")) - "belt": return all_ids.filter(func(id: String) -> bool: return id.begins_with("belt")) - "wrist_l", "wrist_r": return all_ids.filter(func(id: String) -> bool: return id.begins_with("wrist")) - "earring_l", "earring_r": return all_ids.filter(func(id: String) -> bool: return id.begins_with("earring")) - "necklace": return all_ids.filter(func(id: String) -> bool: return id.begins_with("necklace")) - _: return all_ids # gdlint:ignore = max-returns + "hat": + return all_ids.filter(func(id: String) -> bool: return id.begins_with("hat")) + "goggles": + return all_ids.filter(func(id: String) -> bool: return id.begins_with("goggle")) + "mask": + return all_ids.filter(func(id: String) -> bool: return id.begins_with("mask")) + "backpack": + return all_ids.filter(func(id: String) -> bool: return id.begins_with("backpack")) + "belt": + return all_ids.filter(func(id: String) -> bool: return id.begins_with("belt")) + "wrist_l", "wrist_r": + return all_ids.filter(func(id: String) -> bool: return id.begins_with("wrist")) + "earring_l", "earring_r": + return all_ids.filter(func(id: String) -> bool: return id.begins_with("earring")) + "necklace": + return all_ids.filter(func(id: String) -> bool: return id.begins_with("necklace")) + _: + return all_ids # gdlint:ignore = max-returns # ============================================================================= # Color derivation helpers # ============================================================================= + func _derive_hair_highlight(primary: Color) -> Color: return primary.lightened(0.3) diff --git a/client/ui/checklist_overlay.gd b/client/ui/checklist_overlay.gd index 41f8811e5..69857275e 100644 --- a/client/ui/checklist_overlay.gd +++ b/client/ui/checklist_overlay.gd @@ -9,10 +9,10 @@ extends Control const ChecklistEvaluator = preload("res://scripts/checklist/checklist_evaluator.gd") const BG_COLOR := Color(0.05, 0.05, 0.08, 0.45) -const MET_COLOR := Color("#6bc9a6") # Friendly green — condition met -const UNMET_COLOR := Color("#8890a0") # Dim grey — condition pending -const HEADER_COLOR := Color("#c8d0e0") # Insert text color — header/summary -const COMPLETE_COLOR := Color("#e8c547") # Amber — all conditions met +const MET_COLOR := Color("#6bc9a6") # Friendly green — condition met +const UNMET_COLOR := Color("#8890a0") # Dim grey — condition pending +const HEADER_COLOR := Color("#c8d0e0") # Insert text color — header/summary +const COMPLETE_COLOR := Color("#e8c547") # Amber — all conditions met const FONT_SIZE := 11 const LINE_HEIGHT := 16 const PADDING := Vector2(8, 6) @@ -79,13 +79,17 @@ func _draw() -> void: var box_height: float = PADDING.y * 2 + line_count * LINE_HEIGHT # Calculate box width from longest line - var max_width: float = font.get_string_size(header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x + var max_width: float = ( + font.get_string_size(header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x + ) for r in results: var desc: String = r.get("description", r.get("id", "")) if desc.length() > MAX_DESC_CHARS: desc = desc.substr(0, MAX_DESC_CHARS - 1) + "..." var prefix: String = "[x] " if r.get("met", false) else "[ ] " - var line_width: float = font.get_string_size(prefix + desc, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x + var line_width: float = ( + font.get_string_size(prefix + desc, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x + ) if line_width > max_width: max_width = line_width @@ -96,7 +100,15 @@ func _draw() -> void: # Header var y: float = PADDING.y + FONT_SIZE - draw_string(font, Vector2(PADDING.x, y), header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, header_color) + draw_string( + font, + Vector2(PADDING.x, y), + header_text, + HORIZONTAL_ALIGNMENT_LEFT, + -1, + FONT_SIZE, + header_color + ) # Condition lines for r in results: @@ -107,11 +119,20 @@ func _draw() -> void: if desc.length() > MAX_DESC_CHARS: desc = desc.substr(0, MAX_DESC_CHARS - 1) + "..." var color: Color = MET_COLOR if is_met else UNMET_COLOR - draw_string(font, Vector2(PADDING.x, y), prefix + desc, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, color) + draw_string( + font, + Vector2(PADDING.x, y), + prefix + desc, + HORIZONTAL_ALIGNMENT_LEFT, + -1, + FONT_SIZE, + color + ) # -- Public API --------------------------------------------------------------- + func get_evaluator(): return _evaluator diff --git a/client/ui/debug_console.gd b/client/ui/debug_console.gd index e2b673064..d030e933b 100644 --- a/client/ui/debug_console.gd +++ b/client/ui/debug_console.gd @@ -7,7 +7,7 @@ extends Control ## Settings-toggled; enabled state persisted in user://settings.cfg. ## D-088: triggers Overlay pause while open — sim must not advance during debug input. -signal pause_requested # D-088: pause sim while console is open +signal pause_requested # D-088: pause sim while console is open signal unpause_requested # D-088: unpause sim when console closes const PREFS_PATH := "user://settings.cfg" @@ -97,6 +97,7 @@ func _update_panel_layout() -> void: # -- Input handling -- + func _unhandled_input(event: InputEvent) -> void: if not _enabled: return @@ -155,6 +156,7 @@ func is_open() -> bool: # -- Command input -- + func _on_input_submitted(text: String) -> void: var trimmed := text.strip_edges() _input_line.clear() @@ -199,7 +201,9 @@ func _dispatch(line: String) -> void: z = int(parts[3]) else: _append_text("tp: invalid z '%s' — defaulting to 0" % parts[3], ERROR_COLOR) - _send_debug({"TeleportToPosition": {"x": int(parts[1]), "y": int(parts[2]), "z": z}}) + _send_debug( + {"TeleportToPosition": {"x": int(parts[1]), "y": int(parts[2]), "z": z}} + ) else: var loc := " ".join(PackedStringArray(parts.slice(1))) _send_debug({"TeleportToLocation": loc}) @@ -226,17 +230,23 @@ func _dispatch(line: String) -> void: func _send_debug(kind: Variant) -> void: - var err := SimBridge.send_input({ - "action": InputMapper.Action.DEBUG_COMMAND, - "action_data": kind, - "timestamp_msec": Time.get_ticks_msec(), - }) + var err := ( + SimBridge + . send_input( + { + "action": InputMapper.Action.DEBUG_COMMAND, + "action_data": kind, + "timestamp_msec": Time.get_ticks_msec(), + } + ) + ) if err != OK: _append_text("send error: %s" % error_string(err), ERROR_COLOR) # -- Response display -- + ## Append a server debug response to the output log. Auto-opens console if closed ## (only if console is enabled — respect user's settings toggle). func append_response(response: Dictionary) -> void: @@ -250,6 +260,7 @@ func append_response(response: Dictionary) -> void: # -- Log rendering -- + func _append_text(text: String, color: Color) -> void: var escaped := text.replace("[", "[lb]").replace("]", "[rb]") _log_lines.append("[color=%s]%s[/color]" % [color.to_html(false), escaped]) @@ -261,24 +272,27 @@ func _append_text(text: String, color: Color) -> void: func _print_help() -> void: _append_text( - "Commands:\n" - + " ticks — fast-forward N ticks\n" - + " contaminate — skip to contamination phase\n" - + " tp [z] — teleport to tile position\n" - + " tp — teleport to named location\n" - + " activate — force contamination activate\n" - + " triangle — force triangle activation\n" - + " npc — inspect NPC state\n" - + " triangles — list all triangles\n" - + " pop — list active NPCs\n" - + " status — contamination status\n" - + " help — this list", + ( + "Commands:\n" + + " ticks — fast-forward N ticks\n" + + " contaminate — skip to contamination phase\n" + + " tp [z] — teleport to tile position\n" + + " tp — teleport to named location\n" + + " activate — force contamination activate\n" + + " triangle — force triangle activation\n" + + " npc — inspect NPC state\n" + + " triangles — list all triangles\n" + + " pop — list active NPCs\n" + + " status — contamination status\n" + + " help — this list" + ), TEXT_COLOR ) # -- Command history -- + func _history_up() -> void: if _history.is_empty(): return @@ -299,6 +313,7 @@ func _history_down() -> void: # -- Settings -- + func set_enabled(enabled: bool) -> void: _enabled = enabled if not _enabled and _open: diff --git a/client/ui/debug_overlay.gd b/client/ui/debug_overlay.gd index a80e0e4ca..610ae751b 100644 --- a/client/ui/debug_overlay.gd +++ b/client/ui/debug_overlay.gd @@ -37,7 +37,7 @@ const GRAPH_WARN_COLOR := Color("#e8c547") # Vision cone geometry (radians) # Forward: ±60° around facing direction (120° total) # Peripheral: ±60° to ±120° on each side (60° band each side) -const CONE_FORWARD_HALF: float = PI / 3.0 # 60° +const CONE_FORWARD_HALF: float = PI / 3.0 # 60° const CONE_PERIPHERAL_HALF: float = PI * 2.0 / 3.0 # 120° const CONE_ARC_STEPS: int = 20 @@ -65,14 +65,14 @@ var _npc_paths: Dictionary = {} var _last_tick_processed: int = -1 # Tick timing ring -var _tick_times: Array = [] # Time.get_ticks_msec() on each snapshot +var _tick_times: Array = [] # Time.get_ticks_msec() on each snapshot var _tick_deltas: Array = [] # ms between consecutive snapshots - # --------------------------------------------------------------------------- # Lifecycle # --------------------------------------------------------------------------- + func _ready() -> void: _dev_mode = OS.is_debug_build() visible = false @@ -147,6 +147,7 @@ func _update_npc_paths() -> void: # Draw dispatch # --------------------------------------------------------------------------- + func _draw() -> void: if not visible: return @@ -159,6 +160,7 @@ func _draw() -> void: # Panel 1: Stats text (top-left) # --------------------------------------------------------------------------- + func _draw_stats_panel() -> void: var font: Font = _cached_font if _cached_font else ThemeDB.fallback_font @@ -173,7 +175,9 @@ func _draw_stats_panel() -> void: right_lines.append(["facing", GameState.player_facing]) left_lines.append(["stance", GameState.player_stance]) - right_lines.append(["zone", GameState.current_zone_id if GameState.current_zone_id != "" else "-"]) + right_lines.append( + ["zone", GameState.current_zone_id if GameState.current_zone_id != "" else "-"] + ) left_lines.append(["entities", str(GameState.visible_entities.size())]) right_lines.append(["tiles", str(GameState.visible_tiles.size())]) @@ -190,7 +194,9 @@ func _draw_stats_panel() -> void: right_lines.append(["insert", "ON" if GameState.insert_active else "OFF"]) var gt := GameState.game_time - var time_str := "%s d%s" % [gt.get("day_phase", "-"), str(gt.get("day", "-"))] if gt.size() > 0 else "-" + var time_str := ( + "%s d%s" % [gt.get("day_phase", "-"), str(gt.get("day", "-"))] if gt.size() > 0 else "-" + ) var rate_str: String = gt.get("tick_rate", "-") if gt.size() > 0 else "-" left_lines.append(["time", time_str]) right_lines.append(["tick_rate", rate_str]) @@ -210,14 +216,26 @@ func _draw_stats_panel() -> void: var right_value_w: float = 0.0 for line in left_lines: - left_label_w = max(left_label_w, font.get_string_size(line[0] + ": ", HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x) - left_value_w = max(left_value_w, font.get_string_size(line[1], HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x) + left_label_w = max( + left_label_w, + font.get_string_size(line[0] + ": ", HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x + ) + left_value_w = max( + left_value_w, font.get_string_size(line[1], HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x + ) for line in right_lines: - right_label_w = max(right_label_w, font.get_string_size(line[0] + ": ", HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x) - right_value_w = max(right_value_w, font.get_string_size(line[1], HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x) + right_label_w = max( + right_label_w, + font.get_string_size(line[0] + ": ", HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x + ) + right_value_w = max( + right_value_w, font.get_string_size(line[1], HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x + ) var header_text := "F3 DEBUG" - var header_w := font.get_string_size(header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE + 1).x + var header_w := ( + font.get_string_size(header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE + 1).x + ) var content_w := left_label_w + left_value_w + COL_GAP + right_label_w + right_value_w var box_w: float = max(header_w, content_w) + PADDING.x * 2 var line_count: int = maxi(left_lines.size(), right_lines.size()) @@ -226,21 +244,57 @@ func _draw_stats_panel() -> void: draw_rect(Rect2(Vector2.ZERO, Vector2(box_w, box_h)), BG_COLOR) var y: float = PADDING.y + FONT_SIZE - draw_string(font, Vector2(PADDING.x, y), header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE + 1, HEADER_COLOR) + draw_string( + font, + Vector2(PADDING.x, y), + header_text, + HORIZONTAL_ALIGNMENT_LEFT, + -1, + FONT_SIZE + 1, + HEADER_COLOR + ) y += LINE_HEIGHT var right_x: float = PADDING.x + left_label_w + left_value_w + COL_GAP for i in range(line_count): if i < left_lines.size(): - draw_string(font, Vector2(PADDING.x, y), left_lines[i][0] + ": ", - HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, LABEL_COLOR) - draw_string(font, Vector2(PADDING.x + left_label_w, y), left_lines[i][1], - HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, VALUE_COLOR) + draw_string( + font, + Vector2(PADDING.x, y), + left_lines[i][0] + ": ", + HORIZONTAL_ALIGNMENT_LEFT, + -1, + FONT_SIZE, + LABEL_COLOR + ) + draw_string( + font, + Vector2(PADDING.x + left_label_w, y), + left_lines[i][1], + HORIZONTAL_ALIGNMENT_LEFT, + -1, + FONT_SIZE, + VALUE_COLOR + ) if i < right_lines.size(): - draw_string(font, Vector2(right_x, y), right_lines[i][0] + ": ", - HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, LABEL_COLOR) - draw_string(font, Vector2(right_x + right_label_w, y), right_lines[i][1], - HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, VALUE_COLOR) + draw_string( + font, + Vector2(right_x, y), + right_lines[i][0] + ": ", + HORIZONTAL_ALIGNMENT_LEFT, + -1, + FONT_SIZE, + LABEL_COLOR + ) + draw_string( + font, + Vector2(right_x + right_label_w, y), + right_lines[i][1], + HORIZONTAL_ALIGNMENT_LEFT, + -1, + FONT_SIZE, + VALUE_COLOR + ) y += LINE_HEIGHT @@ -248,6 +302,7 @@ func _draw_stats_panel() -> void: # Panel 2: World overlays # --------------------------------------------------------------------------- + func _draw_world_overlays() -> void: var vp := get_viewport() if vp == null: @@ -291,9 +346,15 @@ func _draw_vision_cone(player_screen: Vector2, canvas_xf: Transform2D) -> void: var perip_r_from := facing_angle + CONE_FORWARD_HALF var perip_r_to := facing_angle + CONE_PERIPHERAL_HALF - draw_colored_polygon(_arc_polygon(player_screen, r, forward_from, forward_to), CONE_FORWARD_COLOR) - draw_colored_polygon(_arc_polygon(player_screen, r, perip_l_from, perip_l_to), CONE_PERIPHERAL_COLOR) - draw_colored_polygon(_arc_polygon(player_screen, r, perip_r_from, perip_r_to), CONE_PERIPHERAL_COLOR) + draw_colored_polygon( + _arc_polygon(player_screen, r, forward_from, forward_to), CONE_FORWARD_COLOR + ) + draw_colored_polygon( + _arc_polygon(player_screen, r, perip_l_from, perip_l_to), CONE_PERIPHERAL_COLOR + ) + draw_colored_polygon( + _arc_polygon(player_screen, r, perip_r_from, perip_r_to), CONE_PERIPHERAL_COLOR + ) # Forward arc boundary ring draw_arc(player_screen, r, forward_from, forward_to, CONE_ARC_STEPS, CONE_RING_COLOR, 1.0) @@ -303,7 +364,9 @@ func _draw_vision_cone(player_screen: Vector2, canvas_xf: Transform2D) -> void: # Build a filled polygon fan from center through an arc -func _arc_polygon(center: Vector2, radius: float, angle_from: float, angle_to: float) -> PackedVector2Array: +func _arc_polygon( + center: Vector2, radius: float, angle_from: float, angle_to: float +) -> PackedVector2Array: var pts := PackedVector2Array() pts.append(center) for i in range(CONE_ARC_STEPS + 1): @@ -339,8 +402,14 @@ func _draw_npc_paths(canvas_xf: Transform2D) -> void: var a_screen := _w2s(path[i - 1], canvas_xf) var b_screen := _w2s(path[i], canvas_xf) var alpha := float(i) / float(path.size()) - draw_line(a_screen, b_screen, - Color(NPC_PATH_COLOR.r, NPC_PATH_COLOR.g, NPC_PATH_COLOR.b, NPC_PATH_COLOR.a * alpha), 1.5) + draw_line( + a_screen, + b_screen, + Color( + NPC_PATH_COLOR.r, NPC_PATH_COLOR.g, NPC_PATH_COLOR.b, NPC_PATH_COLOR.a * alpha + ), + 1.5 + ) draw_circle(_w2s(path.back(), canvas_xf), NPC_DOT_RADIUS, NPC_DOT_COLOR) @@ -377,17 +446,25 @@ func _draw_info_tags(canvas_xf: Transform2D) -> void: entity_screen.x - text_w / 2.0 - 3.0, tag_baseline - FONT_SIZE + 2.0, text_w + 6.0, - FONT_SIZE) + FONT_SIZE + ) draw_rect(tag_rect, TAG_BG_COLOR) - draw_string(font, + draw_string( + font, Vector2(entity_screen.x - text_w / 2.0, tag_baseline), - label, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE - 1, TAG_TEXT_COLOR) + label, + HORIZONTAL_ALIGNMENT_LEFT, + -1, + FONT_SIZE - 1, + TAG_TEXT_COLOR + ) # --------------------------------------------------------------------------- # Panel 3: Tick timing sparkline (bottom-left) # --------------------------------------------------------------------------- + func _draw_tick_graph() -> void: if _tick_deltas.size() < 2: return @@ -398,10 +475,15 @@ func _draw_tick_graph() -> void: var box_y := vp_size.y - GRAPH_H - label_h - GRAPH_MARGIN draw_rect(Rect2(box_x, box_y, GRAPH_W, GRAPH_H + label_h), GRAPH_BG_COLOR) - draw_string(font, + draw_string( + font, Vector2(box_x + 4, box_y + FONT_SIZE + 1), "tick ms (n=%d)" % _tick_deltas.size(), - HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE - 1, LABEL_COLOR) + HORIZONTAL_ALIGNMENT_LEFT, + -1, + FONT_SIZE - 1, + LABEL_COLOR + ) var chart_top := box_y + label_h var chart_left := box_x + 4.0 @@ -418,9 +500,12 @@ func _draw_tick_graph() -> void: # Warn threshold dashed line var warn_y := chart_top + chart_h * (1.0 - TICK_WARN_MS / max_ms) draw_dashed_line( - Vector2(chart_left, warn_y), Vector2(chart_left + chart_w, warn_y), + Vector2(chart_left, warn_y), + Vector2(chart_left + chart_w, warn_y), Color(GRAPH_WARN_COLOR.r, GRAPH_WARN_COLOR.g, GRAPH_WARN_COLOR.b, 0.3), - 1.0, 4.0) + 1.0, + 4.0 + ) # Sparkline var n := _tick_deltas.size() @@ -441,25 +526,40 @@ func _draw_tick_graph() -> void: for d in _tick_deltas: avg_ms += float(d) avg_ms /= float(_tick_deltas.size()) - draw_string(font, + draw_string( + font, Vector2(chart_left + chart_w - 54.0, chart_top + chart_h + FONT_SIZE - 2), "avg %.0fms" % avg_ms, - HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE - 1, LABEL_COLOR) + HORIZONTAL_ALIGNMENT_LEFT, + -1, + FONT_SIZE - 1, + LABEL_COLOR + ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- + # Convert facing string to angle in radians (Godot 2D: 0=East, -PI/2=North) static func _facing_to_angle(facing: String) -> float: match facing: - "North": return -PI / 2.0 - "Northeast": return -PI / 4.0 - "East": return 0.0 - "Southeast": return PI / 4.0 - "South": return PI / 2.0 - "Southwest": return PI * 3.0 / 4.0 - "West": return PI - "Northwest": return -PI * 3.0 / 4.0 - _: return -PI / 2.0 + "North": + return -PI / 2.0 + "Northeast": + return -PI / 4.0 + "East": + return 0.0 + "Southeast": + return PI / 4.0 + "South": + return PI / 2.0 + "Southwest": + return PI * 3.0 / 4.0 + "West": + return PI + "Northwest": + return -PI * 3.0 / 4.0 + _: + return -PI / 2.0 diff --git a/client/ui/dialogue_box.gd b/client/ui/dialogue_box.gd index faea0c99f..d9895629b 100644 --- a/client/ui/dialogue_box.gd +++ b/client/ui/dialogue_box.gd @@ -13,7 +13,7 @@ extends Control signal option_selected(response_id: String, text: String) signal dialogue_dismissed # Walk-away or conversation end signal confrontation_monologue(text: String, duration: float) # D-063: beat monologue -signal pause_requested # D-061: auto-pause — main.gd routes through input recording (#507) +signal pause_requested # D-061: auto-pause — main.gd routes through input recording (#507) signal unpause_requested # D-061: auto-unpause # D-020 (#558): Decoupled signals — dialogue_box emits, main.gd (coordinator) handles. # Replaces direct GameState.dialogue_active mutation and AudioManager calls. @@ -25,8 +25,8 @@ const THEME_PATH: String = "res://data/dialogue-theme.yaml" const FADE_IN: float = 0.2 const FADE_OUT: float = 0.3 # D-064: 300ms fade on walk-away -const MAX_OPTIONS: int = 3 # D-061: max 3 response options visible -const MAX_HEIGHT_RATIO: float = 0.2 # D-061: max 20% viewport height +const MAX_OPTIONS: int = 3 # D-061: max 3 response options visible +const MAX_HEIGHT_RATIO: float = 0.2 # D-061: max 20% viewport height const MAX_WIDTH_PX: float = Constants.DIALOGUE_MAX_WIDTH # D-076: 640px (OQ-29) const CONFRONTATION_BEAT_DURATION: float = 1.5 # D-063: pause before sending const CONFRONTATION_DIM_ALPHA: float = 0.7 # D-063: dialogue box dims during beat @@ -38,8 +38,14 @@ const MIN_CONTRAST_LUMINANCE: float = 0.25 # Floor for name colour brightness a # D-064: movement actions that trigger walk-away const _WALK_AWAY_ACTIONS: Array[StringName] = [ - &"move_north", &"move_south", &"move_east", &"move_west", - &"move_northeast", &"move_southeast", &"move_southwest", &"move_northwest", + &"move_north", + &"move_south", + &"move_east", + &"move_west", + &"move_northeast", + &"move_southeast", + &"move_southwest", + &"move_northwest", ] # -- Log state -- @@ -54,7 +60,6 @@ var _log_dirty: bool = false # Dirty flag — prevents per-frame O(n) BBCode re # Populated from server events; drives retroactive re-render when NPC names resolve. var _entity_display: Dictionary = {} - # -- Option state -- var _option_controls: Array[Control] = [] var _option_response_ids: Array[String] = [] @@ -68,8 +73,8 @@ var _npc_name: String = "" # v0.1: colors are per-conversation — cleared in _end_player_conversation() to avoid # palette exhaustion (8 entries) across long sessions with 9+ NPCs. var _npc_entity_colors: Dictionary = {} # entity_id -> Color -var _npc_entity_id: int = -1 # Entity ID of the current player conversation NPC -var _next_npc_color: int = 0 # Round-robin palette index for client-side assignment +var _npc_entity_id: int = -1 # Entity ID of the current player conversation NPC +var _next_npc_color: int = 0 # Round-robin palette index for client-side assignment # -- UI state -- var _active_tween: Tween = null @@ -86,7 +91,8 @@ var _entry_fade: float = 5.0 @onready var panel: PanelContainer = $PanelContainer @onready var dialogue_log: RichTextLabel = $PanelContainer/MarginContainer/VBoxContainer/DialogueLog -@onready var options_container: VBoxContainer = $PanelContainer/MarginContainer/VBoxContainer/OptionsContainer +@onready +var options_container: VBoxContainer = $PanelContainer/MarginContainer/VBoxContainer/OptionsContainer func _ready() -> void: @@ -114,15 +120,17 @@ func _unhandled_input(event: InputEvent) -> void: # Number keys 1-3 select dialogue options if event is InputEventKey and event.pressed: var key_index := -1 - if event.keycode == KEY_1: key_index = 0 - elif event.keycode == KEY_2: key_index = 1 - elif event.keycode == KEY_3: key_index = 2 + if event.keycode == KEY_1: + key_index = 0 + elif event.keycode == KEY_2: + key_index = 1 + elif event.keycode == KEY_3: + key_index = 2 if key_index >= 0 and key_index < _option_controls.size(): get_viewport().set_input_as_handled() _on_option_pressed(key_index) return - # D-064: WASD during active player dialogue → walk-away if event is InputEventKey and event.pressed: for action in _WALK_AWAY_ACTIONS: @@ -136,11 +144,18 @@ func _unhandled_input(event: InputEvent) -> void: # -- Theme loading -- + func _load_theme() -> void: # Default NPC palette in case file not found _npc_colors = [ - Color("#4a9ebb"), Color("#6bc9a6"), Color("#e8c547"), Color("#d49e5d"), - Color("#b586d4"), Color("#d45d5d"), Color("#5daa7d"), Color("#7daccc"), + Color("#4a9ebb"), + Color("#6bc9a6"), + Color("#e8c547"), + Color("#d49e5d"), + Color("#b586d4"), + Color("#d45d5d"), + Color("#5daa7d"), + Color("#7daccc"), ] if not FileAccess.file_exists(THEME_PATH): @@ -180,6 +195,7 @@ func _load_theme() -> void: # -- Responsive layout -- + func _update_layout() -> void: var vp := get_viewport_rect().size var max_h := vp.y * MAX_HEIGHT_RATIO @@ -191,14 +207,21 @@ func _update_layout() -> void: # -- Log entry management -- + ## Append a dialogue line to the log. ## speaker/target: display names. text: the spoken line. ## is_passive: true for overheard NPC-NPC (renders with ┃ prefix + desaturated). ## Active conversation entries are pinned (no timeout) while _in_player_conversation. ## speaker_entity_id/target_entity_id: optional entity IDs for stable color lookup (#573). ## TODO Phase 2: 6 positional params is unwieldy — consider dictionary-options overload. -func append_line(speaker: String, target: String, text: String, - is_passive: bool = false, speaker_entity_id: int = -1, target_entity_id: int = -1) -> void: +func append_line( + speaker: String, + target: String, + text: String, + is_passive: bool = false, + speaker_entity_id: int = -1, + target_entity_id: int = -1 +) -> void: var pinned := not is_passive and _in_player_conversation var entry: Dictionary = { "speaker": speaker, @@ -294,10 +317,13 @@ func append_dialogue_response(npc_name: String, text: String, entity_id: int = - # -- Active player conversation -- + ## Show dialogue with NPC speech and response options. ## npc_name: who is speaking. speech: the NPC's line. options: player choices. ## npc_entity_id: entity ID of the NPC for stable color assignment (#573). -func show_dialogue(npc_name: String, speech: String, options: Array = [], npc_entity_id: int = -1) -> void: +func show_dialogue( + npc_name: String, speech: String, options: Array = [], npc_entity_id: int = -1 +) -> void: _npc_name = npc_name _npc_entity_id = npc_entity_id _cancel_beat() @@ -379,6 +405,7 @@ func has_active_entries() -> bool: # -- Options -- + func _show_options(options: Array) -> void: var sorted_opts: Array = options.duplicate() sorted_opts.sort_custom(func(a, b): return a.get("priority", 0) < b.get("priority", 0)) @@ -398,12 +425,15 @@ func _show_options(options: Array) -> void: rtl.fit_content = true rtl.scroll_active = false rtl.add_theme_font_size_override("normal_font_size", 14) - rtl.add_theme_color_override("default_color", Color( - Constants.INSERT_COLOR_TEXT.r * 1.08, - Constants.INSERT_COLOR_TEXT.g * 0.96, - Constants.INSERT_COLOR_TEXT.b * 0.90, - 1.0 - )) # Slight warm tint for confrontation weight + rtl.add_theme_color_override( + "default_color", + Color( + Constants.INSERT_COLOR_TEXT.r * 1.08, + Constants.INSERT_COLOR_TEXT.g * 0.96, + Constants.INSERT_COLOR_TEXT.b * 0.90, + 1.0 + ) + ) # Slight warm tint for confrontation weight rtl.mouse_filter = Control.MOUSE_FILTER_STOP rtl.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND rtl.text = "[i]%d. %s[/i]" % [i + 1, raw_text] @@ -419,9 +449,14 @@ func _show_options(options: Array) -> void: ctrl = label var idx := i - ctrl.gui_input.connect(func(event: InputEvent): - if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: - _on_option_pressed(idx) + ctrl.gui_input.connect( + func(event: InputEvent): + if ( + event is InputEventMouseButton + and event.pressed + and event.button_index == MOUSE_BUTTON_LEFT + ): + _on_option_pressed(idx) ) ctrl.mouse_entered.connect(_make_hover_on(ctrl)) ctrl.mouse_exited.connect(_make_hover_off(ctrl)) @@ -438,7 +473,9 @@ func _on_option_pressed(index: int) -> void: return var rid: String = _option_response_ids[index] if index < _option_response_ids.size() else "" var text: String = _option_texts[index] if index < _option_texts.size() else "" - var is_confront: bool = _option_is_confrontation[index] if index < _option_is_confrontation.size() else false + var is_confront: bool = ( + _option_is_confrontation[index] if index < _option_is_confrontation.size() else false + ) # Append player's chosen response to the log append_player_line(_npc_name, text) @@ -452,6 +489,7 @@ func _on_option_pressed(index: int) -> void: # -- Confrontation beat (D-063) -- + func _start_confrontation_beat(response_id: String, text: String) -> void: for ctrl in _option_controls: if is_instance_valid(ctrl): @@ -462,15 +500,18 @@ func _start_confrontation_beat(response_id: String, text: String) -> void: _active_tween = create_tween() _active_tween.tween_property(panel, "modulate:a", CONFRONTATION_DIM_ALPHA, 0.2) - confrontation_monologue.emit(UIStrings.get_text(CONFRONTATION_MONOLOGUE_KEY), CONFRONTATION_BEAT_DURATION) + confrontation_monologue.emit( + UIStrings.get_text(CONFRONTATION_MONOLOGUE_KEY), CONFRONTATION_BEAT_DURATION + ) audio_dip_requested.emit("confrontation") # D-069: coordinator routes to AudioManager _beat_tween = create_tween() _beat_tween.tween_interval(CONFRONTATION_BEAT_DURATION) - _beat_tween.tween_callback(func(): - audio_dip_cleared.emit() # D-069: coordinator routes to AudioManager - option_selected.emit(response_id, text) - _end_player_conversation() + _beat_tween.tween_callback( + func(): + audio_dip_cleared.emit() # D-069: coordinator routes to AudioManager + option_selected.emit(response_id, text) + _end_player_conversation() ) @@ -483,6 +524,7 @@ func _cancel_beat() -> void: # -- Log rendering -- + ## Rebuild the dialogue log BBCode from all non-expired entries. func _rebuild_log() -> void: if not dialogue_log: @@ -550,7 +592,9 @@ func _format_entry(entry: Dictionary, alpha: float) -> String: target_color = _npc_entity_colors[tg_eid] else: target_color = _color_for_name(entry.get("target", "?")) - involves_player = (entry.get("speaker", "") == PLAYER_NAME) or (entry.get("target", "") == PLAYER_NAME) + involves_player = ( + (entry.get("speaker", "") == PLAYER_NAME) or (entry.get("target", "") == PLAYER_NAME) + ) var text: String = _escape_bbcode(entry.text) var is_passive: bool = entry.is_passive @@ -570,13 +614,14 @@ func _format_entry(entry: Dictionary, alpha: float) -> String: # Non-blocking: simplify 1-on-1 player dialogue — no arrow for Speaker → You or You → Speaker if involves_player and not is_passive: # Just "Speaker: text" or "You: text" - return "%s[color=%s][b]%s[/b][/color][color=%s]: %s[/color]" % [ - prefix, sc, speaker, txc, text - ] + return ( + "%s[color=%s][b]%s[/b][/color][color=%s]: %s[/color]" % [prefix, sc, speaker, txc, text] + ) # Full "Speaker → Target: text" for overheard - return "%s[color=%s][b]%s[/b][/color][color=%s] \u2192 [/color][color=%s][b]%s[/b][/color][color=%s]: %s[/color]" % [ - prefix, sc, speaker, ac, tc, target, txc, text - ] + return ( + "%s[color=%s][b]%s[/b][/color][color=%s] \u2192 [/color][color=%s][b]%s[/b][/color][color=%s]: %s[/color]" + % [prefix, sc, speaker, ac, tc, target, txc, text] + ) ## Escape BBCode bracket characters in server-sourced text (Hoshe #2). @@ -644,6 +689,7 @@ static func _color_with_alpha(base: Color, alpha: float) -> String: # -- Entry expiry -- + ## Remove fully expired entries. Mark dirty if any fading entries exist. ## Pinned entries (active conversation) skip expiry entirely (Araminta #2). func _expire_entries() -> void: @@ -689,6 +735,7 @@ func _expire_entries() -> void: # -- Visibility -- + ## No-op — panel is always visible as a permanent insert UI element. func _ensure_visible() -> void: pass diff --git a/client/ui/examine_display.gd b/client/ui/examine_display.gd index 6cb76280a..14e3899d8 100644 --- a/client/ui/examine_display.gd +++ b/client/ui/examine_display.gd @@ -9,7 +9,7 @@ extends Control ## Positioned in InsertOverlay (CanvasLayer 10, z-layer 6). ## Only one examine result is shown at a time — new result replaces old. -const DISMISS_DELAY: float = 5.0 # Auto-dismiss after 5 seconds +const DISMISS_DELAY: float = 5.0 # Auto-dismiss after 5 seconds const FADE_IN: float = 0.18 const FADE_OUT: float = 0.35 @@ -27,6 +27,7 @@ var _active: bool = false @onready var panel: PanelContainer = $PanelContainer @onready var text_label: RichTextLabel = $PanelContainer/MarginContainer/TextLabel + func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE modulate.a = 0.0 @@ -48,8 +49,12 @@ func show_result(result: Dictionary) -> void: # Apply confidence-based alpha to the insert color var alpha: float = CONFIDENCE_ALPHA.get(confidence, 0.75) - var col := Color(Constants.INSERT_COLOR_TEXT.r, Constants.INSERT_COLOR_TEXT.g, - Constants.INSERT_COLOR_TEXT.b, alpha) + var col := Color( + Constants.INSERT_COLOR_TEXT.r, + Constants.INSERT_COLOR_TEXT.g, + Constants.INSERT_COLOR_TEXT.b, + alpha + ) text_label.add_theme_color_override("default_color", col) text_label.text = text @@ -69,9 +74,10 @@ func _start_fade_out() -> void: return var t := create_tween() t.tween_property(self, "modulate:a", 0.0, FADE_OUT) - t.tween_callback(func(): - visible = false - _active = false + t.tween_callback( + func(): + visible = false + _active = false ) diff --git a/client/ui/gauntlet_hud.gd b/client/ui/gauntlet_hud.gd index ca69b1531..376776c8a 100644 --- a/client/ui/gauntlet_hud.gd +++ b/client/ui/gauntlet_hud.gd @@ -5,9 +5,9 @@ extends Control ## Hidden in non-gauntlet mode. Stats persisted to user://dev/gauntlet-stats.json. const BG_COLOR := Color(0.05, 0.05, 0.08, 0.5) -const TIMER_COLOR := Color("#c8d0e0") # Default insert text -const PB_COLOR := Color("#6bc9a6") # Friendly green — personal best -const NEW_PB_COLOR := Color("#e8c547") # Amber flash on new PB +const TIMER_COLOR := Color("#c8d0e0") # Default insert text +const PB_COLOR := Color("#6bc9a6") # Friendly green — personal best +const NEW_PB_COLOR := Color("#e8c547") # Amber flash on new PB const FONT_SIZE := 13 const PADDING := Vector2(10, 6) const STATS_PATH := "user://dev/gauntlet-stats.json" @@ -109,13 +109,28 @@ func _draw() -> void: # Timer text var y_offset := PADDING.y + text_size.y var timer_size := font.get_string_size(timer_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE) - draw_string(font, Vector2(PADDING.x, y_offset), timer_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, TIMER_COLOR) + draw_string( + font, + Vector2(PADDING.x, y_offset), + timer_text, + HORIZONTAL_ALIGNMENT_LEFT, + -1, + FONT_SIZE, + TIMER_COLOR + ) # PB text (different color) if not pb_text.is_empty(): var pb_color: Color = NEW_PB_COLOR if _new_pb_flash > 0.0 else PB_COLOR - draw_string(font, Vector2(PADDING.x + timer_size.x, y_offset), pb_text, - HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, pb_color) + draw_string( + font, + Vector2(PADDING.x + timer_size.x, y_offset), + pb_text, + HORIZONTAL_ALIGNMENT_LEFT, + -1, + FONT_SIZE, + pb_color + ) static func _format_time(seconds: float) -> String: @@ -154,9 +169,15 @@ func print_session_summary() -> void: for room_id in _session_rooms: var entry: Dictionary = _session_rooms[room_id] var best_str := _format_time(entry["best"]) if entry["best"] != INF else "--:--" - var pb_str := _format_time(_personal_bests[room_id]) if _personal_bests.has(room_id) else "--:--" - print(" Room %s: %d attempts, session best %s, all-time PB %s" % [ - room_id, entry["attempts"], best_str, pb_str]) + var pb_str := ( + _format_time(_personal_bests[room_id]) if _personal_bests.has(room_id) else "--:--" + ) + print( + ( + " Room %s: %d attempts, session best %s, all-time PB %s" + % [room_id, entry["attempts"], best_str, pb_str] + ) + ) print("================================") @@ -170,14 +191,18 @@ func finalize() -> void: # -- Public API --------------------------------------------------------------- + func get_timer_seconds() -> float: return _timer_seconds + func is_timer_running() -> bool: return _timer_running + func get_current_room_id() -> Variant: return _current_room_id + func get_personal_best(for_room_id: String) -> float: return _personal_bests.get(for_room_id, INF) diff --git a/client/ui/hardware_detector.gd b/client/ui/hardware_detector.gd index 03d4137ea..22ad8ed61 100644 --- a/client/ui/hardware_detector.gd +++ b/client/ui/hardware_detector.gd @@ -8,10 +8,10 @@ extends Node ## Loaded as HardwareDetector autoload. Tests access via get_node("/root/HardwareDetector"). ## Settings dialog reads classification strings and power-state methods to configure the UI. -const RAM_PASS_THRESHOLD_MB := 2000.0 +const RAM_PASS_THRESHOLD_MB := 2000.0 const RAM_MARGINAL_THRESHOLD_MB := 1600.0 -const TPT_GREEN_THRESHOLD := 6.0 # tokens/sec — enable silently -const TPT_YELLOW_THRESHOLD := 3.0 # tokens/sec — partial pre-voicing note +const TPT_GREEN_THRESHOLD := 6.0 # tokens/sec — enable silently +const TPT_YELLOW_THRESHOLD := 3.0 # tokens/sec — partial pre-voicing note const TPT_DEGRADATION_THRESHOLD := 0.4 # fraction — >40% sustained drop → yellow ## Benchmark cache path — reads from PlatformInfo for cross-platform correctness. @@ -28,9 +28,9 @@ var inference_suspended: bool = false # User's preference at the moment battery suspension activated (to restore on plug-in). var _pre_battery_pref: bool = true - # -- Lifecycle ---------------------------------------------------------------- + func _ready() -> void: assert(PlatformInfo != null, "PlatformInfo must load before HardwareDetector") load_ai_pref() @@ -40,6 +40,7 @@ func _ready() -> void: # -- Layer 1: RAM classification ---------------------------------------------- + ## Layer 1 — RAM classification. ## Accepts free MB as input; returns "pass" | "marginal" | "fail". func classify_ram(free_mb: float) -> String: @@ -54,11 +55,15 @@ func classify_ram(free_mb: float) -> String: ## Returns: {classification: String, free_mb: float} func check_ram() -> Dictionary: PlatformInfo.refresh_memory() - return {"classification": classify_ram(PlatformInfo.free_memory_mb), "free_mb": PlatformInfo.free_memory_mb} + return { + "classification": classify_ram(PlatformInfo.free_memory_mb), + "free_mb": PlatformInfo.free_memory_mb + } # -- Layer 2: TPT benchmark classification ------------------------------------ + ## Layer 2 — TPT benchmark classification. ## Accepts tokens/sec as input; returns "green" | "yellow" | "red". func classify_tpt(tps: float) -> String: @@ -90,6 +95,7 @@ func read_benchmark_cache() -> Variant: # -- Layer 3: degradation + battery ------------------------------------------- + ## Layer 3 — Ongoing TPT degradation classification. ## D-138 §8: >40% sustained drop from baseline → yellow (not forced disable). ## Returns "ok" | "yellow". @@ -130,6 +136,7 @@ func is_inference_suspended() -> bool: # -- Startup pref load -------------------------------------------------------- + ## Load AI dialogue preference from settings.cfg and apply to GameState. ## Called from _ready() to restore the toggle state across session restarts. ## Spec: D-138 §8 — toggle persists via ConfigFile (client-local) + server SQLite (#627). @@ -144,6 +151,7 @@ func load_ai_pref() -> void: # -- Battery suspend/resume --------------------------------------------------- + func _on_power_profile_changed(old_profile: int, new_profile: int) -> void: if new_profile == PlatformInfo.PowerProfile.BATTERY: _suspend_for_battery() @@ -176,34 +184,49 @@ func _resume_from_battery() -> void: func _send_settings_change(enabled: bool) -> void: if SimBridge.state != SimBridge.ConnectionState.CONNECTED: return - SimBridge.send_input({ - "action": InputMapper.Action.CHANGE_SETTINGS, - "action_data": {"ai_enhanced_dialogue": enabled}, - "timestamp_msec": Time.get_ticks_msec(), - }) + ( + SimBridge + . send_input( + { + "action": InputMapper.Action.CHANGE_SETTINGS, + "action_data": {"ai_enhanced_dialogue": enabled}, + "timestamp_msec": Time.get_ticks_msec(), + } + ) + ) # -- Status message ----------------------------------------------------------- + ## Compose a human-readable status message for the settings dialog. ## classification: "pass" | "marginal" | "fail" (RAM) or "green" | "yellow" | "red" (TPT). ## Returns empty string when no message is needed. -func status_message(ram_classification: String, tpt_classification: String, free_mb: float, tps: float) -> String: +func status_message( + ram_classification: String, tpt_classification: String, free_mb: float, tps: float +) -> String: if ram_classification == "fail": return ( - "AI-Enhanced Dialogue requires 2 GB of free memory. Your system currently has %.0f MB available." + ( + "AI-Enhanced Dialogue requires 2 GB of free memory. Your system currently has %.0f MB available." + " Close other applications and try again, or leave the setting off — the game is complete either way." - ) % free_mb + ) + % free_mb + ) if ram_classification == "marginal": return "Only %.0f MB free — performance may vary. You can still enable it." % free_mb match tpt_classification: "yellow": return ( - "Running at %.0f t/s — pre-voicing will work for main characters and key scenes." + ( + "Running at %.0f t/s — pre-voicing will work for main characters and key scenes." + " Background NPCs may show base text until the queue catches up." - ) % tps + ) + % tps + ) "red": return ( - "Running very slowly at %.0f t/s — we recommend leaving this off, but the choice is yours." - ) % tps + ("Running very slowly at %.0f t/s — we recommend leaving this off, but the choice is yours.") + % tps + ) return "" diff --git a/client/ui/hud.gd b/client/ui/hud.gd index ba4bf70c6..a7ae0c239 100644 --- a/client/ui/hud.gd +++ b/client/ui/hud.gd @@ -6,9 +6,11 @@ extends Control @onready var perception_label: Label = $MarginContainer/VBoxContainer/PerceptionLabel @onready var time_label: Label = $MarginContainer/VBoxContainer/TimeLabel + func _ready() -> void: print("HUD: Initialized") + # Update HUD from snapshot data func update_from_hud_data(data: Dictionary) -> void: # Update perception mode display @@ -27,6 +29,7 @@ func update_from_hud_data(data: Dictionary) -> void: else: time_label.text = prefix + ": " + data.time + # Update player health (from player data, not hud_data) func update_health(health: int) -> void: health_label.text = UIStrings.get_text("hud.health") + ": " + str(health) diff --git a/client/ui/implant/implant_separator.gd b/client/ui/implant/implant_separator.gd index 6150dd65f..9e0a7a27c 100644 --- a/client/ui/implant/implant_separator.gd +++ b/client/ui/implant/implant_separator.gd @@ -20,9 +20,4 @@ func _draw() -> void: if not _theme: return var y: float = _theme.sep_above - draw_line( - Vector2(0, y), - Vector2(size.x, y), - _theme.separator, - 1.0 - ) + draw_line(Vector2(0, y), Vector2(size.x, y), _theme.separator, 1.0) diff --git a/client/ui/implant/implant_text_block.gd b/client/ui/implant/implant_text_block.gd index 8b3b6524b..7a3437dd7 100644 --- a/client/ui/implant/implant_text_block.gd +++ b/client/ui/implant/implant_text_block.gd @@ -25,5 +25,6 @@ func set_content(content: String, max_lines: int = -1) -> void: func apply_implant_theme(t: ImplantTheme) -> void: add_theme_font_size_override("normal_font_size", t.font_small) - add_theme_color_override("default_color", - Color(t.text_primary.r, t.text_primary.g, t.text_primary.b, _text_alpha)) + add_theme_color_override( + "default_color", Color(t.text_primary.r, t.text_primary.g, t.text_primary.b, _text_alpha) + ) diff --git a/client/ui/interaction_list.gd b/client/ui/interaction_list.gd index c8c714cf3..ba8d0acc4 100644 --- a/client/ui/interaction_list.gd +++ b/client/ui/interaction_list.gd @@ -42,6 +42,7 @@ func _ready() -> void: visible = false mouse_filter = Control.MOUSE_FILTER_IGNORE + func _process(_delta: float) -> void: if _showing: _update_screen_position() @@ -113,14 +114,21 @@ func _rebuild_labels() -> void: var lbl := Label.new() lbl.text = " %s " % verb.get("label", "") lbl.add_theme_font_size_override("font_size", 14) - lbl.add_theme_color_override("font_color", INSERT_FG if i == _selected_index else INSERT_DIM) + lbl.add_theme_color_override( + "font_color", INSERT_FG if i == _selected_index else INSERT_DIM + ) lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT lbl.mouse_filter = Control.MOUSE_FILTER_STOP lbl.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND var idx := i - lbl.gui_input.connect(func(event: InputEvent): - if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: - _select_and_interact(idx) + lbl.gui_input.connect( + func(event: InputEvent): + if ( + event is InputEventMouseButton + and event.pressed + and event.button_index == MOUSE_BUTTON_LEFT + ): + _select_and_interact(idx) ) lbl.mouse_entered.connect(func(): _hover_index(idx)) lbl.mouse_exited.connect(func(): _unhover_index(idx)) @@ -155,7 +163,9 @@ func _update_screen_position() -> void: return var viewport_size := get_viewport_rect().size var cam_center := camera.get_screen_center_position() - var zoom: Vector2 = camera.zoom if camera.zoom.length_squared() > 0.01 else Constants.CAMERA_DEFAULT_ZOOM + var zoom: Vector2 = ( + camera.zoom if camera.zoom.length_squared() > 0.01 else Constants.CAMERA_DEFAULT_ZOOM + ) var world_px := _entity_world_pos * Constants.TILE_SIZE var screen_pos := (world_px - cam_center) * zoom + viewport_size / 2.0 # Anchor above the entity, centered horizontally @@ -184,17 +194,19 @@ func _hide() -> void: _active_tween = create_tween() _active_tween.tween_property(self, "modulate:a", 0.0, FADE_OUT) # Guard: tween callback from previous _hide() may fire after labels already freed - _active_tween.tween_callback(func(): - visible = false - for lbl in _verb_labels: - if is_instance_valid(lbl): - lbl.queue_free() - _verb_labels.clear() + _active_tween.tween_callback( + func(): + visible = false + for lbl in _verb_labels: + if is_instance_valid(lbl): + lbl.queue_free() + _verb_labels.clear() ) # -- Lazy sync for getters (tests may set GameState without calling update) -- + func _ensure_synced() -> void: if _current_target_id == -1 and not GameState.nearby_interactions.is_empty(): update_from_state() @@ -202,6 +214,7 @@ func _ensure_synced() -> void: # -- Public API (QA test contract) ------------------------------------------ + func get_visible_verb_count() -> int: _ensure_synced() return _verb_items.size() @@ -254,7 +267,8 @@ func _update_label_colors() -> void: for i in range(_verb_labels.size()): if is_instance_valid(_verb_labels[i]): _verb_labels[i].add_theme_color_override( - "font_color", INSERT_FG if i == _selected_index else INSERT_DIM) + "font_color", INSERT_FG if i == _selected_index else INSERT_DIM + ) func set_insert_active(active: bool) -> void: diff --git a/client/ui/interaction_prompt.gd b/client/ui/interaction_prompt.gd index 6e827ca98..10d36f8fb 100644 --- a/client/ui/interaction_prompt.gd +++ b/client/ui/interaction_prompt.gd @@ -19,11 +19,13 @@ var _insert_active: bool = true @onready var prompt_label: Label = $MarginContainer/PromptLabel + func _ready() -> void: modulate.a = 0.0 visible = false _is_showing = false + func _process(_delta: float) -> void: # OQ-07: insert off means no verb labels (z-layer 6 insert overlay suppressed) if not _insert_active: @@ -36,6 +38,7 @@ func _process(_delta: float) -> void: elif _is_showing: _hide_prompt() + func _show_prompt(interaction: Dictionary) -> void: var target_id: int = interaction.get("entity_id", -1) var verbs: Array = interaction.get("verbs", []) @@ -61,6 +64,7 @@ func _show_prompt(interaction: Dictionary) -> void: _active_tween = create_tween() _active_tween.tween_property(self, "modulate:a", 1.0, FADE_IN) + func _hide_prompt() -> void: if not _is_showing: return @@ -72,10 +76,12 @@ func _hide_prompt() -> void: _active_tween.tween_property(self, "modulate:a", 0.0, FADE_OUT) _active_tween.tween_callback(func(): visible = false) + ## Returns the current interaction target entity ID, or -1 if no interaction. func get_interaction_target() -> int: return _current_target_id + ## OQ-07 (#522): insert off hides prompt (diegetic: no insert data on z-layer 6). ## Cursor shape changes still fire on cursor_renderer.gd. func set_insert_active(active: bool) -> void: diff --git a/client/ui/inventory_grid.gd b/client/ui/inventory_grid.gd index 52be53dcb..d062146b4 100644 --- a/client/ui/inventory_grid.gd +++ b/client/ui/inventory_grid.gd @@ -67,10 +67,7 @@ func _draw() -> void: var col: int = slot_idx % GRID_COLS var row: int = slot_idx / GRID_COLS - var pos := Vector2( - col * (SLOT_SIZE + SLOT_GAP), - row * (SLOT_SIZE + SLOT_GAP) - ) + var pos := Vector2(col * (SLOT_SIZE + SLOT_GAP), row * (SLOT_SIZE + SLOT_GAP)) var rect := Rect2(pos, Vector2(SLOT_SIZE, SLOT_SIZE)) # Slot background @@ -89,13 +86,22 @@ func _draw() -> void: var font := ThemeDB.fallback_font var font_size := 11 var text_size := font.get_string_size(item_name, HORIZONTAL_ALIGNMENT_CENTER, -1, font_size) - var text_pos := pos + Vector2((SLOT_SIZE - text_size.x) / 2.0, SLOT_SIZE / 2.0 + text_size.y / 4.0) + var text_pos := ( + pos + Vector2((SLOT_SIZE - text_size.x) / 2.0, SLOT_SIZE / 2.0 + text_size.y / 4.0) + ) draw_string(font, text_pos, item_name, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, SLOT_TEXT) # Hotkey number (top-left corner) var hotkey := str(slot_idx + 1) - draw_string(font, pos + Vector2(3, HOTKEY_SIZE + 2), hotkey, - HORIZONTAL_ALIGNMENT_LEFT, -1, HOTKEY_SIZE, SLOT_TEXT_DIM) + draw_string( + font, + pos + Vector2(3, HOTKEY_SIZE + 2), + hotkey, + HORIZONTAL_ALIGNMENT_LEFT, + -1, + HOTKEY_SIZE, + SLOT_TEXT_DIM + ) func _unhandled_input(event: InputEvent) -> void: @@ -125,6 +131,7 @@ func _select_slot(slot_idx: int) -> void: # -- Public API --------------------------------------------------------------- + func get_slot_count() -> int: return _slots.size() diff --git a/client/ui/journal_panel.gd b/client/ui/journal_panel.gd index bb1863b59..34616e539 100644 --- a/client/ui/journal_panel.gd +++ b/client/ui/journal_panel.gd @@ -29,8 +29,9 @@ var _last_rendered_tick: int = -1 @onready var panel: PanelContainer = $PanelContainer @onready var title_label: Label = $PanelContainer/MarginContainer/VBoxContainer/TitleLabel @onready var scroll: ScrollContainer = $PanelContainer/MarginContainer/VBoxContainer/ScrollContainer -@onready var entries_container: VBoxContainer = \ - $PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/EntriesContainer +@onready +var entries_container: VBoxContainer = $PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/EntriesContainer + func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE @@ -118,12 +119,16 @@ func _add_entity_entry(entity: Dictionary) -> void: header_rtl.add_theme_font_size_override("normal_font_size", 14) var rel_color: Color = Constants.color_for_relationship(relationship) - var rel_label: String = UIStrings.get_text("relationship_states.%s.label" % relationship.to_lower()) + var rel_label: String = UIStrings.get_text( + "relationship_states.%s.label" % relationship.to_lower() + ) if rel_label == "relationship_states.%s.label" % relationship.to_lower(): rel_label = relationship # fallback if key missing var state_color: Color = _state_color(state) - var conf_label: String = UIStrings.get_text("knowledge_panel.confidence_%s" % confidence.to_lower()) + var conf_label: String = UIStrings.get_text( + "knowledge_panel.confidence_%s" % confidence.to_lower() + ) var src_label: String = _resolve_source_label(source) # Build BBCode: @@ -133,13 +138,15 @@ func _add_entity_entry(entity: Dictionary) -> void: var header_bbcode: String if state == "Contradicted": - header_bbcode = "[color=#%s][b][s]%s[/s][/b][/color] [color=#%s]%s[/color]" % [ - name_hex, name_str, rel_hex, rel_label - ] + header_bbcode = ( + "[color=#%s][b][s]%s[/s][/b][/color] [color=#%s]%s[/color]" + % [name_hex, name_str, rel_hex, rel_label] + ) else: - header_bbcode = "[color=#%s][b]%s[/b][/color] [color=#%s]%s[/color]" % [ - name_hex, name_str, rel_hex, rel_label - ] + header_bbcode = ( + "[color=#%s][b]%s[/b][/color] [color=#%s]%s[/color]" + % [name_hex, name_str, rel_hex, rel_label] + ) header_rtl.text = header_bbcode entries_container.add_child(header_rtl) @@ -198,9 +205,9 @@ func _entity_name_for_id(entity_id: int) -> String: func _state_color(state: String) -> Color: match state: "Contradicted": - return Constants.ENTITY_COLOR_POI # amber — THE FRIEND arc + return Constants.ENTITY_COLOR_POI # amber — THE FRIEND arc "Stale": - return Constants.IMPLANT_TEXT_DIM # dimmed + return Constants.IMPLANT_TEXT_DIM # dimmed _: return Constants.INSERT_COLOR_TEXT # active — normal diff --git a/client/ui/main_menu.gd b/client/ui/main_menu.gd index 4e406ccb9..d578c3114 100644 --- a/client/ui/main_menu.gd +++ b/client/ui/main_menu.gd @@ -156,8 +156,15 @@ static func _format_save_entry(save: Dictionary) -> String: if parts.size() >= 2 and parts[0].length() == 8 and parts[1].length() == 6: var d := parts[0] var t := parts[1] - return "%s-%s-%s %s:%s:%s" % [ - d.substr(0, 4), d.substr(4, 2), d.substr(6, 2), - t.substr(0, 2), t.substr(2, 2), t.substr(4, 2), - ] + return ( + "%s-%s-%s %s:%s:%s" + % [ + d.substr(0, 4), + d.substr(4, 2), + d.substr(6, 2), + t.substr(0, 2), + t.substr(2, 2), + t.substr(4, 2), + ] + ) return game_id diff --git a/client/ui/minimap.gd b/client/ui/minimap.gd index 0f27ee575..35f45f7c6 100644 --- a/client/ui/minimap.gd +++ b/client/ui/minimap.gd @@ -29,27 +29,31 @@ const CARDINAL_TICK_LEN: float = 4.0 # Colors — insert palette from Constants, tuned for the circular minimap frame const COLOR_BG: Color = Color(0.04, 0.07, 0.12, 0.82) -const COLOR_FRAME: Color = Color(0.784, 0.816, 0.878, 0.55) # INSERT_COLOR_TEXT at reduced alpha -const COLOR_NORTH: Color = Color(0.784, 0.816, 0.878, 0.9) # Brighter for N tick +const COLOR_FRAME: Color = Color(0.784, 0.816, 0.878, 0.55) # INSERT_COLOR_TEXT at reduced alpha +const COLOR_NORTH: Color = Color(0.784, 0.816, 0.878, 0.9) # Brighter for N tick const COLOR_CARDINAL: Color = Color(0.784, 0.816, 0.878, 0.4) # Dimmer E/S/W ticks const COLOR_PLAYER: Color = Constants.ENTITY_COLOR_PLAYER var _insert_active: bool = true + func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE set_process(true) + func _process(_delta: float) -> void: if _insert_active: queue_redraw() + ## Called from main.gd when GameState.insert_active changes. ## Hides the minimap overlay when the neural insert is inactive. func set_insert_active(active: bool) -> void: _insert_active = active visible = active + func _draw() -> void: var sz: Vector2 = get_rect().size var center := sz / 2.0 @@ -68,8 +72,16 @@ func _draw() -> void: # --- Player dot at center --- draw_circle(center, PLAYER_DOT_RADIUS, COLOR_PLAYER) # Soft bloom ring - draw_arc(center, PLAYER_DOT_RADIUS + 1.5, 0.0, TAU, 32, - Color(COLOR_PLAYER.r, COLOR_PLAYER.g, COLOR_PLAYER.b, 0.22), 1.0, true) + draw_arc( + center, + PLAYER_DOT_RADIUS + 1.5, + 0.0, + TAU, + 32, + Color(COLOR_PLAYER.r, COLOR_PLAYER.g, COLOR_PLAYER.b, 0.22), + 1.0, + true + ) # --- POIs --- var pois: Array = GameState.discovered_pois @@ -118,7 +130,9 @@ func _draw_cardinal_ticks(center: Vector2, outer_r: float) -> void: draw_line( center + n_dir * (outer_r - NORTH_TICK_LEN), center + n_dir * outer_r, - COLOR_NORTH, FRAME_WIDTH + 0.5, true + COLOR_NORTH, + FRAME_WIDTH + 0.5, + true ) # East (PI/2), South (PI), West (3PI/2) — shorter, dimmer for angle in [PI / 2.0, PI, 3.0 * PI / 2.0]: @@ -126,7 +140,9 @@ func _draw_cardinal_ticks(center: Vector2, outer_r: float) -> void: draw_line( center + dir * (outer_r - CARDINAL_TICK_LEN), center + dir * outer_r, - COLOR_CARDINAL, FRAME_WIDTH, true + COLOR_CARDINAL, + FRAME_WIDTH, + true ) @@ -136,10 +152,14 @@ func _draw_poi_shape(pos: Vector2, color: Color, category: String) -> void: # Diamond for danger var s: float = POI_DOT_RADIUS + 1.0 draw_polygon( - PackedVector2Array([ - pos + Vector2(0.0, -s), pos + Vector2(s, 0.0), - pos + Vector2(0.0, s), pos + Vector2(-s, 0.0) - ]), + PackedVector2Array( + [ + pos + Vector2(0.0, -s), + pos + Vector2(s, 0.0), + pos + Vector2(0.0, s), + pos + Vector2(-s, 0.0) + ] + ), PackedColorArray([color, color, color, color]) ) "evidence", "note", "clue": @@ -163,10 +183,10 @@ func _draw_border_arrow(tip: Vector2, dir: Vector2, color: Color) -> void: func _category_color(category: String) -> Color: match category.to_lower(): "danger", "threat", "hostile": - return Constants.ENTITY_COLOR_HOSTILE # #d45d5d — red + return Constants.ENTITY_COLOR_HOSTILE # #d45d5d — red "evidence", "note", "clue": - return Constants.ENTITY_COLOR_POI # #e8c547 — amber + return Constants.ENTITY_COLOR_POI # #e8c547 — amber "contact", "npc", "person": - return Constants.ENTITY_COLOR_UNKNOWN # #4a9ebb — teal + return Constants.ENTITY_COLOR_UNKNOWN # #4a9ebb — teal _: - return Constants.INSERT_COLOR_TEXT # #c8d0e0 — white-blue + return Constants.INSERT_COLOR_TEXT # #c8d0e0 — white-blue diff --git a/client/ui/monologue_display.gd b/client/ui/monologue_display.gd index 9c32d00f4..71f16342f 100644 --- a/client/ui/monologue_display.gd +++ b/client/ui/monologue_display.gd @@ -13,28 +13,30 @@ extends Control # is_urgent=true → opacity 1.0 and elevated colour variant (bloom deferred). const MAX_VISIBLE: int = 3 -const MAX_QUEUE: int = 5 +const MAX_QUEUE: int = 5 -const STAGGER_SEC: float = 0.15 -const FADE_IN_SEC: float = 0.3 -const FADE_OUT_SEC: float = 0.5 -const MIN_DURATION: float = FADE_IN_SEC + 0.1 # clamp: line must survive its own fade-in +const STAGGER_SEC: float = 0.15 +const FADE_IN_SEC: float = 0.3 +const FADE_OUT_SEC: float = 0.5 +const MIN_DURATION: float = FADE_IN_SEC + 0.1 # clamp: line must survive its own fade-in # Lattice colour palette — keyed by lattice_profile passed from GameState at show time. # standard opacity = 0.85, urgent opacity = 1.0. # Source: Tyre architecture review, Sprint 14. const _LATTICE_COLORS: Dictionary = { - "lattice_augmented": { # detective + "lattice_augmented": + { # detective "standard": Color("#d0d4e0"), - "urgent": Color("#e0e8f8"), + "urgent": Color("#e0e8f8"), }, - "lattice_baseline": { # smuggler + "lattice_baseline": + { # smuggler "standard": Color("#d8d0c4"), - "urgent": Color("#f0e4d4"), + "urgent": Color("#f0e4d4"), }, } const _FALLBACK_STANDARD: Color = Color("#c8d0e0") -const _FALLBACK_URGENT: Color = Color("#e0e8f8") +const _FALLBACK_URGENT: Color = Color("#e0e8f8") const _NOTIFICATION_COLOR: Color = Color("#8890a0") # #554: neutral system notification const _NOTIFICATION_DURATION: float = 2.5 @@ -67,7 +69,9 @@ func _process(delta: float) -> void: if next.get("is_notification", false): _show_notification_line(next.text) else: - _show_line(next.text, next.duration, next.priority, next.is_urgent, next.lattice_profile) + _show_line( + next.text, next.duration, next.priority, next.is_urgent, next.lattice_profile + ) # Display a monologue line. @@ -86,20 +90,30 @@ func show_notification(text: String) -> void: _show_notification_line(text) else: var entry := { - text = text, duration = _NOTIFICATION_DURATION, priority = 1, - is_urgent = false, lattice_profile = "", is_notification = true + text = text, + duration = _NOTIFICATION_DURATION, + priority = 1, + is_urgent = false, + lattice_profile = "", + is_notification = true } if _queue.size() < MAX_QUEUE: _queue.append(entry) - _queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a.priority > b.priority) + _queue.sort_custom( + func(a: Dictionary, b: Dictionary) -> bool: return a.priority > b.priority + ) else: var lowest := _lowest_priority_idx() if 1 >= _queue[lowest].priority: _queue[lowest] = entry - _queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a.priority > b.priority) + _queue.sort_custom( + func(a: Dictionary, b: Dictionary) -> bool: return a.priority > b.priority + ) -func show_monologue(text: String, duration: float, priority: int = 2, is_urgent: bool = false) -> void: +func show_monologue( + text: String, duration: float, priority: int = 2, is_urgent: bool = false +) -> void: if text.is_empty(): return var profile := GameState.lattice_profile @@ -114,11 +128,12 @@ func show_monologue(text: String, duration: float, priority: int = 2, is_urgent: # Internal # --------------------------------------------------------------------------- + func _show_notification_line(text: String) -> void: var container := MarginContainer.new() - container.add_theme_constant_override("margin_left", 4) - container.add_theme_constant_override("margin_right", 4) - container.add_theme_constant_override("margin_top", 2) + container.add_theme_constant_override("margin_left", 4) + container.add_theme_constant_override("margin_right", 4) + container.add_theme_constant_override("margin_top", 2) container.add_theme_constant_override("margin_bottom", 2) var label := RichTextLabel.new() label.bbcode_enabled = true @@ -130,10 +145,10 @@ func _show_notification_line(text: String) -> void: container.add_child(label) _vbox.add_child(container) var slot := { - node = container, + node = container, expire_timer = _NOTIFICATION_DURATION, - priority = 1, - tween = null, + priority = 1, + tween = null, } _visible.append(slot) _next_fade_in_msec = float(Time.get_ticks_msec()) + STAGGER_SEC * 1000.0 @@ -143,15 +158,17 @@ func _show_notification_line(text: String) -> void: tween.tween_property(container, "modulate:a", 0.85, FADE_IN_SEC) -func _show_line(text: String, duration: float, priority: int, is_urgent: bool, lattice_profile: String) -> void: +func _show_line( + text: String, duration: float, priority: int, is_urgent: bool, lattice_profile: String +) -> void: var line_node := _build_line_node(text, is_urgent, lattice_profile) _vbox.add_child(line_node) var slot := { - node = line_node, + node = line_node, expire_timer = maxf(duration, MIN_DURATION), # clamp: survives own fade-in - priority = priority, - tween = null, + priority = priority, + tween = null, } _visible.append(slot) _next_fade_in_msec = float(Time.get_ticks_msec()) + STAGGER_SEC * 1000.0 @@ -174,22 +191,36 @@ func _retire_slot(slot: Dictionary) -> void: tween.tween_callback(node.queue_free) -func _enqueue(text: String, duration: float, priority: int, is_urgent: bool, lattice_profile: String) -> void: +func _enqueue( + text: String, duration: float, priority: int, is_urgent: bool, lattice_profile: String +) -> void: if _queue.size() < MAX_QUEUE: - _queue.append({ - text = text, duration = duration, priority = priority, - is_urgent = is_urgent, lattice_profile = lattice_profile - }) - _queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a.priority > b.priority) + _queue.append( + { + text = text, + duration = duration, + priority = priority, + is_urgent = is_urgent, + lattice_profile = lattice_profile + } + ) + _queue.sort_custom( + func(a: Dictionary, b: Dictionary) -> bool: return a.priority > b.priority + ) else: # >= tiebreak: newest replaces oldest at equal priority (FIFO for equal ranks) var lowest := _lowest_priority_idx() if priority >= _queue[lowest].priority: _queue[lowest] = { - text = text, duration = duration, priority = priority, - is_urgent = is_urgent, lattice_profile = lattice_profile + text = text, + duration = duration, + priority = priority, + is_urgent = is_urgent, + lattice_profile = lattice_profile } - _queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a.priority > b.priority) + _queue.sort_custom( + func(a: Dictionary, b: Dictionary) -> bool: return a.priority > b.priority + ) # else: incoming line is strictly lower priority — silently drop; no sort needed @@ -203,13 +234,16 @@ func _lowest_priority_idx() -> int: func _build_line_node(text: String, is_urgent: bool, lattice_profile: String) -> Control: var palette: Dictionary = _LATTICE_COLORS.get(lattice_profile, {}) - var color: Color = palette.get("urgent", _FALLBACK_URGENT) if is_urgent \ + var color: Color = ( + palette.get("urgent", _FALLBACK_URGENT) + if is_urgent else palette.get("standard", _FALLBACK_STANDARD) + ) var container := MarginContainer.new() - container.add_theme_constant_override("margin_left", 4) - container.add_theme_constant_override("margin_right", 4) - container.add_theme_constant_override("margin_top", 2) + container.add_theme_constant_override("margin_left", 4) + container.add_theme_constant_override("margin_right", 4) + container.add_theme_constant_override("margin_top", 2) container.add_theme_constant_override("margin_bottom", 2) var label := RichTextLabel.new() diff --git a/client/ui/settings_dialog.gd b/client/ui/settings_dialog.gd index af4e3a95d..1583dcd5f 100644 --- a/client/ui/settings_dialog.gd +++ b/client/ui/settings_dialog.gd @@ -8,33 +8,33 @@ extends Control signal closed signal debug_console_toggled(enabled: bool) # #581: debug console enabled/disabled -signal ai_dialogue_toggled(enabled: bool) # #646: AI-Enhanced Dialogue enabled/disabled +signal ai_dialogue_toggled(enabled: bool) # #646: AI-Enhanced Dialogue enabled/disabled const BG_COLOR := Color(0.05, 0.05, 0.08, 0.90) const BORDER_COLOR := Color("#4a9ebb") const TEXT_COLOR := Color(0.878, 0.969, 0.98, 1) const TITLE_COLOR := Color("#4a9ebb") -const STATUS_GREEN := Color("#6bc9a6") +const STATUS_GREEN := Color("#6bc9a6") const STATUS_YELLOW := Color("#e8c547") -const STATUS_RED := Color("#c84040") +const STATUS_RED := Color("#c84040") const FONT_SIZE := 14 const FONT_SIZE_SMALL := 11 ## Shared client preferences file — avoids compile-time dependency on DebugConsole for path. const SETTINGS_CFG_PATH := "user://settings.cfg" -const BOX_WIDTH := 460 +const BOX_WIDTH := 460 const BOX_HEIGHT := 500 # +36 debug console, +88 AI dialogue section -const PADDING := 20 +const PADDING := 20 const ROW_HEIGHT := 36 # Bus display labels → bus name strings (must match AudioManager BUS_* constants) const BUS_ROWS: Array = [ - ["Music", "Music"], - ["Ambient", "Ambient"], - ["World SFX", "WorldSFX"], + ["Music", "Music"], + ["Ambient", "Ambient"], + ["World SFX", "WorldSFX"], ["Player Actions", "PlayerActions"], - ["UI Sounds", "UISounds"], + ["UI Sounds", "UISounds"], ] var _active: bool = false @@ -77,10 +77,7 @@ func is_open() -> bool: func _build_ui() -> void: var vp_size := get_viewport_rect().size - var box_pos := Vector2( - (vp_size.x - BOX_WIDTH) / 2.0, - (vp_size.y - BOX_HEIGHT) / 2.0 - ) + var box_pos := Vector2((vp_size.x - BOX_WIDTH) / 2.0, (vp_size.y - BOX_HEIGHT) / 2.0) _container = VBoxContainer.new() _container.position = box_pos + Vector2(PADDING, PADDING + 28) @@ -121,9 +118,10 @@ func _build_ui() -> void: db_label.add_theme_color_override("font_color", TEXT_COLOR) hbox.add_child(db_label) - slider.value_changed.connect(func(value: float) -> void: - AudioManager.set_volume(bus_name, value) - db_label.text = _format_db(value) + slider.value_changed.connect( + func(value: float) -> void: + AudioManager.set_volume(bus_name, value) + db_label.text = _format_db(value) ) # #581: Debug Console toggle @@ -148,10 +146,10 @@ func _build_ui() -> void: var cfg := ConfigFile.new() debug_check.button_pressed = true if cfg.load(DebugConsole.PREFS_PATH) == OK: - debug_check.button_pressed = cfg.get_value(DebugConsole.PREFS_SECTION, DebugConsole.PREFS_KEY_ENABLED, true) - debug_check.toggled.connect(func(enabled: bool) -> void: - debug_console_toggled.emit(enabled) - ) + debug_check.button_pressed = cfg.get_value( + DebugConsole.PREFS_SECTION, DebugConsole.PREFS_KEY_ENABLED, true + ) + debug_check.toggled.connect(func(enabled: bool) -> void: debug_console_toggled.emit(enabled)) debug_hbox.add_child(debug_check) # #646: AI Dialogue section divider @@ -223,7 +221,8 @@ func _build_ui() -> void: # Status message label — only shown when non-empty var status_msg: String = HardwareDetector.status_message( - ram_class, tpt_class, ram_result["free_mb"], tpt_tps) + ram_class, tpt_class, ram_result["free_mb"], tpt_tps + ) var ai_status_label := Label.new() ai_status_label.text = status_msg ai_status_label.add_theme_font_size_override("font_size", FONT_SIZE_SMALL) @@ -247,16 +246,22 @@ func _build_ui() -> void: _ai_battery_warning_label.visible = _ai_inference_suspended _container.add_child(_ai_battery_warning_label) - _ai_check_node.toggled.connect(func(enabled: bool) -> void: - GameState.ai_enhanced_dialogue_enabled = enabled - _save_ai_pref(enabled) - if SimBridge.state == SimBridge.ConnectionState.CONNECTED: - SimBridge.send_input({ - "action": InputMapper.Action.CHANGE_SETTINGS, - "action_data": {"ai_enhanced_dialogue": enabled}, - "timestamp_msec": Time.get_ticks_msec(), - }) - ai_dialogue_toggled.emit(enabled) + _ai_check_node.toggled.connect( + func(enabled: bool) -> void: + GameState.ai_enhanced_dialogue_enabled = enabled + _save_ai_pref(enabled) + if SimBridge.state == SimBridge.ConnectionState.CONNECTED: + ( + SimBridge + . send_input( + { + "action": InputMapper.Action.CHANGE_SETTINGS, + "action_data": {"ai_enhanced_dialogue": enabled}, + "timestamp_msec": Time.get_ticks_msec(), + } + ) + ) + ai_dialogue_toggled.emit(enabled) ) # Spacer @@ -284,12 +289,13 @@ func _destroy_ui() -> void: if _container: _container.queue_free() _container = null - _ai_check_node = null # freed with _container + _ai_check_node = null # freed with _container _ai_battery_warning_label = null # -- #646: AI Dialogue testable API ------------------------------------------- + ## Returns the canonical label text for the AI-Enhanced Dialogue toggle (D-138). ## Reads from UIStrings yaml (settings.ai_dialogue_toggle) for localization support. func get_ai_dialogue_label_text() -> String: @@ -354,8 +360,7 @@ func _draw() -> void: # Dialog box var box_pos := Vector2( - (viewport_size.x - BOX_WIDTH) / 2.0, - (viewport_size.y - BOX_HEIGHT) / 2.0 + (viewport_size.x - BOX_WIDTH) / 2.0, (viewport_size.y - BOX_HEIGHT) / 2.0 ) var box_rect := Rect2(box_pos, Vector2(BOX_WIDTH, BOX_HEIGHT)) draw_rect(box_rect, Color(0.08, 0.08, 0.12, 0.95)) @@ -363,10 +368,15 @@ func _draw() -> void: # Title var font := ThemeDB.fallback_font - draw_string(font, + draw_string( + font, box_pos + Vector2(PADDING, PADDING + 18), "SETTINGS", - HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE + 2, TITLE_COLOR) + HORIZONTAL_ALIGNMENT_LEFT, + -1, + FONT_SIZE + 2, + TITLE_COLOR + ) func _on_quit_to_menu() -> void: diff --git a/client/ui/stance_indicator.gd b/client/ui/stance_indicator.gd index 4c5b2de92..29a3c8472 100644 --- a/client/ui/stance_indicator.gd +++ b/client/ui/stance_indicator.gd @@ -5,10 +5,10 @@ extends Control ## Lives on UILayer (z-layer 7). const STANCE_COLORS := { - "Sprint": Color("#d45d5d"), # Red — fast, loud, dangerous - "Walk": Color("#c8d0e0"), # Default — neutral white-blue + "Sprint": Color("#d45d5d"), # Red — fast, loud, dangerous + "Walk": Color("#c8d0e0"), # Default — neutral white-blue "Careful": Color("#6bc9a6"), # Green — quiet, observant - "Crouch": Color("#e8c547"), # Amber — very quiet, slow + "Crouch": Color("#e8c547"), # Amber — very quiet, slow } const STANCE_DEFAULT_COLOR := Color("#c8d0e0") @@ -42,10 +42,19 @@ func _draw() -> void: # Stance text var color: Color = STANCE_COLORS.get(_current_stance, STANCE_DEFAULT_COLOR) - draw_string(font, PADDING + Vector2(0, text_size.y), text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, color) + draw_string( + font, + PADDING + Vector2(0, text_size.y), + text, + HORIZONTAL_ALIGNMENT_LEFT, + -1, + FONT_SIZE, + color + ) # -- Public API --------------------------------------------------------------- + func get_current_stance() -> String: return _current_stance diff --git a/client/ui/star_map.gd b/client/ui/star_map.gd index 6fb5c2d9a..e61eb79c1 100644 --- a/client/ui/star_map.gd +++ b/client/ui/star_map.gd @@ -16,9 +16,9 @@ const DATA_PATH := "res://data/star_map_data.json" # Layout const MAP_CENTER_FRACTION := Vector2(0.5, 0.5) # center of control -const MIN_RING_RADIUS: float = 30.0 # innermost ring (hop 0 = gateway dot only) -const RING_SPACING: float = 22.0 # pixels between hop rings -const MAX_HOP_RINGS: int = 24 # max hop distance we render rings for +const MIN_RING_RADIUS: float = 30.0 # innermost ring (hop 0 = gateway dot only) +const RING_SPACING: float = 22.0 # pixels between hop rings +const MAX_HOP_RINGS: int = 24 # max hop distance we render rings for # Dot sizing const DOT_RADIUS_HUB: float = 4.5 @@ -75,8 +75,8 @@ const SECTOR_ANGLE_CENTER: Dictionary = { "west_reach": PI, } const SECTOR_ANGLE_SPREAD: float = PI / 2.5 # each sector occupies ~72° of arc -const CORE_ANGLE_SPREAD: float = TAU # core systems spread full circle -const DEEP_FRONTIER_ANGLE_SPREAD: float = TAU # deep frontier wraps entire outer edge +const CORE_ANGLE_SPREAD: float = TAU # core systems spread full circle +const DEEP_FRONTIER_ANGLE_SPREAD: float = TAU # deep frontier wraps entire outer edge # Pan/zoom const ZOOM_MIN: float = 0.3 @@ -87,7 +87,7 @@ const ZOOM_STEP: float = 0.15 var _nodes: Array = [] var _edges: Array = [] var _node_positions: Dictionary = {} # system_id -> Vector2 (screen coords relative to map center) -var _node_lookup: Dictionary = {} # system_id -> node dict +var _node_lookup: Dictionary = {} # system_id -> node dict var _selected_system: String = "" var _hovered_system: String = "" @@ -99,8 +99,8 @@ var _pan_start_offset: Vector2 = Vector2.ZERO var _data_loaded: bool = false var _insert_active: bool = true -var _dirty: bool = true # redraw needed — set by state changes, cleared after _draw -var _info_panel: ImplantPanel # D-169: component-based info panel +var _dirty: bool = true # redraw needed — set by state changes, cleared after _draw +var _info_panel: ImplantPanel # D-169: component-based info panel var _implant_theme: ImplantTheme @@ -170,6 +170,7 @@ func get_edge_count() -> int: # Data loading # ============================================================================= + func _load_data() -> void: if not FileAccess.file_exists(DATA_PATH): push_warning("StarMapRenderer: data file not found at %s" % DATA_PATH) @@ -195,6 +196,7 @@ func _load_data() -> void: # Layout — place systems on concentric rings by hop distance # ============================================================================= + func _compute_layout() -> void: _node_positions.clear() @@ -260,7 +262,9 @@ func _compute_layout() -> void: var jitter: float = _system_hash(node["system_id"]) * 0.08 angle += jitter # Slight radial variation to avoid perfect circles - var r_var: float = radius + _system_hash(node["system_id"] + "r") * RING_SPACING * 0.3 + var r_var: float = ( + radius + _system_hash(node["system_id"] + "r") * RING_SPACING * 0.3 + ) _node_positions[node["system_id"]] = Vector2(cos(angle), sin(angle)) * r_var @@ -275,13 +279,20 @@ func _sort_by_sector_angle(a: Dictionary, b: Dictionary) -> bool: func _sector_sort_key(node: Dictionary) -> float: var sector: String = node.get("geographic_sector", "unknown") match sector: - "core": return 0.0 - "north_reach": return 1.0 - "east_reach": return 2.0 - "south_reach": return 3.0 - "west_reach": return 4.0 - "deep_frontier": return 5.0 - _: return 6.0 # gdlint:ignore = max-returns + "core": + return 0.0 + "north_reach": + return 1.0 + "east_reach": + return 2.0 + "south_reach": + return 3.0 + "west_reach": + return 4.0 + "deep_frontier": + return 5.0 + _: + return 6.0 # gdlint:ignore = max-returns ## Deterministic float in [-1, 1] from a string key. @@ -294,6 +305,7 @@ func _system_hash(key: String) -> float: # Drawing # ============================================================================= + func _draw() -> void: if not _data_loaded: return @@ -357,8 +369,12 @@ func _draw_sector_labels(center: Vector2) -> void: var color: Color = SECTOR_COLORS.get(sector, COLOR_TEXT_DIM) var font := get_theme_default_font() var font_size: int = 10 - var text_size: Vector2 = font.get_string_size(label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size) - draw_string(font, pos - text_size / 2.0, label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, color) + var text_size: Vector2 = font.get_string_size( + label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size + ) + draw_string( + font, pos - text_size / 2.0, label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, color + ) func _draw_systems(center: Vector2) -> void: @@ -383,7 +399,9 @@ func _draw_systems(center: Vector2) -> void: # Hover highlight if sid == _hovered_system and sid != _selected_system: - draw_arc(pos, radius + 3.0, 0.0, TAU, 16, Color(color.r, color.g, color.b, 0.4), 1.0, true) + draw_arc( + pos, radius + 3.0, 0.0, TAU, 16, Color(color.r, color.g, color.b, 0.4), 1.0, true + ) draw_circle(pos, radius, color) @@ -415,8 +433,15 @@ func _draw_selection(center: Vector2) -> void: if label.is_empty(): label = _selected_system var font := get_theme_default_font() - draw_string(font, pos + Vector2(SELECTION_RING_RADIUS + 4, 4), label, - HORIZONTAL_ALIGNMENT_LEFT, -1, 12, COLOR_SELECTION) + draw_string( + font, + pos + Vector2(SELECTION_RING_RADIUS + 4, 4), + label, + HORIZONTAL_ALIGNMENT_LEFT, + -1, + 12, + COLOR_SELECTION + ) ## Rebuild the info panel with components for the selected system (D-169). @@ -462,9 +487,13 @@ func _rebuild_info_panel() -> void: elif not population.is_empty(): stat_line_3 = "Population: " + population else: - stat_line_3 = "%d aperture%s" % [ - int(node.get("aperture_count", 0)), - "s" if int(node.get("aperture_count", 0)) != 1 else ""] + stat_line_3 = ( + "%d aperture%s" + % [ + int(node.get("aperture_count", 0)), + "s" if int(node.get("aperture_count", 0)) != 1 else "" + ] + ) _info_panel.add_component(ImplantDataRow.new(stat_line_3)) # ── GTTR excerpt ───────────────────────────────────────────────────────── @@ -487,23 +516,43 @@ func _rebuild_info_panel() -> void: func _draw_title() -> void: var font := get_theme_default_font() - draw_string(font, Vector2(16, 28), "THE REACH — NAVIGATOR", HORIZONTAL_ALIGNMENT_LEFT, -1, 16, COLOR_TEXT) - draw_string(font, Vector2(16, 44), "Concord Assembly Gate Network — %d Systems" % _nodes.size(), - HORIZONTAL_ALIGNMENT_LEFT, -1, 10, COLOR_TEXT_DIM) + draw_string( + font, + Vector2(16, 28), + "THE REACH — NAVIGATOR", + HORIZONTAL_ALIGNMENT_LEFT, + -1, + 16, + COLOR_TEXT + ) + draw_string( + font, + Vector2(16, 44), + "Concord Assembly Gate Network — %d Systems" % _nodes.size(), + HORIZONTAL_ALIGNMENT_LEFT, + -1, + 10, + COLOR_TEXT_DIM + ) func _dot_radius(topology: String) -> float: match topology: - "hub": return DOT_RADIUS_HUB - "junction": return DOT_RADIUS_JUNCTION - "dead_end": return DOT_RADIUS_DEAD_END - _: return DOT_RADIUS_DEFAULT + "hub": + return DOT_RADIUS_HUB + "junction": + return DOT_RADIUS_JUNCTION + "dead_end": + return DOT_RADIUS_DEAD_END + _: + return DOT_RADIUS_DEFAULT # ============================================================================= # Input — selection, pan, zoom # ============================================================================= + func _gui_input(event: InputEvent) -> void: if event is InputEventMouseButton: var mb := event as InputEventMouseButton diff --git a/client/ui/time_display.gd b/client/ui/time_display.gd index 71898f6ee..29c868fcf 100644 --- a/client/ui/time_display.gd +++ b/client/ui/time_display.gd @@ -15,10 +15,10 @@ const BORDER_COLOR := Color(0.10, 0.20, 0.26, 0.65) # Day phase colors — station lighting cycle (D-031) const PHASE_COLORS := { - "Morning": Color("#aed6dc"), # pale cyan-blue — early light - "Afternoon": Color("#E0F7FA"), # bright cyan-white — full day - "Evening": Color("#9EBFC4"), # dimmed — dusk transition - "Night": Color("#4a7080"), # dark teal — station nightwatch + "Morning": Color("#aed6dc"), # pale cyan-blue — early light + "Afternoon": Color("#E0F7FA"), # bright cyan-white — full day + "Evening": Color("#9EBFC4"), # dimmed — dusk transition + "Night": Color("#4a7080"), # dark teal — station nightwatch } var _time_str: String = "--:--" @@ -86,12 +86,33 @@ func _draw() -> void: var font := ThemeDB.fallback_font # HH:MM (primary, full brightness) - draw_string(font, Vector2(PADDING.x, PADDING.y + _time_size.y), - _time_str, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE_TIME, Constants.IMPLANT_TEXT_COLOR) + draw_string( + font, + Vector2(PADDING.x, PADDING.y + _time_size.y), + _time_str, + HORIZONTAL_ALIGNMENT_LEFT, + -1, + FONT_SIZE_TIME, + Constants.IMPLANT_TEXT_COLOR + ) # Phase + day number (secondary, dimmed + phase-tinted) var meta_y := PADDING.y + _time_size.y + 3 + _meta_h - draw_string(font, Vector2(PADDING.x, meta_y), - _phase_str, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE_META, _phase_color) - draw_string(font, Vector2(PADDING.x + _phase_size.x, meta_y), - _day_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE_META, Constants.IMPLANT_TEXT_DIM) + draw_string( + font, + Vector2(PADDING.x, meta_y), + _phase_str, + HORIZONTAL_ALIGNMENT_LEFT, + -1, + FONT_SIZE_META, + _phase_color + ) + draw_string( + font, + Vector2(PADDING.x + _phase_size.x, meta_y), + _day_text, + HORIZONTAL_ALIGNMENT_LEFT, + -1, + FONT_SIZE_META, + Constants.IMPLANT_TEXT_DIM + ) diff --git a/client/ui/world_radial.gd b/client/ui/world_radial.gd index 5451efc95..0a0f8485b 100644 --- a/client/ui/world_radial.gd +++ b/client/ui/world_radial.gd @@ -144,20 +144,30 @@ func _activate_insert() -> void: # if already paused (e.g. Gauntlet interlude), server ignores duplicate. if not _insert_active: _insert_active = true - SimBridge.send_input({ - "action": InputMapper.Action.PAUSE, - "timestamp_msec": Time.get_ticks_msec(), - }) + ( + SimBridge + . send_input( + { + "action": InputMapper.Action.PAUSE, + "timestamp_msec": Time.get_ticks_msec(), + } + ) + ) func deactivate_insert() -> void: # #518/D-058: Send ResumeSimulation when insert closes. if _insert_active: _insert_active = false - SimBridge.send_input({ - "action": InputMapper.Action.UNPAUSE, - "timestamp_msec": Time.get_ticks_msec(), - }) + ( + SimBridge + . send_input( + { + "action": InputMapper.Action.UNPAUSE, + "timestamp_msec": Time.get_ticks_msec(), + } + ) + ) func _draw() -> void: @@ -217,11 +227,17 @@ func _draw_spoke_icon(spoke: int, center: Vector2, color: Color) -> void: var hh := ICON_SIZE * 0.55 draw_rect(Rect2(center - Vector2(hw, hh), Vector2(hw * 2, hh * 2)), color, false, 1.0) # Screen line - draw_line(center - Vector2(hw * 0.6, hh * 0.3), center + Vector2(hw * 0.6, -hh * 0.3), color, 1.0) + draw_line( + center - Vector2(hw * 0.6, hh * 0.3), + center + Vector2(hw * 0.6, -hh * 0.3), + color, + 1.0 + ) # -- Public API --------------------------------------------------------------- + func is_open() -> bool: return _open