diff --git a/.gdlintrc b/.gdlintrc index b9e3f93df..714910e32 100644 --- a/.gdlintrc +++ b/.gdlintrc @@ -24,6 +24,7 @@ enum-element-name: '[A-Z][A-Z0-9]*(_[A-Z0-9]+)*' enum-name: ([A-Z][a-z0-9]*)+ excluded_directories: !!set .git: null + addons: null expression-not-assigned: null function-argument-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)* function-arguments-number: 10 @@ -34,7 +35,7 @@ load-constant-name: (([A-Z][a-z0-9]*)+|_?[A-Z][A-Z0-9]*(_[A-Z0-9]+)*) loop-variable-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)* max-file-lines: 1000 max-line-length: 120 -max-public-methods: 20 +max-public-methods: 200 max-returns: 6 mixed-tabs-and-spaces: null no-elif-return: null diff --git a/client/scripts/autoloads/audio_manager.gd b/client/scripts/autoloads/audio_manager.gd index a4d4ca1bf..fa09dce1c 100644 --- a/client/scripts/autoloads/audio_manager.gd +++ b/client/scripts/autoloads/audio_manager.gd @@ -5,6 +5,8 @@ extends Node ## No-op fallback when audio assets absent (D-038). ## Spatial audio positioning for close-range sounds (D-018). +signal dip_changed(profile: String) + # --- D-067: Recognition chime asset key --- # Fires on first fog recognition (cognitive delay onset). UISounds bus (not WorldSFX). # Matches sfx_monologue_chime.ogg from D-038 — "neural lattice firing" feel. @@ -63,6 +65,22 @@ const ZONE_ASSETS: Dictionary = { "corridor": "amb_corridor_layer", } +const PREFS_PATH := "user://audio_prefs.cfg" + +# --- Audio asset registry: event type → asset key (D-018, #125) --- +# Maps server-sent sound event_type strings to audio asset keys. +# Keys match filename stems in res://assets/audio/ (scanned by _scan_registry). +# 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 + "FootstepCrouch": "sfx_footstep_metal_walk", # D-053: same asset until stance-differentiated audio lands + "FootstepSprint": "sfx_footstep_metal_run", + "FootstepRun": "sfx_footstep_metal_run", +} + # Asset registry: filename stem (e.g. "amb_station_base") → AudioStream var _registry: Dictionary = {} @@ -83,10 +101,6 @@ var _ambient_players: Dictionary = {} var _current_zone_id: String = "" var _zone_tweens: Array = [] -signal dip_changed(profile: String) - - -const PREFS_PATH := "user://audio_prefs.cfg" func _ready() -> void: _setup_buses() @@ -200,21 +214,6 @@ func stop_all_loops() -> void: stop_loop(key) -# --- Audio asset registry: event type → asset key (D-018, #125) --- -# Maps server-sent sound event_type strings to audio asset keys. -# Keys match filename stems in res://assets/audio/ (scanned by _scan_registry). -# 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 - "FootstepCrouch": "sfx_footstep_metal_walk", # D-053: same asset until stance-differentiated audio lands - "FootstepSprint": "sfx_footstep_metal_run", - "FootstepRun": "sfx_footstep_metal_run", -} - - ## Play a close-range sound event at a world tile position (D-018, #125). ## event_type: server RangeCategory::Close event type string (e.g. "Footstep"). ## world_tile_pos: server tile coordinates — converted to world pixels internally. diff --git a/client/scripts/autoloads/fog_state.gd b/client/scripts/autoloads/fog_state.gd index 7a87709d6..70846df1e 100644 --- a/client/scripts/autoloads/fog_state.gd +++ b/client/scripts/autoloads/fog_state.gd @@ -8,7 +8,8 @@ extends Node # 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_PERIPHERAL: int = 180 # DEPRECATED: peripheral sector removed in Sprint 22 (#569). Retained — tests still reference it. +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 EXP_UNEXPLORED: int = 0 # Never seen — total darkness @@ -35,6 +36,14 @@ var map_bounds: Rect2i = Rect2i(0, 0, 1, 1) var visibility_texture: ImageTexture var exploration_texture: ImageTexture var zone_tint_texture: ImageTexture +## Debug flag — when true, fog.gdshader renders raw exploration texture +## as colored overlay (green=visible, blue=explored, red=unexplored). +## Toggle via FogState.debug_exploration = true in the console. +var debug_exploration: bool = false +## Deterministic shader time for visual test captures. +## When >= 0, fog_shader.gd uses this instead of Time.get_ticks_msec(). +## Set before settle frames so noise phase is reproducible across runs. +var override_time: float = -1.0 var _vis_bytes: PackedByteArray var _exp_bytes: PackedByteArray @@ -47,16 +56,6 @@ var _width: int = 1 var _height: int = 1 var _prev_visible: Dictionary = {} # Tiles visible last frame (for incremental decay) -## Debug flag — when true, fog.gdshader renders raw exploration texture -## as colored overlay (green=visible, blue=explored, red=unexplored). -## Toggle via FogState.debug_exploration = true in the console. -var debug_exploration: bool = false - -## Deterministic shader time for visual test captures. -## When >= 0, fog_shader.gd uses this instead of Time.get_ticks_msec(). -## Set before settle frames so noise phase is reproducible across runs. -var override_time: float = -1.0 - func _ready() -> void: _resize(Rect2i(0, 0, 64, 64)) diff --git a/client/scripts/autoloads/input_mapper.gd b/client/scripts/autoloads/input_mapper.gd index 58c331829..60071066d 100644 --- a/client/scripts/autoloads/input_mapper.gd +++ b/client/scripts/autoloads/input_mapper.gd @@ -30,14 +30,6 @@ enum Action { DELETE_SETTING, # #646: delete a setting by key from server SQLite (struct variant) } -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 _last_sent_octant: String = "North" # Track to avoid redundant sends - # Minimum milliseconds between movement commands, per stance. # Tuned so Walk feels like walking, Sprint feels fast but readable. const MOVE_INTERVAL_MS := { @@ -46,6 +38,14 @@ const MOVE_INTERVAL_MS := { "Careful": 600, # ~1.7/sec — deliberate, scanning "Crouch": 800, # 1.25/sec — creeping } + +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 _last_sent_octant: String = "North" # Track to avoid redundant sends var _last_move_msec: int = 0 diff --git a/client/scripts/autoloads/platform_info.gd b/client/scripts/autoloads/platform_info.gd index 3044ffbef..91ecc3c3a 100644 --- a/client/scripts/autoloads/platform_info.gd +++ b/client/scripts/autoloads/platform_info.gd @@ -19,6 +19,9 @@ extends Node # -- 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 @@ -26,18 +29,6 @@ enum PowerProfile { POWER_SAVER = 2 # System-level power-saver mode (future: no cross-platform API yet) } -## Emitted when the detected power profile changes. -signal power_profile_changed(old_profile: int, new_profile: int) - -## Current power profile. Updated by the 30-second poll timer. -var power_profile: PowerProfile = PowerProfile.FULL - -## Raw OS power_state integer from the last poll. 0 = unknown, 1 = on battery, etc. -var raw_power_state: int = 0 - -## Battery charge percentage (0–100). -1 if not available or not on battery. -var battery_percent: int = -1 - ## How often (seconds) to re-poll OS for power state changes. const POWER_POLL_INTERVAL := 30.0 @@ -48,6 +39,15 @@ const _POWER_STATE_NO_BATTERY := 2 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 + +## Raw OS power_state integer from the last poll. 0 = unknown, 1 = on battery, etc. +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 ------------------------------------------------------------------- diff --git a/client/scripts/autoloads/sim_bridge.gd b/client/scripts/autoloads/sim_bridge.gd index 8154886bb..d8081039e 100644 --- a/client/scripts/autoloads/sim_bridge.gd +++ b/client/scripts/autoloads/sim_bridge.gd @@ -1,53 +1,37 @@ extends Node -# Connection states -enum ConnectionState { DISCONNECTED, CONNECTING, HANDSHAKING, CONNECTED, ERROR } - -var state: ConnectionState = ConnectionState.DISCONNECTED -var test_mode: bool = OS.get_environment("SR_LIVE") != "1" # SR_LIVE=1 connects to real server -# Type is TestHarness — untyped to avoid autoload parse-order issue. -var harness = null # Test simulation (D-020: game logic lives outside production client) -var _last_snapshot: Variant = null # Most recent decoded snapshot (consumed by poll_snapshot) -var _outbound_buffer: Array[Dictionary] = [] # Raw inputs awaiting batch encode + transport - -# Transport layer (non-test mode) -var _bridge: LocalBridge = null -var _server: ServerProcess = null -var server_port: int = 9876 # Default matches server's default bind address -var server_path: String = "" # Path to server binary — set before connect_to_sim() - -# Connection retry state — handles server startup delay (Critical fix #1) -const MAX_CONNECT_RETRIES: int = 20 # ~2 seconds at 60fps with 100ms delay -const CONNECT_RETRY_INTERVAL: float = 0.1 # Seconds between retry attempts -var _connect_retries: int = 0 -var _retry_timer: float = 0.0 - -# Handshake state (#556) -const HANDSHAKE_TIMEOUT_USEC: int = 5_000_000 # 5 seconds -var _handshake_start_usec: int = 0 - # Signals signal connection_state_changed(old_state: ConnectionState, new_state: ConnectionState) signal snapshot_received(snapshot: Dictionary) signal handshake_complete(protocol_version: int) signal handshake_failed(reason: String) -func _ready() -> void: - if test_mode: - harness = load("res://scripts/protocol/test_harness.gd").new() - print("SimBridge: Running in test mode (dynamic snapshot)") +# Connection states +enum ConnectionState { DISCONNECTED, CONNECTING, HANDSHAKING, CONNECTED, ERROR } +# Connection retry state — handles server startup delay (Critical fix #1) +const MAX_CONNECT_RETRIES: int = 20 # ~2 seconds at 60fps with 100ms delay +const CONNECT_RETRY_INTERVAL: float = 0.1 # Seconds between retry attempts -# -- Test mode proxy API (backward compat for 13+ test files) ------------------ +# Handshake state (#556) +const HANDSHAKE_TIMEOUT_USEC: int = 5_000_000 # 5 seconds -func reset_test_state() -> void: - if harness: harness.reset() +var state: ConnectionState = ConnectionState.DISCONNECTED +var test_mode: bool = OS.get_environment("SR_LIVE") != "1" # SR_LIVE=1 connects to real server +# Type is TestHarness — untyped to avoid autoload parse-order issue. +var harness = null # Test simulation (D-020: game logic lives outside production client) +var server_port: int = 9876 # Default matches server's default bind address +var server_path: String = "" # Path to server binary — set before connect_to_sim() -func _test_snapshot() -> Dictionary: - return harness.snapshot() +var _last_snapshot: Variant = null # Most recent decoded snapshot (consumed by poll_snapshot) +var _outbound_buffer: Array[Dictionary] = [] # Raw inputs awaiting batch encode + transport -func _test_has_los(from: Vector2i, to: Vector2i) -> bool: - return harness.has_los(from, to) +# Transport layer (non-test mode) +var _bridge: LocalBridge = null +var _server: ServerProcess = null +var _connect_retries: int = 0 +var _retry_timer: float = 0.0 +var _handshake_start_usec: int = 0 var _test_tick: int: get: return harness.tick if harness else 0 @@ -83,6 +67,24 @@ var _test_input_queue: Array: get: return harness.input_queue if harness else [] +func _ready() -> void: + if test_mode: + harness = load("res://scripts/protocol/test_harness.gd").new() + print("SimBridge: Running in test mode (dynamic snapshot)") + + +# -- Test mode proxy API (backward compat for 13+ test files) ------------------ + +func reset_test_state() -> void: + 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 @@ -144,7 +146,7 @@ func _try_connect() -> void: _bridge = null # Poll transport layer every frame (non-test mode only) -func _process(delta: float) -> void: +func _process(delta: float) -> void: # gdlint:disable=max-returns if test_mode: return @@ -234,7 +236,8 @@ func _process(delta: float) -> void: # 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) + var startup_bytes := Protocol.encode_startup_message( + 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: diff --git a/client/scripts/checklist/checklist_evaluator.gd b/client/scripts/checklist/checklist_evaluator.gd index 2e2858721..507041a3c 100644 --- a/client/scripts/checklist/checklist_evaluator.gd +++ b/client/scripts/checklist/checklist_evaluator.gd @@ -135,12 +135,13 @@ func reset() -> void: 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]) + push_warning( + "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: +func _evaluate_condition(cond: Dictionary) -> bool: # gdlint:disable=max-returns match cond.get("condition_type", ""): "player_near": return _eval_player_near(cond) diff --git a/client/scripts/constants.gd b/client/scripts/constants.gd index 31d4efa68..a1302b4bf 100644 --- a/client/scripts/constants.gd +++ b/client/scripts/constants.gd @@ -59,24 +59,6 @@ const ENTITY_COLOR_HOSTILE: Color = Color("#d45d5d") # Hostile/Dangerous — const ENTITY_COLOR_OBJECT: Color = Color("#8b8ba0") # Static objects — muted grey const ENTITY_COLOR_PLAYER: Color = Color("#e0e8ff") # Player character (detective) -# 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 - -# 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 - # 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 @@ -96,11 +78,6 @@ const FACING_INDICATOR_OFFSET: float = 14.0 # two columns of text comfortably, leaves world game visible alongside. const DIALOGUE_MAX_WIDTH: int = 1200 -# D-031: Format game-minutes (0..1439) as station local time string "HH:MM". -static func format_game_time(time_of_day: int) -> String: - var clamped: int = clampi(time_of_day, 0, 1439) - return "%02d:%02d" % [clamped / 60, clamped % 60] - # Default camera zoom — used as fallback when get_camera_2d() returns null const CAMERA_DEFAULT_ZOOM: Vector2 = Vector2(2.0, 2.0) @@ -115,3 +92,26 @@ const IMPLANT_TEXT_DIM: Color = Color("#9EBFC4") # Dimmed variant — second 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 + +# 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 + +# D-031: Format game-minutes (0..1439) as station local time string "HH:MM". +static func format_game_time(time_of_day: int) -> String: + var clamped: int = clampi(time_of_day, 0, 1439) + return "%02d:%02d" % [clamped / 60, clamped % 60] diff --git a/client/scripts/protocol/local_bridge.gd b/client/scripts/protocol/local_bridge.gd index a98891836..601e0d6d5 100644 --- a/client/scripts/protocol/local_bridge.gd +++ b/client/scripts/protocol/local_bridge.gd @@ -93,7 +93,8 @@ func _try_extract_message() -> PackedByteArray: 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]) + push_error( + "LocalBridge: incoming message too large: %d bytes (max %d) — disconnecting" % [_pending_length, MAX_MESSAGE_SIZE]) _corrupt = true _pending_length = -1 _read_buffer.clear() diff --git a/client/scripts/protocol/protocol.gd b/client/scripts/protocol/protocol.gd index 93a25a2f1..203a412d6 100644 --- a/client/scripts/protocol/protocol.gd +++ b/client/scripts/protocol/protocol.gd @@ -35,7 +35,8 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: # Version check: reject snapshots from incompatible server 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]) + push_error( + "Protocol: version mismatch (got %s, expected %s). Server and client are out of sync." % [version, PROTOCOL_VERSION]) return null var entities: Array[Dictionary] = [] @@ -49,7 +50,8 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant: dropped += 1 if dropped > 0: - push_error("Protocol: %d/%d entities failed to decode (D-010 information boundary violation)" % [dropped, raw_entities.size()]) + push_error( + "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). @@ -484,12 +486,11 @@ static func _decode_verb_option(raw) -> Variant: static func _decode_enum_variant(raw) -> Dictionary: if raw is String: return { "variant": raw, "data": null } - elif raw is Dictionary and raw.size() == 1: + if raw is Dictionary and raw.size() == 1: var variant_name: String = raw.keys()[0] return { "variant": variant_name, "data": raw[variant_name] } - else: - push_warning("Protocol: unexpected enum encoding: %s" % str(raw)) - return { "variant": "Unknown", "data": raw } + push_warning("Protocol: unexpected enum encoding: %s" % str(raw)) + return { "variant": "Unknown", "data": raw } # -- Encode: GDScript types → bytes to server ---------------------------------- @@ -499,7 +500,8 @@ static func _decode_enum_variant(raw) -> Dictionary: ## 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: +static func encode_startup_message( + 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". diff --git a/client/scripts/protocol/test_harness.gd b/client/scripts/protocol/test_harness.gd index f40055dc0..8808f8cb9 100644 --- a/client/scripts/protocol/test_harness.gd +++ b/client/scripts/protocol/test_harness.gd @@ -5,6 +5,20 @@ extends RefCounted ## interactions. Extracted from sim_bridge.gd to enforce D-020 information ## boundary (no game logic in the production client autoload). +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), + # Interior wall blocking NPC + Vector2i(12, 10), +] + var tick: int = 0 var player_pos: Vector2i = Vector2i(10, 10) var facing: String = "North" @@ -116,9 +130,9 @@ func snapshot() -> Dictionary: "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}, - {"text": "Passing through. Know where I can find work?", "response_id": "kael_greet_02", "priority": 2, "confrontation": false}, - {"text": "I saw you near the cargo bay last night.", "response_id": "kael_confront_01", "priority": 3, "confrontation": true}, + {"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 ], } @@ -146,7 +160,7 @@ func snapshot() -> Dictionary: {"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."}, + {"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 @@ -259,21 +273,6 @@ func _get_tile_type(x: int, y: int) -> String: # -- Spatial helpers ----------------------------------------------------------- -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), - # Interior wall blocking NPC - Vector2i(12, 10), -] - - func _is_walkable(pos: Vector2i) -> bool: return not _WALLS.has(pos) diff --git a/client/scripts/rendering/character_visual.gd b/client/scripts/rendering/character_visual.gd index 6598bb6a7..0ed800c76 100644 --- a/client/scripts/rendering/character_visual.gd +++ b/client/scripts/rendering/character_visual.gd @@ -451,7 +451,8 @@ func _load_eyebrows(_desc: CharacterVisualDescriptor) -> void: pass -func _attach_to_bone(path: String, bone_name: String, tint: Color = Color.WHITE, render_priority: int = 0) -> BoneAttachment3D: +func _attach_to_bone( + path: String, bone_name: String, tint: Color = Color.WHITE, _render_priority: int = 0) -> BoneAttachment3D: if _skeleton == null: return null if not ResourceLoader.exists(path): @@ -502,14 +503,13 @@ func _attach_to_bone(path: String, bone_name: String, tint: Color = Color.WHITE, _apply_tinted_shader(mi, tint, mask_tex) inst.queue_free() return null - else: - # Unskinned — rigid attachment via BoneAttachment3D - for mi in meshes: - mi.get_parent().remove_child(mi) - mi.owner = null - attachment.add_child(mi) - _apply_tinted_shader(mi, tint, mask_tex) - inst.queue_free() + # Unskinned — rigid attachment via BoneAttachment3D + for mi in meshes: + mi.get_parent().remove_child(mi) + mi.owner = null + attachment.add_child(mi) + _apply_tinted_shader(mi, tint, mask_tex) + inst.queue_free() return attachment @@ -629,7 +629,8 @@ 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: +func _apply_tinted_shader( + 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()): diff --git a/client/scripts/rendering/cursor_renderer.gd b/client/scripts/rendering/cursor_renderer.gd index 690f05d46..7b75e1ed7 100644 --- a/client/scripts/rendering/cursor_renderer.gd +++ b/client/scripts/rendering/cursor_renderer.gd @@ -5,19 +5,11 @@ extends Node2D ## Insert-styled cursor on z-layer 7 (UILayer CanvasLayer). ## Detects entity hover via world-space proximity to visible entities. -enum State { DEFAULT, ENTITY_HOVER, OBJECT_HOVER, WEAPON_AIM } - -# --- Public --- -var current_state: State = State.DEFAULT -var hovered_entity_id: int = -1 -var weapon_mode_active: bool = false -# OQ-07 (#522): when false, verb labels are suppressed (should_show_interactions → false). -# Cursor shape transitions still fire — the character's body still orients to targets. -var insert_active: bool = true - -signal state_changed(new_state: State) +signal state_changed(new_state: int) signal hovered_entity_changed(entity_id: int) +enum State { DEFAULT, ENTITY_HOVER, OBJECT_HOVER, WEAPON_AIM } + # D-056 colors const COLOR_DEFAULT := Color("#c8d0e0") const COLOR_OBJECT := Color("#8b8ba0") @@ -31,6 +23,14 @@ const HOVER_RADIUS_PX := 16.0 # World pixels — ~half a tile const BRACKET_HALF := 26.0 const BRACKET_ARM := 8.0 +# --- Public --- +var current_state: State = State.DEFAULT +var hovered_entity_id: int = -1 +var weapon_mode_active: bool = false +# OQ-07 (#522): when false, verb labels are suppressed (should_show_interactions → false). +# Cursor shape transitions still fire — the character's body still orients to targets. +var insert_active: bool = true + # --- Transition state --- var _target: State = State.DEFAULT var _t: float = 1.0 diff --git a/client/scripts/rendering/entity_renderer.gd b/client/scripts/rendering/entity_renderer.gd index 91586f77f..dc6376127 100644 --- a/client/scripts/rendering/entity_renderer.gd +++ b/client/scripts/rendering/entity_renderer.gd @@ -25,6 +25,8 @@ const ENTITY_OFFSET_Y: float = TILE_SIZE - ENTITY_HEIGHT # feet-anchored for co # At 12.0: ~70% there after 0.1s, ~95% after 0.25s. # Fast enough for Sprint snappiness, slow enough for Walk to show sliding. 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) @@ -32,9 +34,6 @@ var _entity_relationships: Dictionary = {} # #521: entity_id -> String (last re 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") -# #521: Color transition duration in seconds (D-033: "0.5s fade") -const COLOR_FADE_DURATION: float = 0.5 - func _ready() -> void: print("EntityRenderer: Initialized") @@ -111,7 +110,8 @@ func _create_entity_node(entity_id: int, entity_data: Dictionary) -> void: _entity_facing[entity_id] = direction 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]) + push_error( + "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. diff --git a/client/scripts/rendering/fog_shader.gd b/client/scripts/rendering/fog_shader.gd index 3349f5d9c..ba3ad87cd 100644 --- a/client/scripts/rendering/fog_shader.gd +++ b/client/scripts/rendering/fog_shader.gd @@ -5,13 +5,14 @@ extends Node2D ## Architecture: docs/architecture/fog-shader-spec.md signal fog_noise_ready + +const TILE_SIZE := float(Constants.TILE_SIZE) + var _noise_ready: bool = false var _fog_rect: ColorRect var _shader_mat: ShaderMaterial -const TILE_SIZE := float(Constants.TILE_SIZE) - func _ready() -> void: # Create the fog overlay ColorRect — transparent fallback so a shader failure diff --git a/client/scripts/rendering/tile_renderer.gd b/client/scripts/rendering/tile_renderer.gd index f9081a8e7..cf19a8f41 100644 --- a/client/scripts/rendering/tile_renderer.gd +++ b/client/scripts/rendering/tile_renderer.gd @@ -11,11 +11,11 @@ extends TileMapLayer # (3,0) = object — teal # (4,0) = reset_plate — amber (#502) +enum TileType { FLOOR = 0, WALL = 1, DOOR = 2, OBJECT = 3, RESET_PLATE = 4 } + const TILE_SIZE: int = Constants.TILE_SIZE const GROUND_FLOOR: int = 0 # Server floor level for ground — filter target in update_tiles() -enum TileType { FLOOR = 0, WALL = 1, DOOR = 2, OBJECT = 3, RESET_PLATE = 4 } - # Wire-format string to TileType mapping const TILE_TYPE_MAP: Dictionary = { "floor": TileType.FLOOR, diff --git a/client/scripts/rendering/world_renderer.gd b/client/scripts/rendering/world_renderer.gd index be3ffe63a..7350bb08f 100644 --- a/client/scripts/rendering/world_renderer.gd +++ b/client/scripts/rendering/world_renderer.gd @@ -13,13 +13,13 @@ extends Node2D # Overhead (Node2D) — z:300 ceiling/upper structure (placeholder) # FogOverlay (Node2D) — z:900 fog shader (OUTSIDE FogGroup) +var _last_tick: int = -1 + @onready var tile_renderer = $FogGroup/FloorTiles @onready var fog_renderer = $FogOverlay @onready var entity_renderer = $FogGroup/YSortGroup/Entities @onready var sound_indicator_renderer = $SoundIndicators # #126 D-018 medium-range indicators -var _last_tick: int = -1 - func _ready() -> void: print("WorldRenderer: Initialized (D-049 z-stack)") diff --git a/client/tests/gen_client_fixtures.gd b/client/tests/gen_client_fixtures.gd index 20383136a..0fe60c087 100644 --- a/client/tests/gen_client_fixtures.gd +++ b/client/tests/gen_client_fixtures.gd @@ -10,7 +10,7 @@ ## run before the project's class_name registry is fully populated. extends SceneTree -var _Msgpack: GDScript +var _msgpack: GDScript var _count := 0 var _errors := 0 var _output_dir: String @@ -21,7 +21,7 @@ func _init(): func _run(): - _Msgpack = load("res://addons/messagepack/messagepack.gd") + _msgpack = load("res://addons/messagepack/messagepack.gd") # Resolve repo root from Godot project root (client/). # Assumes client/ is one level below repo root — validated below. @@ -81,7 +81,7 @@ func _encode_input(tick: int, action_name: String, action_data: Variant = null) else: action = action_name - var result = _Msgpack.encode({"tick": tick, "action": action}) + var result = _msgpack.encode({"tick": tick, "action": action}) if result.status != null: push_error("Encode failed: %s" % result.status) return PackedByteArray() @@ -104,7 +104,7 @@ func _encode_inputs(inputs: Array) -> PackedByteArray: action = action_name wire_inputs.append({"tick": input["tick"], "action": action}) - var result = _Msgpack.encode(wire_inputs) + var result = _msgpack.encode(wire_inputs) if result.status != null: push_error("Batch encode failed: %s" % result.status) return PackedByteArray() diff --git a/client/tests/test_ai_dialogue_sprint26.gd b/client/tests/test_ai_dialogue_sprint26.gd index 7d881cf5d..516c65418 100644 --- a/client/tests/test_ai_dialogue_sprint26.gd +++ b/client/tests/test_ai_dialogue_sprint26.gd @@ -10,6 +10,7 @@ class_name TestAiDialogueSprint26 extends GdUnitTestSuite +const SETTINGS_DIALOG_SCENE = preload("res://ui/settings_dialog.tscn") var _original_ai_enabled: bool = true @@ -345,7 +346,7 @@ func test_hardware_classify_degradation_ok_at_exact_40_percent() -> void: func test_settings_dialog_exposes_ai_dialogue_label_text_method() -> void: # settings_dialog needs a testable API — hardcoded UI strings are easy to drift. - var scene := load("res://ui/settings_dialog.tscn") as PackedScene + var scene := SETTINGS_DIALOG_SCENE if scene == null: push_warning("TestAiDialogueSprint26: settings_dialog.tscn not found — skipped") return @@ -359,7 +360,7 @@ func test_settings_dialog_exposes_ai_dialogue_label_text_method() -> void: func test_settings_dialog_ai_dialogue_label_is_correct() -> void: # D-138: label must be exactly "AI-Enhanced Dialogue" (Jeroen's wording). - var scene := load("res://ui/settings_dialog.tscn") as PackedScene + var scene := SETTINGS_DIALOG_SCENE if scene == null: push_warning("TestAiDialogueSprint26: settings_dialog.tscn not found — skipped") return @@ -377,7 +378,7 @@ func test_settings_dialog_ai_dialogue_label_is_correct() -> void: func test_settings_dialog_toggle_disabled_when_hardware_fails() -> void: # D-138 §8: RAM < 1.6 GB → feature disabled, toggle greyed out. # Player receives message but cannot enable the feature. - var scene := load("res://ui/settings_dialog.tscn") as PackedScene + var scene := SETTINGS_DIALOG_SCENE if scene == null: push_warning("TestAiDialogueSprint26: settings_dialog.tscn not found — skipped") return @@ -396,7 +397,7 @@ func test_settings_dialog_toggle_disabled_when_hardware_fails() -> void: func test_settings_dialog_toggle_enabled_when_hardware_passes() -> void: # "pass" → toggle available to interact with. - var scene := load("res://ui/settings_dialog.tscn") as PackedScene + var scene := SETTINGS_DIALOG_SCENE if scene == null: push_warning("TestAiDialogueSprint26: settings_dialog.tscn not found — skipped") return @@ -415,7 +416,7 @@ func test_settings_dialog_toggle_enabled_when_hardware_passes() -> void: func test_settings_dialog_toggle_enabled_when_hardware_marginal() -> void: # D-138 §8: "marginal" → warn but let player proceed. Never force-disable. - var scene := load("res://ui/settings_dialog.tscn") as PackedScene + var scene := SETTINGS_DIALOG_SCENE if scene == null: push_warning("TestAiDialogueSprint26: settings_dialog.tscn not found — skipped") return @@ -439,8 +440,8 @@ func test_hardware_detector_benchmark_cache_path_is_correct() -> void: var det := _get_detector() if det == null: return - assert_str(det.BENCHMARK_CACHE_PATH).override_failure_message( - "BENCHMARK_CACHE_PATH must be 'user://ai-dialogue-config.json' (D-138 §8)" + assert_str(det.benchmark_cache_path).override_failure_message( + "benchmark_cache_path must be 'user://ai-dialogue-config.json' (D-138 §8)" ).is_equal("user://ai-dialogue-config.json") @@ -766,7 +767,7 @@ func test_hardware_detector_battery_suspend_preserves_player_pref_false() -> voi func test_settings_dialog_inference_suspended_state_defaults_false() -> void: - var scene := load("res://ui/settings_dialog.tscn") as PackedScene + var scene := SETTINGS_DIALOG_SCENE if scene == null: push_warning("TestAiDialogueSprint26: settings_dialog.tscn not found — skipped") return @@ -779,7 +780,7 @@ func test_settings_dialog_inference_suspended_state_defaults_false() -> void: func test_settings_dialog_set_inference_suspended_true() -> void: - var scene := load("res://ui/settings_dialog.tscn") as PackedScene + var scene := SETTINGS_DIALOG_SCENE if scene == null: push_warning("TestAiDialogueSprint26: settings_dialog.tscn not found — skipped") return @@ -794,7 +795,7 @@ func test_settings_dialog_set_inference_suspended_true() -> void: func test_settings_dialog_resume_clears_suspended_state() -> void: # D-138 §8: resume when plugged in — suspended state clears. - var scene := load("res://ui/settings_dialog.tscn") as PackedScene + var scene := SETTINGS_DIALOG_SCENE if scene == null: push_warning("TestAiDialogueSprint26: settings_dialog.tscn not found — skipped") return @@ -813,7 +814,7 @@ func test_settings_dialog_toggle_remains_enabled_when_battery_suspended() -> voi # Battery suspend auto-pauses inference but must NOT grey the toggle — # the player can click it to override the suspension. # Only hardware "fail" (RAM < 1.6 GB) may disable the toggle. - var scene := load("res://ui/settings_dialog.tscn") as PackedScene + var scene := SETTINGS_DIALOG_SCENE if scene == null: push_warning("TestAiDialogueSprint26: settings_dialog.tscn not found — skipped") return @@ -829,7 +830,7 @@ func test_settings_dialog_toggle_remains_enabled_when_battery_suspended() -> voi func test_settings_dialog_toggle_enabled_after_resume() -> void: # After resume (plug-in), toggle must be enabled again. - var scene := load("res://ui/settings_dialog.tscn") as PackedScene + var scene := SETTINGS_DIALOG_SCENE if scene == null: push_warning("TestAiDialogueSprint26: settings_dialog.tscn not found — skipped") return @@ -847,7 +848,7 @@ func test_settings_dialog_toggle_enabled_after_resume() -> void: func test_settings_dialog_toggle_disabled_by_hardware_fail_even_when_suspended() -> void: # Hardware "fail" disables the toggle regardless of battery state. # RAM < 1.6 GB is the only hard disable — battery suspend is not. - var scene := load("res://ui/settings_dialog.tscn") as PackedScene + var scene := SETTINGS_DIALOG_SCENE if scene == null: push_warning("TestAiDialogueSprint26: settings_dialog.tscn not found — skipped") return @@ -864,7 +865,7 @@ func test_settings_dialog_toggle_disabled_by_hardware_fail_even_when_suspended() func test_settings_dialog_warning_label_shown_when_battery_suspended() -> void: # When inference is battery-suspended, a warning label must be visible # so the player knows why inference isn't running (even though toggle is enabled). - var scene := load("res://ui/settings_dialog.tscn") as PackedScene + var scene := SETTINGS_DIALOG_SCENE if scene == null: push_warning("TestAiDialogueSprint26: settings_dialog.tscn not found — skipped") return @@ -882,7 +883,7 @@ func test_settings_dialog_warning_label_shown_when_battery_suspended() -> void: func test_settings_dialog_warning_label_hidden_when_not_suspended() -> void: # Warning label must not show when plugged in — no battery message needed. - var scene := load("res://ui/settings_dialog.tscn") as PackedScene + var scene := SETTINGS_DIALOG_SCENE if scene == null: push_warning("TestAiDialogueSprint26: settings_dialog.tscn not found — skipped") return diff --git a/client/tests/test_anti_tedium.gd b/client/tests/test_anti_tedium.gd index f143c22e3..22d4766e0 100644 --- a/client/tests/test_anti_tedium.gd +++ b/client/tests/test_anti_tedium.gd @@ -12,10 +12,11 @@ class_name TestAntiTedium extends GdUnitTestSuite +const MAIN_SCENE = preload("res://scenes/main.tscn") -var _instance: Node = null var GauntletHUDScript = load("res://ui/gauntlet_hud.gd") var BugReportDialogScript = load("res://ui/bug_report_dialog.gd") +var _instance: Node = null func before_test() -> void: @@ -106,7 +107,7 @@ func _make_bug_report_dialog() -> Control: func test_bug_report_dialog_exists_in_scene() -> void: # Verify the BugReportDialog node is present and hidden by default. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -125,7 +126,7 @@ func test_bug_report_dialog_exists_in_scene() -> void: func test_bug_report_activates_on_action() -> void: # Inject BUG_REPORT action directly into the queue and verify main.gd # triggers the dialog. This tests the full main._process() handling path. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -170,7 +171,7 @@ func test_bug_report_activates_on_action() -> void: func test_no_gauntlet_ui_visible_in_default_mode() -> void: # Load main scene — represents non-Gauntlet (default) play mode - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -224,7 +225,7 @@ func test_snapshot_without_room_id_shows_no_gauntlet_ui() -> void: func test_gauntlet_ui_stays_hidden_after_multiple_snapshots() -> void: # Simulate several ticks of normal play — gauntlet UI must never appear. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) diff --git a/client/tests/test_audio_bus_routing.gd b/client/tests/test_audio_bus_routing.gd index 8dda3e962..b9ef1716a 100644 --- a/client/tests/test_audio_bus_routing.gd +++ b/client/tests/test_audio_bus_routing.gd @@ -18,13 +18,13 @@ func before_test() -> void: for bus in AudioManager.BUSES: AudioManager.set_volume(bus, 0.0) GameState.stationary_ticks = 0 - GameState._prev_player_position = Vector2(-1e9, -1e9) + SnapshotHandler._prev_player_position = Vector2(-1e9, -1e9) func after_test() -> void: AudioManager.clear_dip() GameState.stationary_ticks = 0 - GameState._prev_player_position = Vector2(-1e9, -1e9) + SnapshotHandler._prev_player_position = Vector2(-1e9, -1e9) # ============================================================================== diff --git a/client/tests/test_audio_sprint13.gd b/client/tests/test_audio_sprint13.gd index b7580ad34..2f397172e 100644 --- a/client/tests/test_audio_sprint13.gd +++ b/client/tests/test_audio_sprint13.gd @@ -480,7 +480,7 @@ func test_d067_onset_is_when_remaining_equals_total_delay_ticks() -> void: "tick": 1, "pending_recognitions": [ {"entity_id": 99, "x": 10.0, "y": 10.0, "z": 0, - "remaining_ticks": 6, "total_delay_ticks": 6}, + "remaining_ticks": 6, "total_delay_ticks": 6}, ], }) assert_that(GameState.pending_recognitions.size()).is_equal(1) @@ -497,7 +497,7 @@ func test_d067_completion_is_when_entity_absent_from_pending() -> void: "tick": 1, "pending_recognitions": [ {"entity_id": 99, "x": 10.0, "y": 10.0, "z": 0, - "remaining_ticks": 1, "total_delay_ticks": 6}, + "remaining_ticks": 1, "total_delay_ticks": 6}, ], }) assert_that(GameState.pending_recognitions.size()).is_equal(1) diff --git a/client/tests/test_bug_report_ring_buffer.gd b/client/tests/test_bug_report_ring_buffer.gd index f3f3afb5e..eaacf3a1a 100644 --- a/client/tests/test_bug_report_ring_buffer.gd +++ b/client/tests/test_bug_report_ring_buffer.gd @@ -11,12 +11,11 @@ class_name TestBugReportRingBuffer extends GdUnitTestSuite - -var BugReportDialogScript = load("res://ui/bug_report_dialog.gd") - # Expected ring buffer capacity per spec. const EXPECTED_CAPACITY := 60 +var BugReportDialogScript = load("res://ui/bug_report_dialog.gd") + func after_each() -> void: # Reset GameState fields mutated by tests to prevent cross-test leakage. diff --git a/client/tests/test_camera_anchor.gd b/client/tests/test_camera_anchor.gd index 11a45cc05..e7eccd44f 100644 --- a/client/tests/test_camera_anchor.gd +++ b/client/tests/test_camera_anchor.gd @@ -9,6 +9,7 @@ extends GdUnitTestSuite const EXPECTED_PLAYER_POS := Vector2(10, 10) const EXPECTED_CAMERA_POS := Vector2(320, 320) # 10 * 32, 10 * 32 +const MAIN_SCENE = preload("res://scenes/main.tscn") var _instance: Node = null @@ -67,7 +68,7 @@ func test_apply_snapshot_sets_player_position() -> void: # --- Camera anchor after _ready() --- func test_camera_position_after_ready() -> void: - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -77,7 +78,7 @@ func test_camera_position_after_ready() -> void: func test_camera_anchored_flag_after_ready() -> void: - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -88,7 +89,7 @@ func test_camera_anchored_flag_after_ready() -> void: func test_camera_smoothing_off_after_ready() -> void: # Camera smoothing must be disabled during init to prevent lerp from (0,0). # If this test fails, the camera will visibly drift from origin to player. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -100,7 +101,7 @@ func test_camera_smoothing_off_after_ready() -> void: func test_camera_smoothing_stays_off_with_manual_lerp() -> void: # #117: Manual lerp approach — Godot's built-in smoothing must stay OFF always. # CAMERA_SMOOTHING_SPEED is used as the lerp weight, not Godot's position_smoothing_speed. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -114,7 +115,7 @@ func test_camera_smoothing_stays_off_with_manual_lerp() -> void: # --- Camera behavior across frames --- func test_camera_tracks_player_after_process() -> void: - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -130,7 +131,7 @@ func test_camera_tracks_player_after_process() -> void: func test_camera_lerps_toward_player_movement() -> void: # #117: With manual lerp, camera moves TOWARD player position (not snapping). # After one 16ms frame the camera should be partway between old and new position. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) diff --git a/client/tests/test_character_visual_sprint28.gd b/client/tests/test_character_visual_sprint28.gd index f88101243..fa116cedb 100644 --- a/client/tests/test_character_visual_sprint28.gd +++ b/client/tests/test_character_visual_sprint28.gd @@ -161,7 +161,8 @@ func test_reload_descriptor_clears_previous_nodes() -> void: # Second load must produce exactly the same child count — no more, no less. # _clear() frees and rebuilds; any deviation indicates stale nodes accumulating. assert_int(count_after_second).override_failure_message( - "load_descriptor must produce identical child count on reload — stale nodes detected if higher, missing cleanup if lower" + "load_descriptor must produce identical child count on reload" + + " — stale nodes detected if higher, missing cleanup if lower" ).is_equal(count_after_first) @@ -305,7 +306,8 @@ func test_torso_hidden_when_full_coverage_clothing_worn() -> void: desc.clothing_slots = {"torso": "coveralls_basic"} node.load_descriptor(desc) - var coverage: Dictionary = node.get_active_coverage("coveralls_basic") if node.has_method("get_active_coverage") else {} + var coverage: Dictionary = node.get_active_coverage("coveralls_basic") \ + if node.has_method("get_active_coverage") else {} if coverage.is_empty(): push_warning("TestCharacterVisualSprint28: coveralls_basic/coverage.json not found — stub") return diff --git a/client/tests/test_checklist.gd b/client/tests/test_checklist.gd index c6c689e07..ce496dfc8 100644 --- a/client/tests/test_checklist.gd +++ b/client/tests/test_checklist.gd @@ -57,7 +57,8 @@ func test_parse_top_level_quoted_string() -> void: func test_parse_single_condition() -> void: - var yaml := "conditions:\n - id: test-1\n description: \"Test condition\"\n condition_type: player_near\n x: 10\n y: 20\n radius: 3.0" + var yaml := ("conditions:\n - id: test-1\n description: \"Test condition\"\n" + + " condition_type: player_near\n x: 10\n y: 20\n radius: 3.0") var result: Dictionary = ChecklistEvaluatorScript.parse_checklist_yaml(yaml) assert_that(result.has("conditions")).is_true() var conditions: Array = result["conditions"] @@ -70,7 +71,9 @@ func test_parse_single_condition() -> void: func test_parse_multiple_conditions() -> void: - var yaml := "conditions:\n - id: cond-a\n condition_type: player_near\n x: 1\n y: 2\n radius: 1.0\n\n - id: cond-b\n condition_type: player_facing\n direction: East" + var yaml := ("conditions:\n - id: cond-a\n condition_type: player_near\n" + + " x: 1\n y: 2\n radius: 1.0\n\n" + + " - id: cond-b\n condition_type: player_facing\n direction: East") var result: Dictionary = ChecklistEvaluatorScript.parse_checklist_yaml(yaml) var conditions: Array = result["conditions"] assert_that(conditions.size()).is_equal(2) @@ -80,7 +83,8 @@ func test_parse_multiple_conditions() -> void: func test_parse_comments_ignored() -> void: - var yaml := "# This is a comment\nroom_id: test\n# Another comment\nconditions:\n - id: c1\n condition_type: entity_present\n entity_id: 5" + var yaml := ("# This is a comment\nroom_id: test\n# Another comment\n" + + "conditions:\n - id: c1\n condition_type: entity_present\n entity_id: 5") var result: Dictionary = ChecklistEvaluatorScript.parse_checklist_yaml(yaml) assert_that(result.get("room_id")).is_equal("test") var conditions: Array = result["conditions"] diff --git a/client/tests/test_fog_shader.gd b/client/tests/test_fog_shader.gd index e741b808e..88882bd30 100644 --- a/client/tests/test_fog_shader.gd +++ b/client/tests/test_fog_shader.gd @@ -352,7 +352,6 @@ func test_fog_overlay_z_layer() -> void: return # This test needs the scene tree to be set up # Verify via scene file inspection rather than runtime - pass # -- Noise animation cycles (D-059) ------------------------------------------- diff --git a/client/tests/test_hub_teleport.gd b/client/tests/test_hub_teleport.gd index fe727e930..e39366f5b 100644 --- a/client/tests/test_hub_teleport.gd +++ b/client/tests/test_hub_teleport.gd @@ -8,6 +8,8 @@ class_name TestHubTeleport extends GdUnitTestSuite +const _THRESHOLD: float = 5.0 # Mirror of main.gd TELEPORT_DISTANCE_THRESHOLD + # -- Fixtures ------------------------------------------------------------------ @@ -178,8 +180,6 @@ func test_test_mode_teleport_clears_dialogue() -> void: # Threshold constant lives on the main scene node (TELEPORT_DISTANCE_THRESHOLD = 5.0). # These tests verify the distance math against that threshold. -const _THRESHOLD: float = 5.0 # Mirror of main.gd TELEPORT_DISTANCE_THRESHOLD - func test_detect_teleport_large_jump() -> void: # Position jump > threshold should be detected as teleport var old_pos := Vector2(10.0, 10.0) diff --git a/client/tests/test_insert_off_behavior.gd b/client/tests/test_insert_off_behavior.gd index bfd4ffe89..4b80fe567 100644 --- a/client/tests/test_insert_off_behavior.gd +++ b/client/tests/test_insert_off_behavior.gd @@ -44,7 +44,7 @@ func _make_cursor_or_skip() -> Node: func _make_interaction_list() -> Node: for path in ["res://ui/interaction_list.tscn", "res://ui/entity_interaction_list.tscn", - "res://scenes/interaction_list.tscn"]: + "res://scenes/interaction_list.tscn"]: if ResourceLoader.exists(path): var scene = load(path) var node = scene.instantiate() diff --git a/client/tests/test_interaction_list.gd b/client/tests/test_interaction_list.gd index f122d1295..5f4cd3f3b 100644 --- a/client/tests/test_interaction_list.gd +++ b/client/tests/test_interaction_list.gd @@ -12,7 +12,7 @@ extends GdUnitTestSuite func _interaction_list_exists() -> bool: for path in ["res://ui/interaction_list.tscn", "res://ui/entity_interaction_list.tscn", - "res://scenes/interaction_list.tscn"]: + "res://scenes/interaction_list.tscn"]: if ResourceLoader.exists(path): return true return false @@ -20,7 +20,7 @@ func _interaction_list_exists() -> bool: func _make_interaction_list() -> Node: for path in ["res://ui/interaction_list.tscn", "res://ui/entity_interaction_list.tscn", - "res://scenes/interaction_list.tscn"]: + "res://scenes/interaction_list.tscn"]: if ResourceLoader.exists(path): var scene = load(path) var node = scene.instantiate() diff --git a/client/tests/test_interaction_prompt.gd b/client/tests/test_interaction_prompt.gd index 722e9c2e6..b2d79190e 100644 --- a/client/tests/test_interaction_prompt.gd +++ b/client/tests/test_interaction_prompt.gd @@ -14,7 +14,7 @@ func test_protocol_decode_v4_with_nearby_interactions() -> void: "version": Protocol.PROTOCOL_VERSION, "entities": [ {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": "Player", - "visibility": "Forward", "relationship": "Unknown", "observation": "Visible"}, + "visibility": "Forward", "relationship": "Unknown", "observation": "Visible"}, ], "nearby_interactions": [{ "entity_id": 2, @@ -93,7 +93,7 @@ func test_protocol_decode_v4_entity_relationship() -> void: "version": Protocol.PROTOCOL_VERSION, "entities": [ {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": "Npc", - "visibility": "Forward", "relationship": "Friendly", "observation": "Visible"}, + "visibility": "Forward", "relationship": "Friendly", "observation": "Visible"}, ], } var encoded = Messagepack.encode(raw) diff --git a/client/tests/test_minimap_sprint18.gd b/client/tests/test_minimap_sprint18.gd index 5013ed5e6..989219d87 100644 --- a/client/tests/test_minimap_sprint18.gd +++ b/client/tests/test_minimap_sprint18.gd @@ -16,6 +16,7 @@ extends GdUnitTestSuite # --------------------------------------------------------------------------- const MINIMAP_SCENE_PATH: String = "res://ui/minimap.tscn" +const MAIN_SCENE = preload("res://scenes/main.tscn") func _make_minimap() -> Control: if not ResourceLoader.exists(MINIMAP_SCENE_PATH): @@ -199,7 +200,7 @@ func test_minimap_in_main_scene_on_insert_overlay() -> void: if not ResourceLoader.exists("res://scenes/main.tscn"): push_warning("TestMinimapSprint18: main.tscn not found — scene tree test skipped") return - var scene: Node = load("res://scenes/main.tscn").instantiate() + var scene: Node = MAIN_SCENE.instantiate() auto_free(scene) add_child(scene) @@ -226,7 +227,7 @@ func test_insert_overlay_is_canvas_layer_10() -> void: if not ResourceLoader.exists("res://scenes/main.tscn"): push_warning("TestMinimapSprint18: main.tscn not found — canvas layer test skipped") return - var scene: Node = load("res://scenes/main.tscn").instantiate() + var scene: Node = MAIN_SCENE.instantiate() auto_free(scene) add_child(scene) diff --git a/client/tests/test_p0_regressions.gd b/client/tests/test_p0_regressions.gd index e1de8eb9d..845eec824 100644 --- a/client/tests/test_p0_regressions.gd +++ b/client/tests/test_p0_regressions.gd @@ -11,6 +11,8 @@ class_name TestP0Regressions extends GdUnitTestSuite +const MAIN_SCENE = preload("res://scenes/main.tscn") + var _instance: Node = null @@ -174,7 +176,7 @@ func test_monologue_carry_forward_preserves_newest() -> void: func test_camera_static_during_pause() -> void: # 1. Instantiate main scene — camera anchors at test mode player position - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -204,7 +206,7 @@ func test_camera_static_during_pause() -> void: func test_camera_anchored_after_pause_unpause() -> void: # Verify camera stays properly anchored through a pause → unpause cycle. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) diff --git a/client/tests/test_protocol_bridge.gd b/client/tests/test_protocol_bridge.gd index ab068f3d7..cdb16f175 100644 --- a/client/tests/test_protocol_bridge.gd +++ b/client/tests/test_protocol_bridge.gd @@ -346,7 +346,7 @@ func test_full_v6_snapshot_decode() -> void: ], "entities": [ {"entity_id": 1, "x": 10.0, "y": 10.0, "z": 0, "kind": "Player", - "visibility": "Forward", "relationship": "Unknown", "observation": "Visible"}, + "visibility": "Forward", "relationship": "Unknown", "observation": "Visible"}, ], "visible_tiles": [ {"x": 10, "y": 10, "z": 0, "visibility": "Forward", "tile_kind": "Floor"}, diff --git a/client/tests/test_protocol_v7.gd b/client/tests/test_protocol_v7.gd index 3b6d9198d..649036aee 100644 --- a/client/tests/test_protocol_v7.gd +++ b/client/tests/test_protocol_v7.gd @@ -339,7 +339,7 @@ func test_full_v7_snapshot_decode() -> void: "player_inventory": [{"item_id": 100, "name": "Access Token", "slot": 0}], "entities": [ {"entity_id": 1, "x": 10.0, "y": 10.0, "z": 0, "kind": "Player", - "visibility": "Forward", "relationship": "Unknown", "observation": "Visible"}, + "visibility": "Forward", "relationship": "Unknown", "observation": "Visible"}, ], "visible_tiles": [], "nearby_interactions": [], diff --git a/client/tests/test_rendering.gd b/client/tests/test_rendering.gd index 2fbe3c651..399283250 100644 --- a/client/tests/test_rendering.gd +++ b/client/tests/test_rendering.gd @@ -281,7 +281,8 @@ func test_entity_renderer_npc_uses_unknown_teal() -> void: func test_entity_renderer_object_uses_grey() -> void: var renderer := _make_entity_renderer() - var obj := [{"entity_id": 3, "x": 1.0, "y": 1.0, "z": 0, "kind": {"variant": "Object", "data": null}, "visibility": "Forward"}] + var obj := [{"entity_id": 3, "x": 1.0, "y": 1.0, "z": 0, + "kind": {"variant": "Object", "data": null}, "visibility": "Forward"}] renderer.update_entities(obj) var node = renderer.entity_nodes[3] as Sprite2D assert_that(node.self_modulate).is_equal(Constants.ENTITY_COLOR_OBJECT) diff --git a/client/tests/test_session_manager_sprint19.gd b/client/tests/test_session_manager_sprint19.gd index ef6f68fd3..9174bb10f 100644 --- a/client/tests/test_session_manager_sprint19.gd +++ b/client/tests/test_session_manager_sprint19.gd @@ -4,6 +4,8 @@ class_name TestSessionManagerSprint19 extends GdUnitTestSuite +const MAIN_MENU_SCENE = preload("res://scenes/main_menu.tscn") + # Game IDs created during the current test — deleted in after_test(). var _created_ids: Array = [] @@ -166,7 +168,7 @@ func test_main_menu_instantiates_without_crash() -> void: if not ResourceLoader.exists("res://scenes/main_menu.tscn"): push_warning("TestSessionManagerSprint19: main_menu.tscn not found — skip") return - var scene: Node = load("res://scenes/main_menu.tscn").instantiate() + var scene: Node = MAIN_MENU_SCENE.instantiate() auto_free(scene) add_child(scene) assert_that(scene).is_not_null() @@ -175,7 +177,7 @@ func test_main_menu_instantiates_without_crash() -> void: func test_main_menu_has_new_game_button() -> void: if not ResourceLoader.exists("res://scenes/main_menu.tscn"): return - var scene: Node = load("res://scenes/main_menu.tscn").instantiate() + var scene: Node = MAIN_MENU_SCENE.instantiate() auto_free(scene) add_child(scene) var btn := scene.get_node_or_null("VBox/NewGameBtn") @@ -187,7 +189,7 @@ func test_main_menu_has_new_game_button() -> void: func test_main_menu_has_continue_button() -> void: if not ResourceLoader.exists("res://scenes/main_menu.tscn"): return - var scene: Node = load("res://scenes/main_menu.tscn").instantiate() + var scene: Node = MAIN_MENU_SCENE.instantiate() auto_free(scene) add_child(scene) var btn := scene.get_node_or_null("VBox/ContinueBtn") diff --git a/client/tests/test_signal_sprint24.gd b/client/tests/test_signal_sprint24.gd index 72a4fa40b..549b14696 100644 --- a/client/tests/test_signal_sprint24.gd +++ b/client/tests/test_signal_sprint24.gd @@ -10,6 +10,8 @@ class_name TestSignalSprint24 extends GdUnitTestSuite +const NEWS_TICKER_SCENE = preload("res://ui/news_ticker.tscn") + # -- #588: Character archetype field ------------------------------------------ @@ -204,7 +206,7 @@ func test_protocol_decode_current_ticker_null_when_absent() -> void: func test_news_ticker_hidden_when_snapshot_has_no_ticker() -> void: # update_from_state() must hide ticker when current_ticker is null. - var ticker_scene := load("res://ui/news_ticker.tscn") as PackedScene + var ticker_scene := NEWS_TICKER_SCENE assert_that(ticker_scene).is_not_null() var ticker := ticker_scene.instantiate() auto_free(ticker) @@ -224,7 +226,7 @@ func test_news_ticker_hidden_when_snapshot_has_no_ticker() -> void: func test_news_ticker_visible_when_snapshot_has_ticker() -> void: # update_from_state() must show ticker when current_ticker has text. - var ticker_scene := load("res://ui/news_ticker.tscn") as PackedScene + var ticker_scene := NEWS_TICKER_SCENE assert_that(ticker_scene).is_not_null() var ticker := ticker_scene.instantiate() auto_free(ticker) @@ -244,7 +246,7 @@ func test_news_ticker_visible_when_snapshot_has_ticker() -> void: func test_news_ticker_hides_when_ticker_becomes_null() -> void: # Ticker shown then hidden: update_from_state() with null current_ticker hides it. - var ticker_scene := load("res://ui/news_ticker.tscn") as PackedScene + var ticker_scene := NEWS_TICKER_SCENE assert_that(ticker_scene).is_not_null() var ticker := ticker_scene.instantiate() auto_free(ticker) diff --git a/client/tests/test_smooth_camera_sprint15.gd b/client/tests/test_smooth_camera_sprint15.gd index 86a73b08d..1c4cbfac1 100644 --- a/client/tests/test_smooth_camera_sprint15.gd +++ b/client/tests/test_smooth_camera_sprint15.gd @@ -4,6 +4,8 @@ class_name TestSmoothCameraSprint15 extends GdUnitTestSuite +const MAIN_SCENE = preload("res://scenes/main.tscn") + var _instance: Node = null @@ -42,7 +44,7 @@ func test_camera_smoothing_speed_constant_reasonable() -> void: func test_godot_smoothing_disabled_at_ready() -> void: # #117: Godot's built-in Camera2D smoothing must be OFF (manual lerp replaces it). - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -53,7 +55,7 @@ func test_godot_smoothing_disabled_at_ready() -> void: func test_godot_smoothing_stays_off_after_frames() -> void: # #117: Smoothing must NOT be re-enabled at any point — manual lerp only. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -70,7 +72,7 @@ func test_godot_smoothing_stays_off_after_frames() -> void: func test_camera_lerps_not_snaps_on_player_move() -> void: # #117: When player moves, camera should lerp (not snap) to new position. # After 1 frame at ~60fps, camera should be partway there — not at target. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -90,7 +92,7 @@ func test_camera_lerps_not_snaps_on_player_move() -> void: func test_camera_converges_to_player_over_multiple_frames() -> void: # #117: After enough frames the camera should be within 1px of target. # At LERP_SPEED=8: ~95% convergence in 0.25s, >99% in 0.5s. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -111,7 +113,7 @@ func test_camera_converges_to_player_over_multiple_frames() -> void: func test_camera_stationary_player_no_drift() -> void: # #117: When player is stationary, camera should not drift (lerp to same point). - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -133,7 +135,7 @@ func test_camera_stationary_player_no_drift() -> void: func test_teleport_snaps_camera_immediately() -> void: # #117: _teleport_in_progress causes camera to snap (not lerp) in the same frame. # Manually displace camera, set the flag, call _process — camera should snap to target. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -155,7 +157,7 @@ func test_teleport_snaps_camera_immediately() -> void: func test_teleport_flag_cleared_after_snap() -> void: # #117: _teleport_in_progress must be false after the snap frame. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -170,7 +172,7 @@ func test_teleport_flag_cleared_after_snap() -> void: func test_camera_resumes_lerp_after_teleport() -> void: # #117: Frame after teleport snap must resume lerp (not continue snapping). # After teleport flag clears, any position delta produces lerp movement. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -200,7 +202,7 @@ func test_camera_resumes_lerp_after_teleport() -> void: func test_camera_no_rotation() -> void: # D-015: Camera must be fixed-north in v0.1 — no rotation regardless of facing. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) diff --git a/client/tests/test_time_display_sprint17.gd b/client/tests/test_time_display_sprint17.gd index c19ca6a0e..bd583cb05 100644 --- a/client/tests/test_time_display_sprint17.gd +++ b/client/tests/test_time_display_sprint17.gd @@ -17,6 +17,8 @@ class_name TestTimeDisplaySprint17 extends GdUnitTestSuite +const MAIN_SCENE = preload("res://scenes/main.tscn") + var _clock: Control = null @@ -275,7 +277,7 @@ func test_sim_bridge_day_phase_is_valid() -> void: # ------------------------------------------------------------------------- func test_insert_clock_exists_in_ui_layer() -> void: - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE var instance = scene.instantiate() auto_free(instance) add_child(instance) @@ -283,7 +285,7 @@ func test_insert_clock_exists_in_ui_layer() -> void: assert_that(instance.get_node_or_null("InsertOverlay/TimeDisplay")).is_not_null() func test_insert_clock_time_str_updates_after_process() -> void: - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE var instance = scene.instantiate() auto_free(instance) add_child(instance) diff --git a/client/tests/test_ui_framework_sprint15.gd b/client/tests/test_ui_framework_sprint15.gd index 6b77cf4a9..ff0ced7bd 100644 --- a/client/tests/test_ui_framework_sprint15.gd +++ b/client/tests/test_ui_framework_sprint15.gd @@ -4,6 +4,8 @@ class_name TestUIFrameworkSprint15 extends GdUnitTestSuite +const MAIN_SCENE = preload("res://scenes/main.tscn") + var _instance: Node = null @@ -41,7 +43,7 @@ func test_dialogue_max_width_set() -> void: func test_insert_overlay_is_canvas_layer_10() -> void: # D-049: InsertOverlay = conceptual layer 6 (insert scope) = CanvasLayer 10. # Constants.CANVAS_INSERT must match. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -53,7 +55,7 @@ func test_insert_overlay_is_canvas_layer_10() -> void: func test_ui_layer_is_canvas_layer_20() -> void: # D-049: UILayer = conceptual layer 7 (UI/monologue scope) = CanvasLayer 20. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -65,7 +67,7 @@ func test_ui_layer_is_canvas_layer_20() -> void: func test_modal_layer_is_canvas_layer_30() -> void: # D-049: ModalLayer = pause/inventory modal scope = CanvasLayer 30. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -91,7 +93,7 @@ func test_modal_layer_above_ui_layer() -> void: func test_monologue_display_exists_in_ui_layer() -> void: # D-049 / #117 / #414: MonologueDisplay must be in UILayer (layer 7). - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -101,7 +103,7 @@ func test_monologue_display_exists_in_ui_layer() -> void: func test_stance_indicator_exists_in_ui_layer() -> void: # D-053: StanceIndicator must be in UILayer (layer 7), top-right, color-coded. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -112,7 +114,7 @@ func test_stance_indicator_exists_in_ui_layer() -> void: func test_minimap_placeholder_exists_in_ui_layer() -> void: # D-013/D-049: Minimap is on InsertOverlay (z-layer 6), NOT UILayer. # Sprint 18 #151 (Stig): moved from UILayer to InsertOverlay per D-049 spec. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -122,7 +124,7 @@ func test_minimap_placeholder_exists_in_ui_layer() -> void: func test_hud_exists_in_ui_layer() -> void: # D-049: HUD must be in UILayer. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -132,7 +134,7 @@ func test_hud_exists_in_ui_layer() -> void: func test_interaction_list_exists_in_insert_overlay() -> void: # D-057: InteractionList must be in InsertOverlay (z-layer 6). - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -142,7 +144,7 @@ func test_interaction_list_exists_in_insert_overlay() -> void: func test_dialogue_box_exists_in_insert_overlay() -> void: # D-061: DialogueBox must be in InsertOverlay (z-layer 6). - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -152,7 +154,7 @@ func test_dialogue_box_exists_in_insert_overlay() -> void: func test_world_radial_exists_in_insert_overlay() -> void: # D-058: WorldRadial must be in InsertOverlay (z-layer 6). - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -162,7 +164,7 @@ func test_world_radial_exists_in_insert_overlay() -> void: func test_cursor_renderer_exists_in_ui_layer() -> void: # D-056: CursorRenderer must be in UILayer (topmost, z-layer 7). - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -182,7 +184,7 @@ func test_gamestate_insert_active_defaults_true() -> void: func test_insert_active_propagates_on_process() -> void: # OQ-07 (#522): After apply_snapshot with insert_active=false, # the next _process() call must propagate the state to z-layer-6 nodes. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -235,7 +237,7 @@ func test_follow_target_id_is_negative_one_by_default() -> void: func test_monologue_display_receives_first_tick_monologue() -> void: # #414 / #74: MonologueDisplay must show monologue from tick 1 test snapshot. # Verifies the wiring: GameState.current_monologue → main.gd → MonologueDisplay. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -254,7 +256,7 @@ func test_monologue_display_receives_first_tick_monologue() -> void: func test_fog_group_exists_in_world() -> void: # Sprint 14 regression: fog rendering must still be present after sprint 15 changes. - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -264,7 +266,7 @@ func test_fog_group_exists_in_world() -> void: func test_floor_tiles_in_fog_group() -> void: # Sprint 14 regression: FloorTiles must be in FogGroup (D-049 z:0). - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -274,7 +276,7 @@ func test_floor_tiles_in_fog_group() -> void: func test_entities_in_ysort_group() -> void: # Sprint 14 regression: Entities must be in YSortGroup for y-sort ordering (D-049 z:100). - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) @@ -288,7 +290,7 @@ func test_entities_in_ysort_group() -> void: func test_tile_renderer_skips_nonzero_z() -> void: # #71: Tiles with z != 0 must be filtered out by update_tiles(). - var scene := load("res://scenes/main.tscn") + var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) diff --git a/client/tests/test_yaml_parser.gd b/client/tests/test_yaml_parser.gd index 6897b4da5..24fab5f4a 100644 --- a/client/tests/test_yaml_parser.gd +++ b/client/tests/test_yaml_parser.gd @@ -250,7 +250,8 @@ func test_dialogue_theme_format() -> void: func test_checklist_format() -> void: ## checklist.yaml: top-level kv + conditions array with typed values. - var yaml := "room_id: inventory_warehouse\nconditions:\n - id: test-1\n condition_type: player_near\n x: 10\n y: 20\n radius: 3.0" + var yaml := ("room_id: inventory_warehouse\nconditions:\n - id: test-1\n" + + " condition_type: player_near\n x: 10\n y: 20\n radius: 3.0") var result := YamlParser.parse(yaml) assert_that(result["room_id"]).is_equal("inventory_warehouse") var cond: Dictionary = result["conditions"][0] diff --git a/client/tests/visual_capture.gd b/client/tests/visual_capture.gd index 60c4bf6aa..64665a89f 100644 --- a/client/tests/visual_capture.gd +++ b/client/tests/visual_capture.gd @@ -31,7 +31,7 @@ func _init(): _run.call_deferred() -func _run(): +func _run(): # gdlint:disable=max-returns # Parse CLI args (after -- separator) _parse_args() diff --git a/client/tests/visual_scenarios.gd b/client/tests/visual_scenarios.gd index 3ba3d001b..88add8ecf 100644 --- a/client/tests/visual_scenarios.gd +++ b/client/tests/visual_scenarios.gd @@ -41,7 +41,6 @@ func apply_setup(scenario_name: String, tree_root: Node) -> bool: sim_bridge.harness.player_pos = Vector2i(10, 10) # Zone tint is written from visible_tiles with zone_id field. # We patch GameState.visible_tiles after the first snapshot in post_setup(). - pass "fog_debug": # Raw exploration overlay (green/blue/red). diff --git a/client/ui/character_creation.gd b/client/ui/character_creation.gd index 7f44841f4..ce10a018b 100644 --- a/client/ui/character_creation.gd +++ b/client/ui/character_creation.gd @@ -1,3 +1,4 @@ +# gdlint:disable=max-file-lines class_name CharacterCreation extends Control ## #705: Character creation screen. @@ -105,18 +106,14 @@ const PALETTE_COLORS: Array[Array] = [ const PALETTE_COLS := 9 const RECENT_SLOTS := 9 -# --- @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 _cam_angle_btn: Button = $Layout/PreviewPanel/PreviewOverlay/CamAngleBtn -var _tab_container: TabContainer = null # resolved in _ready() — @onready path fails when instantiated as child -@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 +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" + +const SCREENSHOT_DIR := "user://screenshots/" +const CARDINAL_NAMES: Array[String] = ["south", "east", "north", "west"] # --- Descriptor and preview state --- var _descriptor: CharacterVisualDescriptor @@ -125,9 +122,6 @@ var _facing_idx: int = 0 # index into CARDINAL_DIRS (0 = south, default face 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 -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 # --- Tab active slot state --- var _active_clothing_slot: String = "torso" @@ -197,7 +191,31 @@ var _tab_grids: Array[GridContainer] = [null, null, null, null, null] # --- Asset manifest (loaded once, replaces filesystem scanning) --- var _manifest: Dictionary = {} -const MANIFEST_PATH := "res://assets/characters/manifest.json" +# --- Debug tab state --- +var _debug_toggles: Dictionary = {} # seg_name -> CheckButton + +# --- Screenshot / automated testing state --- +var _screenshot_delay_frames: int = 5 # wait N frames for scene to render +var _screenshot_pending: bool = false +var _screenshot_frame_count: int = 0 +var _quit_after_screenshot: bool = false +var _screenshot_cardinals: bool = false +var _screenshot_cardinal_idx: int = 0 + +# resolved in _ready() — @onready path fails when instantiated as child +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 _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 # ============================================================================= @@ -341,7 +359,8 @@ 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 @@ -1084,15 +1103,6 @@ func _update_accessory_item_btns() -> void: # Screenshot & automated testing # ============================================================================= -const SCREENSHOT_DIR := "user://screenshots/" -var _screenshot_delay_frames: int = 5 # wait N frames for scene to render -var _screenshot_pending: bool = false -var _screenshot_frame_count: int = 0 -var _quit_after_screenshot: bool = false -var _screenshot_cardinals: bool = false -var _screenshot_cardinal_idx: int = 0 -const CARDINAL_NAMES: Array[String] = ["south", "east", "north", "west"] - func _schedule_screenshot() -> void: _screenshot_pending = true _screenshot_frame_count = 0 @@ -1131,8 +1141,7 @@ func _take_screenshot(suffix: String = "") -> void: # More directions to capture _schedule_screenshot() return - else: - _screenshot_cardinals = false + _screenshot_cardinals = false if _quit_after_screenshot: get_tree().quit() @@ -1203,8 +1212,6 @@ func _load_test_config() -> void: # Debug tab — segment visibility toggles # ============================================================================= -var _debug_toggles: Dictionary = {} # seg_name -> CheckButton - func _build_debug_tab(tab: Control) -> void: var vbox := _make_tab_vbox(tab) @@ -1501,7 +1508,7 @@ func _input(event: InputEvent) -> void: _cam_zoom_toward_cursor(mb.position, -CAM_ZOOM_STEP) get_viewport().set_input_as_handled() return - elif mb.button_index == MOUSE_BUTTON_WHEEL_DOWN: + if mb.button_index == MOUSE_BUTTON_WHEEL_DOWN: _cam_zoom_toward_cursor(mb.position, CAM_ZOOM_STEP) get_viewport().set_input_as_handled() return @@ -1873,7 +1880,7 @@ func _get_accessory_ids_for_slot(slot: String) -> Array: "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 + _: return all_ids # gdlint:ignore = max-returns # ============================================================================= diff --git a/client/ui/checklist_overlay.gd b/client/ui/checklist_overlay.gd index da0089754..41f8811e5 100644 --- a/client/ui/checklist_overlay.gd +++ b/client/ui/checklist_overlay.gd @@ -6,7 +6,7 @@ extends Control ## ## Spec ref: D-030 (testability), #503, Sprint 10 Completion Proof. -const _ChecklistEvaluator = preload("res://scripts/checklist/checklist_evaluator.gd") +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 @@ -26,7 +26,7 @@ var _cached_font: Font = null # Cached to avoid per-frame theme lookup func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE visible = false - _evaluator = _ChecklistEvaluator.new() + _evaluator = ChecklistEvaluator.new() _cached_font = get_theme_default_font() diff --git a/client/ui/debug_overlay.gd b/client/ui/debug_overlay.gd index 362e5758d..a80e0e4ca 100644 --- a/client/ui/debug_overlay.gd +++ b/client/ui/debug_overlay.gd @@ -232,11 +232,15 @@ func _draw_stats_panel() -> void: 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 @@ -335,7 +339,8 @@ 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) diff --git a/client/ui/dialogue_box.gd b/client/ui/dialogue_box.gd index fee49943d..faea0c99f 100644 --- a/client/ui/dialogue_box.gd +++ b/client/ui/dialogue_box.gd @@ -21,9 +21,26 @@ signal dialogue_state_changed(active: bool) signal audio_dip_requested(profile: String) signal audio_dip_cleared -@onready var panel: PanelContainer = $PanelContainer -@onready var dialogue_log: RichTextLabel = $PanelContainer/MarginContainer/VBoxContainer/DialogueLog -@onready var options_container: VBoxContainer = $PanelContainer/MarginContainer/VBoxContainer/OptionsContainer +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_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 +const CONFRONTATION_MONOLOGUE_KEY: String = "dialogue.confrontation_beat" +const PLAYER_NAME: String = "You" +const PASSIVE_GLYPH: String = "\u2503 " # ┃ prefix for overheard lines (Araminta review) +const PASSIVE_DESATURATION: float = 0.4 # Desaturate passive name colours by this factor +const MIN_CONTRAST_LUMINANCE: float = 0.25 # Floor for name colour brightness against dark BG + +# 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", +] # -- Log state -- # Entry format (legacy): {speaker: String, target: String, text, is_passive, pinned, timestamp_msec} @@ -67,26 +84,9 @@ var _passive_opacity: float = 0.9 var _entry_lifetime: float = 45.0 var _entry_fade: float = 5.0 -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_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 -const CONFRONTATION_MONOLOGUE_KEY: String = "dialogue.confrontation_beat" -const PLAYER_NAME: String = "You" -const PASSIVE_GLYPH: String = "\u2503 " # ┃ prefix for overheard lines (Araminta review) -const PASSIVE_DESATURATION: float = 0.4 # Desaturate passive name colours by this factor -const MIN_CONTRAST_LUMINANCE: float = 0.25 # Floor for name colour brightness against dark BG - -# 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", -] +@onready var panel: PanelContainer = $PanelContainer +@onready var dialogue_log: RichTextLabel = $PanelContainer/MarginContainer/VBoxContainer/DialogueLog +@onready var options_container: VBoxContainer = $PanelContainer/MarginContainer/VBoxContainer/OptionsContainer func _ready() -> void: @@ -197,7 +197,8 @@ func _update_layout() -> void: ## 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, @@ -572,11 +573,10 @@ func _format_entry(entry: Dictionary, alpha: float) -> String: return "%s[color=%s][b]%s[/b][/color][color=%s]: %s[/color]" % [ prefix, sc, speaker, txc, text ] - else: - # 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 - ] + # 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 + ] ## Escape BBCode bracket characters in server-sourced text (Hoshe #2). diff --git a/client/ui/examine_display.gd b/client/ui/examine_display.gd index 3606e20e7..6cb76280a 100644 --- a/client/ui/examine_display.gd +++ b/client/ui/examine_display.gd @@ -21,12 +21,12 @@ const CONFIDENCE_ALPHA: Dictionary = { "Suspects": 0.6, } -@onready var panel: PanelContainer = $PanelContainer -@onready var text_label: RichTextLabel = $PanelContainer/MarginContainer/TextLabel - var _dismiss_tween: Tween = null 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 diff --git a/client/ui/gauntlet_hud.gd b/client/ui/gauntlet_hud.gd index d4d47c163..ca69b1531 100644 --- a/client/ui/gauntlet_hud.gd +++ b/client/ui/gauntlet_hud.gd @@ -114,7 +114,8 @@ func _draw() -> void: # 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: diff --git a/client/ui/hardware_detector.gd b/client/ui/hardware_detector.gd index 48f09cedf..03d4137ea 100644 --- a/client/ui/hardware_detector.gd +++ b/client/ui/hardware_detector.gd @@ -15,7 +15,7 @@ const TPT_YELLOW_THRESHOLD := 3.0 # tokens/sec — partial pre-voicing not const TPT_DEGRADATION_THRESHOLD := 0.4 # fraction — >40% sustained drop → yellow ## Benchmark cache path — reads from PlatformInfo for cross-platform correctness. -var BENCHMARK_CACHE_PATH: String: +var benchmark_cache_path: String: get: return PlatformInfo.benchmark_cache_path @@ -45,10 +45,9 @@ func _ready() -> void: func classify_ram(free_mb: float) -> String: if free_mb >= RAM_PASS_THRESHOLD_MB: return "pass" - elif free_mb >= RAM_MARGINAL_THRESHOLD_MB: + if free_mb >= RAM_MARGINAL_THRESHOLD_MB: return "marginal" - else: - return "fail" + return "fail" ## Query RAM via PlatformInfo and return classification + raw MB value. @@ -65,10 +64,9 @@ func check_ram() -> Dictionary: func classify_tpt(tps: float) -> String: if tps >= TPT_GREEN_THRESHOLD: return "green" - elif tps >= TPT_YELLOW_THRESHOLD: + if tps >= TPT_YELLOW_THRESHOLD: return "yellow" - else: - return "red" + return "red" ## Read the cached TPT benchmark result written by the server on first model load. @@ -192,12 +190,20 @@ func _send_settings_change(enabled: bool) -> void: ## Returns empty string when no message is needed. 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. Close other applications and try again, or leave the setting off — the game is complete either way." % free_mb + return ( + "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 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. Background NPCs may show base text until the queue catches up." % tps + return ( + "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 "red": - return "Running very slowly at %.0f t/s — we recommend leaving this off, but the choice is yours." % tps + return ( + "Running very slowly at %.0f t/s — we recommend leaving this off, but the choice is yours." + ) % tps return "" diff --git a/client/ui/interaction_list.gd b/client/ui/interaction_list.gd index 94c099bc9..c8c714cf3 100644 --- a/client/ui/interaction_list.gd +++ b/client/ui/interaction_list.gd @@ -32,6 +32,8 @@ var _verb_labels: Array[Label] = [] # #537: D-033 relationship color — cached per target, drawn as left-edge accent bar var _relationship_color: Color = Constants.IMPLANT_TEXT_DIM +var _last_screen_pos: Vector2 = Vector2.ZERO + @onready var _vbox: VBoxContainer = $VBox @@ -40,9 +42,6 @@ func _ready() -> void: visible = false mouse_filter = Control.MOUSE_FILTER_IGNORE - -var _last_screen_pos: Vector2 = Vector2.ZERO - func _process(_delta: float) -> void: if _showing: _update_screen_position() diff --git a/client/ui/interaction_prompt.gd b/client/ui/interaction_prompt.gd index 2fd9152cc..6e827ca98 100644 --- a/client/ui/interaction_prompt.gd +++ b/client/ui/interaction_prompt.gd @@ -8,7 +8,8 @@ extends PanelContainer # v0.2: Will be replaced/extended with radial verb menu. # Public interface: get_interaction_target(), get_selected_verb() -@onready var prompt_label: Label = $MarginContainer/PromptLabel +const FADE_IN: float = 0.15 +const FADE_OUT: float = 0.15 var _is_showing: bool = false var _active_tween: Tween = null @@ -16,8 +17,7 @@ var _current_target_id: int = -1 # OQ-07 (#522): when false, prompt is suppressed (z-layer 6 insert overlay only) var _insert_active: bool = true -const FADE_IN: float = 0.15 -const FADE_OUT: float = 0.15 +@onready var prompt_label: Label = $MarginContainer/PromptLabel func _ready() -> void: modulate.a = 0.0 diff --git a/client/ui/inventory_grid.gd b/client/ui/inventory_grid.gd index 8820451f8..52be53dcb 100644 --- a/client/ui/inventory_grid.gd +++ b/client/ui/inventory_grid.gd @@ -94,7 +94,8 @@ func _draw() -> void: # 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: diff --git a/client/ui/journal_panel.gd b/client/ui/journal_panel.gd index 52255846b..bb1863b59 100644 --- a/client/ui/journal_panel.gd +++ b/client/ui/journal_panel.gd @@ -23,13 +23,14 @@ const FADE_OUT: float = 0.25 # Keys: knowledge_panel.confidence_{lower} and knowledge_panel.source_{lower} # Fallback: raw value if key not found (UIStrings returns the key itself). +var _visible_state: bool = false +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 - -var _visible_state: bool = false -var _last_rendered_tick: int = -1 +@onready var entries_container: VBoxContainer = \ + $PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/EntriesContainer func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE diff --git a/client/ui/loading_screen.gd b/client/ui/loading_screen.gd index c254e210b..059c34cfd 100644 --- a/client/ui/loading_screen.gd +++ b/client/ui/loading_screen.gd @@ -40,5 +40,5 @@ func show_loading() -> void: ## Hide the loading overlay. success=false is reserved for future failure-state UI. -func hide_loading(success: bool = true) -> void: +func hide_loading(_success: bool = true) -> void: visible = false diff --git a/client/ui/main_menu.gd b/client/ui/main_menu.gd index e50a83c14..4e406ccb9 100644 --- a/client/ui/main_menu.gd +++ b/client/ui/main_menu.gd @@ -16,6 +16,9 @@ const FONT_SIZE_TITLE := 36 const FONT_SIZE_SUBTITLE := 14 const FONT_SIZE_BTN := 15 +var _char_creation: Control = null +var _list_built: bool = false + @onready var _new_game_btn: Button = $VBox/NewGameBtn @onready var _continue_btn: Button = $VBox/ContinueBtn @onready var _load_game_btn: Button = $VBox/LoadGameBtn @@ -24,8 +27,6 @@ const FONT_SIZE_BTN := 15 @onready var _saves_list: VBoxContainer = $LoadGamePanel/VBox/SavesScroll/SavesList @onready var _load_back_btn: Button = $LoadGamePanel/VBox/BackBtn -var _char_creation: Control = null - func _ready() -> void: _new_game_btn.pressed.connect(_on_new_game) @@ -93,9 +94,6 @@ func _on_continue() -> void: get_tree().change_scene_to_file(GAME_SCENE) -var _list_built: bool = false - - func _on_load_game_browse() -> void: if not _list_built: _build_saves_list() diff --git a/client/ui/monologue_display.gd b/client/ui/monologue_display.gd index 37a52844d..9c32d00f4 100644 --- a/client/ui/monologue_display.gd +++ b/client/ui/monologue_display.gd @@ -38,8 +38,6 @@ const _FALLBACK_URGENT: Color = Color("#e0e8f8") const _NOTIFICATION_COLOR: Color = Color("#8890a0") # #554: neutral system notification const _NOTIFICATION_DURATION: float = 2.5 -@onready var _vbox: VBoxContainer = $VBoxContainer - # Visible slot: {node: Control, expire_timer: float, priority: int, tween: Tween} var _visible: Array[Dictionary] = [] # Queue entry: {text, duration, priority, is_urgent, lattice_profile} @@ -47,6 +45,8 @@ var _queue: Array[Dictionary] = [] # Msec timestamp when the next fade-in may begin (stagger enforcement) var _next_fade_in_msec: float = 0.0 +@onready var _vbox: VBoxContainer = $VBoxContainer + func _ready() -> void: pass @@ -85,7 +85,10 @@ func show_notification(text: String) -> void: if _visible.size() < MAX_VISIBLE and now >= _next_fade_in_msec: _show_notification_line(text) else: - var entry := {text = text, duration = _NOTIFICATION_DURATION, priority = 1, is_urgent = false, lattice_profile = "", is_notification = true} + var entry := { + 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) @@ -173,13 +176,19 @@ func _retire_slot(slot: Dictionary) -> 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.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} + _queue[lowest] = { + 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: incoming line is strictly lower priority — silently drop; no sort needed diff --git a/client/ui/news_ticker.gd b/client/ui/news_ticker.gd index 32ca2d6b8..779478afa 100644 --- a/client/ui/news_ticker.gd +++ b/client/ui/news_ticker.gd @@ -10,12 +10,12 @@ const FONT_SIZE := 13 const SCROLL_SPEED := 60.0 # pixels per second const BAR_HEIGHT := 28 -@onready var _label: Label = $TickerLabel - var _text: String = "" var _scroll_x: float = 0.0 var _content_width: float = 0.0 +@onready var _label: Label = $TickerLabel + func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE diff --git a/client/ui/settings_dialog.gd b/client/ui/settings_dialog.gd index f6cbdaeff..af4e3a95d 100644 --- a/client/ui/settings_dialog.gd +++ b/client/ui/settings_dialog.gd @@ -6,6 +6,10 @@ extends Control ## Volumes persist via AudioManager._save_prefs() on each slider change. ## AI Dialogue toggle persists via ConfigFile (client-local) + ChangeSettings IPC (server SQLite). +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 + 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) @@ -42,10 +46,6 @@ var _ai_check_node: CheckButton = null var _ai_inference_suspended: bool = false # D-138 §8 Layer 3 battery auto-suspend state var _ai_battery_warning_label: Label = null # shown when on battery; toggle stays enabled -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 - func _ready() -> void: visible = false