fix(client): resolve all gdlint warnings — zero warnings policy

Fix 354 gdlint warnings across 65 files: 194 class-definitions-order
(reorder declarations), 138 max-line-length (split long lines),
22 code issues (unused args, no-else-return, naming). Update .gdlintrc
to exclude addons/ and raise max-public-methods for test files.
No logic changes — declaration order, whitespace, and naming only.

Ticket: #783

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-05 09:59:56 +02:00
co-authored by Claude Opus 4.6
parent 260eefcdd1
commit 4eb54a6f7a
60 changed files with 432 additions and 377 deletions
+2 -1
View File
@@ -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
+18 -19
View File
@@ -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.
+10 -11
View File
@@ -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))
+8 -8
View File
@@ -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
+12 -12
View File
@@ -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 (0100). -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 (0100). -1 if not available or not on battery.
var battery_percent: int = -1
# -- Memory -------------------------------------------------------------------
+42 -39
View File
@@ -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:
@@ -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)
+23 -23
View File
@@ -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]
+2 -1
View File
@@ -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()
+9 -7
View File
@@ -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".
+18 -19
View File
@@ -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)
+11 -10
View File
@@ -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()):
+11 -11
View File
@@ -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
+4 -4
View File
@@ -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.
+3 -2
View File
@@ -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
+2 -2
View File
@@ -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,
+2 -2
View File
@@ -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)")
+4 -4
View File
@@ -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()
+16 -15
View File
@@ -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
+6 -5
View File
@@ -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)
+2 -2
View File
@@ -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)
# ==============================================================================
+2 -2
View File
@@ -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)
+2 -3
View File
@@ -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.
+7 -6
View File
@@ -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)
@@ -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
+7 -3
View File
@@ -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"]
-1
View File
@@ -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) -------------------------------------------
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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()
+2 -2
View File
@@ -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()
+2 -2
View File
@@ -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)
+3 -2
View File
@@ -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)
+4 -2
View File
@@ -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)
+1 -1
View File
@@ -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"},
+1 -1
View File
@@ -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": [],
+2 -1
View File
@@ -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)
@@ -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")
+5 -3
View File
@@ -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)
+11 -9
View File
@@ -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)
+4 -2
View File
@@ -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)
+19 -17
View File
@@ -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)
+2 -1
View File
@@ -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]
+1 -1
View File
@@ -31,7 +31,7 @@ func _init():
_run.call_deferred()
func _run():
func _run(): # gdlint:disable=max-returns
# Parse CLI args (after -- separator)
_parse_args()
-1
View File
@@ -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).
+39 -32
View File
@@ -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
# =============================================================================
+2 -2
View File
@@ -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()
+10 -5
View File
@@ -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)
+29 -29
View File
@@ -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).
+3 -3
View File
@@ -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
+2 -1
View File
@@ -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:
+16 -10
View File
@@ -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 ""
+2 -3
View File
@@ -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()
+3 -3
View File
@@ -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
+2 -1
View File
@@ -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:
+5 -4
View File
@@ -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
+1 -1
View File
@@ -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
+3 -5
View File
@@ -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()
+14 -5
View File
@@ -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
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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