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
+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)")