style(client): gdformat all 52 GDScript files — zero format warnings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -46,11 +46,11 @@ var _captured_screenshot: Image = null
|
||||
# Memory ceiling: 60 snapshot JSON strings (each ~2-8KB depending on entity count)
|
||||
# + 60 input arrays (negligible). Worst case ~480KB resident. Acceptable for a
|
||||
# debug tool that is always active during Gauntlet play.
|
||||
var _input_ring: Array = [] # Array[Array] — each slot: Array of {tick, action} dicts
|
||||
var _input_head: int = 0 # Next write index (0..RING_SIZE-1)
|
||||
var _input_count: int = 0 # Filled slot count (0..RING_SIZE)
|
||||
var _input_ring: Array = [] # Array[Array] — each slot: Array of {tick, action} dicts
|
||||
var _input_head: int = 0 # Next write index (0..RING_SIZE-1)
|
||||
var _input_count: int = 0 # Filled slot count (0..RING_SIZE)
|
||||
|
||||
var _snapshot_ring: Array = [] # Array[String] — each slot: JSON-serialized ObserverSnapshot
|
||||
var _snapshot_ring: Array = [] # Array[String] — each slot: JSON-serialized ObserverSnapshot
|
||||
var _snapshot_head: int = 0
|
||||
var _snapshot_count: int = 0
|
||||
|
||||
@@ -74,6 +74,7 @@ func _ready() -> void:
|
||||
# Public API for main.gd: record one tick's data
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
## Record one tick. Called from main.gd on every server tick (snapshot arrival).
|
||||
## - tick: current server tick number
|
||||
## - snapshot_json: JSON.stringify(GameState.current_snapshot)
|
||||
@@ -180,18 +181,23 @@ func _to_replay_format(input: Dictionary, tick: int) -> Dictionary:
|
||||
# Test-accessible accessors (ring buffer introspection)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
func _get_buffer_capacity() -> int:
|
||||
return RING_SIZE
|
||||
|
||||
|
||||
func _get_snapshot_buffer_capacity() -> int:
|
||||
return RING_SIZE
|
||||
|
||||
|
||||
func _get_input_buffer() -> Array:
|
||||
return _input_ring
|
||||
|
||||
|
||||
func _get_filled_input_count() -> int:
|
||||
return _input_count
|
||||
|
||||
|
||||
func _get_filled_snapshot_count() -> int:
|
||||
return _snapshot_count
|
||||
|
||||
@@ -200,6 +206,7 @@ func _get_filled_snapshot_count() -> int:
|
||||
# UI / capture flow
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
func start_capture() -> void:
|
||||
if _active:
|
||||
return
|
||||
@@ -209,10 +216,15 @@ func start_capture() -> void:
|
||||
visible = true
|
||||
|
||||
# Pause the simulation
|
||||
SimBridge.send_input({
|
||||
"action": InputMapper.Action.PAUSE,
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
})
|
||||
(
|
||||
SimBridge
|
||||
. send_input(
|
||||
{
|
||||
"action": InputMapper.Action.PAUSE,
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Create the LineEdit dynamically
|
||||
_line_edit = LineEdit.new()
|
||||
@@ -254,10 +266,15 @@ func _close() -> void:
|
||||
_captured_screenshot = null
|
||||
|
||||
# Unpause the simulation
|
||||
SimBridge.send_input({
|
||||
"action": InputMapper.Action.UNPAUSE,
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
})
|
||||
(
|
||||
SimBridge
|
||||
. send_input(
|
||||
{
|
||||
"action": InputMapper.Action.UNPAUSE,
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _save_report(description: String) -> void:
|
||||
@@ -301,7 +318,9 @@ func _save_report(description: String) -> void:
|
||||
if desc_file:
|
||||
desc_file.store_string("Description: %s\n" % description)
|
||||
desc_file.store_string("Tick: %d\n" % tick)
|
||||
desc_file.store_string("Room: %s\n" % str(GameState.room_id if GameState.room_id else "none"))
|
||||
desc_file.store_string(
|
||||
"Room: %s\n" % str(GameState.room_id if GameState.room_id else "none")
|
||||
)
|
||||
desc_file.store_string("Stance: %s\n" % GameState.player_stance)
|
||||
desc_file.store_string("Facing: %s\n" % GameState.player_facing)
|
||||
desc_file.store_string("Position: %s\n" % str(GameState.player_position))
|
||||
@@ -343,8 +362,11 @@ func _save_report(description: String) -> void:
|
||||
var seed_val: Variant = _get_current_seed()
|
||||
seed_file.store_string(str(seed_val) + "\n")
|
||||
if seed_val == "unavailable":
|
||||
seed_file.store_string(
|
||||
"# Server protocol change required: add 'rng_seed' (u64) field to ObserverSnapshot.\n"
|
||||
(
|
||||
seed_file
|
||||
. store_string(
|
||||
"# Server protocol change required: add 'rng_seed' (u64) field to ObserverSnapshot.\n"
|
||||
)
|
||||
)
|
||||
seed_file.close()
|
||||
files_saved += 1
|
||||
@@ -360,8 +382,12 @@ func _save_report(description: String) -> void:
|
||||
else:
|
||||
push_error("BugReport: failed to write %s (error %d)" % [screenshot_path, img_err])
|
||||
|
||||
print("BugReport: saved %d/7 files to %s (ring: %d ticks)" % [
|
||||
files_saved, base_path, _input_count])
|
||||
print(
|
||||
(
|
||||
"BugReport: saved %d/7 files to %s (ring: %d ticks)"
|
||||
% [files_saved, base_path, _input_count]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
## Simplified client-side text render of the current snapshot.
|
||||
@@ -369,14 +395,24 @@ func _save_report(description: String) -> void:
|
||||
func _render_snapshot_text() -> String:
|
||||
var lines: PackedStringArray = []
|
||||
lines.append("=== Snapshot t%d ===" % GameState.current_tick)
|
||||
lines.append("Player: %s facing %s (%s)" % [
|
||||
str(GameState.player_position), GameState.player_facing, GameState.player_stance])
|
||||
lines.append(
|
||||
(
|
||||
"Player: %s facing %s (%s)"
|
||||
% [str(GameState.player_position), GameState.player_facing, GameState.player_stance]
|
||||
)
|
||||
)
|
||||
|
||||
if GameState.game_time.size() > 0:
|
||||
lines.append("Time: day %s, %s, %s" % [
|
||||
str(GameState.game_time.get("day", "?")),
|
||||
str(GameState.game_time.get("day_phase", "?")),
|
||||
str(GameState.game_time.get("tick_rate", "?"))])
|
||||
lines.append(
|
||||
(
|
||||
"Time: day %s, %s, %s"
|
||||
% [
|
||||
str(GameState.game_time.get("day", "?")),
|
||||
str(GameState.game_time.get("day_phase", "?")),
|
||||
str(GameState.game_time.get("tick_rate", "?"))
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
lines.append("Entities (%d):" % GameState.visible_entities.size())
|
||||
@@ -387,12 +423,18 @@ func _render_snapshot_text() -> String:
|
||||
elif entity.has("kind") and entity.kind is String:
|
||||
kind_str = entity.kind
|
||||
var vis: String = entity.get("visibility", "?")
|
||||
lines.append(" #%s %s at (%s, %s) [%s]" % [
|
||||
str(entity.get("entity_id", "?")),
|
||||
kind_str,
|
||||
str(entity.get("x", "?")),
|
||||
str(entity.get("y", "?")),
|
||||
vis])
|
||||
lines.append(
|
||||
(
|
||||
" #%s %s at (%s, %s) [%s]"
|
||||
% [
|
||||
str(entity.get("entity_id", "?")),
|
||||
kind_str,
|
||||
str(entity.get("x", "?")),
|
||||
str(entity.get("y", "?")),
|
||||
vis
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
lines.append("Visible tiles: %d" % GameState.visible_tiles.size())
|
||||
@@ -400,9 +442,15 @@ func _render_snapshot_text() -> String:
|
||||
if GameState.current_monologue != null:
|
||||
lines.append("Monologue: %s" % str(GameState.current_monologue.get("text", "")))
|
||||
if GameState.current_dialogue != null:
|
||||
lines.append("Dialogue: %s says '%s'" % [
|
||||
str(GameState.current_dialogue.get("npc_name", "?")),
|
||||
str(GameState.current_dialogue.get("speech", ""))])
|
||||
lines.append(
|
||||
(
|
||||
"Dialogue: %s says '%s'"
|
||||
% [
|
||||
str(GameState.current_dialogue.get("npc_name", "?")),
|
||||
str(GameState.current_dialogue.get("speech", ""))
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -416,8 +464,7 @@ func _draw() -> void:
|
||||
|
||||
# Center box
|
||||
var box_pos := Vector2(
|
||||
(viewport_size.x - BOX_WIDTH) / 2.0,
|
||||
(viewport_size.y - BOX_HEIGHT) / 2.0
|
||||
(viewport_size.x - BOX_WIDTH) / 2.0, (viewport_size.y - BOX_HEIGHT) / 2.0
|
||||
)
|
||||
var box_rect := Rect2(box_pos, Vector2(BOX_WIDTH, BOX_HEIGHT))
|
||||
draw_rect(box_rect, Color(0.08, 0.08, 0.12, 0.95))
|
||||
@@ -425,15 +472,21 @@ func _draw() -> void:
|
||||
|
||||
# Title
|
||||
var font := ThemeDB.fallback_font
|
||||
draw_string(font,
|
||||
draw_string(
|
||||
font,
|
||||
box_pos + Vector2(PADDING, 24),
|
||||
"WRONG — Describe the issue (Enter to save, Esc to cancel):",
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, LABEL_FONT_SIZE, TEXT_COLOR)
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
LABEL_FONT_SIZE,
|
||||
TEXT_COLOR
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
func is_active() -> bool:
|
||||
return _active
|
||||
|
||||
+235
-94
@@ -16,10 +16,10 @@ signal creation_cancelled
|
||||
const BG_COLOR := Color(0.03, 0.03, 0.06, 1.0)
|
||||
const PANEL_BG := Color(0.05, 0.05, 0.08, 1.0)
|
||||
const BORDER_COLOR := Color(0.12, 0.15, 0.20, 1.0)
|
||||
const TEXT_COLOR := Color(0.784, 0.816, 0.878, 1.0) # INSERT_COLOR_TEXT
|
||||
const TEXT_COLOR := Color(0.784, 0.816, 0.878, 1.0) # INSERT_COLOR_TEXT
|
||||
const TEXT_DIM := Color(0.53, 0.56, 0.63, 1.0)
|
||||
const HIGHLIGHT_COLOR := Color(0.906, 0.773, 0.278, 1.0) # INSERT_COLOR_HOVER
|
||||
const ACTIVE_COLOR := Color(0.42, 0.79, 0.65, 1.0) # INSERT_COLOR_ACTIVE
|
||||
const ACTIVE_COLOR := Color(0.42, 0.79, 0.65, 1.0) # INSERT_COLOR_ACTIVE
|
||||
const ITEM_NORMAL_BG := Color(0.07, 0.07, 0.10, 1.0)
|
||||
const ITEM_SELECTED_BG := Color(0.10, 0.13, 0.18, 1.0)
|
||||
const ITEM_SELECTED_BORDER := Color(0.906, 0.773, 0.278, 1.0) # INSERT_COLOR_HOVER — selection border
|
||||
@@ -27,7 +27,7 @@ const ITEM_SELECTED_BORDER := Color(0.906, 0.773, 0.278, 1.0) # INSERT_COLOR_HO
|
||||
# --- Camera presets (D-158) ---
|
||||
# Pitch is degrees below horizontal: 5=near eye-level, 30=steeper, 80=near top-down.
|
||||
# Negative values = tilt downward (camera looks toward ground).
|
||||
const CAM_PITCHES: Array[float] = [-5.0, -30.0, -80.0] # frontal, dramatic, overhead
|
||||
const CAM_PITCHES: Array[float] = [-5.0, -30.0, -80.0] # frontal, dramatic, overhead
|
||||
const CAM_PITCH_NAMES := ["Frontal", "Dramatic", "Overhead"]
|
||||
const CAM_DISTANCE: float = 3.8
|
||||
const CAM_TARGET_HEIGHT: float = 0.85
|
||||
@@ -79,35 +79,111 @@ const CLOTHING_SLOT_LABELS := ["Torso", "Legs", "Feet", "Hands"]
|
||||
|
||||
# --- Accessory slots (no held_l/held_r — play-time decisions per wireframe spec) ---
|
||||
const ACCESSORY_SLOTS := [
|
||||
"hat", "goggles", "mask", "backpack", "belt",
|
||||
"wrist_l", "wrist_r", "earring_l", "earring_r", "necklace",
|
||||
"hat",
|
||||
"goggles",
|
||||
"mask",
|
||||
"backpack",
|
||||
"belt",
|
||||
"wrist_l",
|
||||
"wrist_r",
|
||||
"earring_l",
|
||||
"earring_r",
|
||||
"necklace",
|
||||
]
|
||||
const ACCESSORY_SLOT_LABELS := [
|
||||
"Hat", "Goggles", "Mask", "Backpack", "Belt",
|
||||
"Wrist L", "Wrist R", "Earring L", "Earring R", "Necklace",
|
||||
"Hat",
|
||||
"Goggles",
|
||||
"Mask",
|
||||
"Backpack",
|
||||
"Belt",
|
||||
"Wrist L",
|
||||
"Wrist R",
|
||||
"Earring L",
|
||||
"Earring R",
|
||||
"Necklace",
|
||||
]
|
||||
|
||||
# --- D-165 palette (5 chromatic rows + 1 neutral row, 9 cols each) ---
|
||||
# Hardcoded hex per D-165 spec (HSL L=18→76% per chromatic row, S fixed per row).
|
||||
const PALETTE_COLORS: Array[Array] = [
|
||||
# Row 1: Reds H=0° S=35%
|
||||
["#3d1d1d", "#522727", "#673131", "#7f3d3d", "#974848", "#af5959", "#bc7575", "#c99090", "#d7acac"],
|
||||
[
|
||||
"#3d1d1d",
|
||||
"#522727",
|
||||
"#673131",
|
||||
"#7f3d3d",
|
||||
"#974848",
|
||||
"#af5959",
|
||||
"#bc7575",
|
||||
"#c99090",
|
||||
"#d7acac"
|
||||
],
|
||||
# Row 2: Greens H=150° S=28%
|
||||
["#213a2d", "#2c4e3d", "#37614c", "#43785e", "#508f70", "#62a684", "#7cb599", "#96c4ad", "#b0d2c1"],
|
||||
[
|
||||
"#213a2d",
|
||||
"#2c4e3d",
|
||||
"#37614c",
|
||||
"#43785e",
|
||||
"#508f70",
|
||||
"#62a684",
|
||||
"#7cb599",
|
||||
"#96c4ad",
|
||||
"#b0d2c1"
|
||||
],
|
||||
# Row 3: Blues H=215° S=35%
|
||||
["#1d2b3d", "#273952", "#314867", "#3d587f", "#486997", "#597daf", "#7593bc", "#90a8c9", "#acbed7"],
|
||||
[
|
||||
"#1d2b3d",
|
||||
"#273952",
|
||||
"#314867",
|
||||
"#3d587f",
|
||||
"#486997",
|
||||
"#597daf",
|
||||
"#7593bc",
|
||||
"#90a8c9",
|
||||
"#acbed7"
|
||||
],
|
||||
# Row 4: Purples H=275° S=28%
|
||||
["#30213a", "#402c4e", "#503761", "#624378", "#75508f", "#8a62a6", "#9d7cb5", "#b196c4", "#c4b0d2"],
|
||||
[
|
||||
"#30213a",
|
||||
"#402c4e",
|
||||
"#503761",
|
||||
"#624378",
|
||||
"#75508f",
|
||||
"#8a62a6",
|
||||
"#9d7cb5",
|
||||
"#b196c4",
|
||||
"#c4b0d2"
|
||||
],
|
||||
# Row 5: Browns H=28° S=35%
|
||||
["#3d2c1d", "#523b27", "#674a31", "#7f5c3d", "#976d48", "#af8159", "#bc9675", "#c9ab90", "#d7c0ac"],
|
||||
[
|
||||
"#3d2c1d",
|
||||
"#523b27",
|
||||
"#674a31",
|
||||
"#7f5c3d",
|
||||
"#976d48",
|
||||
"#af8159",
|
||||
"#bc9675",
|
||||
"#c9ab90",
|
||||
"#d7c0ac"
|
||||
],
|
||||
# Row 6: Neutral grays
|
||||
["#000000", "#1f1f1f", "#3f3f3f", "#5f5f5f", "#7f7f7f", "#9f9f9f", "#bfbfbf", "#dfdfdf", "#ffffff"],
|
||||
[
|
||||
"#000000",
|
||||
"#1f1f1f",
|
||||
"#3f3f3f",
|
||||
"#5f5f5f",
|
||||
"#7f7f7f",
|
||||
"#9f9f9f",
|
||||
"#bfbfbf",
|
||||
"#dfdfdf",
|
||||
"#ffffff"
|
||||
],
|
||||
]
|
||||
const PALETTE_COLS := 9
|
||||
const RECENT_SLOTS := 9
|
||||
|
||||
const CAM_ZOOM_MIN: float = 0.25 # closest zoom (face detail)
|
||||
const CAM_ZOOM_MAX: float = 3.0 # farthest zoom (crowd level)
|
||||
const CAM_ZOOM_MIN: float = 0.25 # closest zoom (face detail)
|
||||
const CAM_ZOOM_MAX: float = 3.0 # farthest zoom (crowd level)
|
||||
const CAM_ZOOM_STEP: float = 0.12
|
||||
|
||||
const MANIFEST_PATH := "res://assets/characters/manifest.json"
|
||||
@@ -118,7 +194,7 @@ const CARDINAL_NAMES: Array[String] = ["south", "east", "north", "west"]
|
||||
# --- Descriptor and preview state ---
|
||||
var _descriptor: CharacterVisualDescriptor
|
||||
var _char_visual: CharacterVisual = null
|
||||
var _facing_idx: int = 0 # index into CARDINAL_DIRS (0 = south, default face-forward)
|
||||
var _facing_idx: int = 0 # index into CARDINAL_DIRS (0 = south, default face-forward)
|
||||
var _cam_pitch_idx: int = 0 # 0=frontal(-5°), 1=dramatic(-30°), 2=overhead(-80°)
|
||||
var _cam_zoom: float = 1.0 # 1.0 = default distance, <1.0 = zoomed in
|
||||
var _cam_zoom_offset: Vector3 = Vector3.ZERO # camera offset toward cursor when zoomed
|
||||
@@ -128,42 +204,42 @@ var _active_clothing_slot: String = "torso"
|
||||
var _active_accessory_slot: String = "hat"
|
||||
|
||||
# --- Body/skin tone buttons shared across Body + Head tabs ---
|
||||
var _body_type_btns: Dictionary = {} # BodyType int -> Button
|
||||
var _body_type_btns: Dictionary = {} # BodyType int -> Button
|
||||
var _body_skin_btns: Array[Button] = [] # 9 buttons in Body tab skin dock
|
||||
var _head_skin_btns: Array[Button] = [] # 9 buttons in Head tab skin dock
|
||||
|
||||
# --- Head / hair buttons ---
|
||||
var _head_item_btns: Dictionary = {} # head_id -> Button
|
||||
var _hair_item_btns: Dictionary = {} # hair_id -> Button
|
||||
var _head_item_btns: Dictionary = {} # head_id -> Button
|
||||
var _hair_item_btns: Dictionary = {} # hair_id -> Button
|
||||
var _facial_hair_btns: Array[Button] = []
|
||||
var _eyebrow_btns: Array[Button] = []
|
||||
|
||||
# --- Clothing/accessory grid content per slot ---
|
||||
var _clothing_grids: Dictionary = {} # slot -> GridContainer
|
||||
var _clothing_grids: Dictionary = {} # slot -> GridContainer
|
||||
var _clothing_slot_btns: Array[Button] = []
|
||||
var _clothing_item_btns: Dictionary = {} # slot -> Dictionary {item_id -> Button}
|
||||
var _accessory_grids: Dictionary = {} # slot -> GridContainer
|
||||
var _clothing_item_btns: Dictionary = {} # slot -> Dictionary {item_id -> Button}
|
||||
var _accessory_grids: Dictionary = {} # slot -> GridContainer
|
||||
var _accessory_slot_btns: Array[Button] = []
|
||||
var _accessory_item_btns: Dictionary = {} # slot -> Dictionary {item_id -> Button}
|
||||
var _accessory_item_btns: Dictionary = {} # slot -> Dictionary {item_id -> Button}
|
||||
|
||||
# --- Color dock swatch references ---
|
||||
var _hair_primary_swatch: Button = null
|
||||
var _hair_highlight_swatch: Button = null
|
||||
var _eyebrow_tint_swatch: Button = null
|
||||
var _facial_hair_tint_swatch: Button = null
|
||||
var _clothing_primary_swatches: Dictionary = {} # slot -> Button
|
||||
var _clothing_secondary_swatches: Dictionary = {} # slot -> Button
|
||||
var _clothing_accent_swatches: Dictionary = {} # slot -> Button
|
||||
var _clothing_primary_swatches: Dictionary = {} # slot -> Button
|
||||
var _clothing_secondary_swatches: Dictionary = {} # slot -> Button
|
||||
var _clothing_accent_swatches: Dictionary = {} # slot -> Button
|
||||
var _accessory_primary_swatches: Dictionary = {} # slot -> Button
|
||||
var _accessory_secondary_swatches: Dictionary = {} # slot -> Button
|
||||
var _accessory_secondary_swatches: Dictionary = {} # slot -> Button
|
||||
|
||||
# --- Auto-derive flags (true = auto-derive from hair/clothing primary) ---
|
||||
# Note: hair highlight is always auto-derived (display-only swatch, no override).
|
||||
var _eyebrow_tint_auto: bool = true
|
||||
var _facial_hair_tint_auto: bool = true
|
||||
var _clothing_secondary_auto: Dictionary = {} # slot -> bool
|
||||
var _clothing_accent_auto: Dictionary = {} # slot -> bool
|
||||
var _accessory_secondary_auto: Dictionary = {} # slot -> bool
|
||||
var _clothing_secondary_auto: Dictionary = {} # slot -> bool
|
||||
var _clothing_accent_auto: Dictionary = {} # slot -> bool
|
||||
var _accessory_secondary_auto: Dictionary = {} # slot -> bool
|
||||
|
||||
# --- Color picker modal state ---
|
||||
var _modal_callback: Callable
|
||||
@@ -171,7 +247,7 @@ var _modal_original_color: Color = Color.WHITE
|
||||
var _modal_preview_swatch: Button = null # the swatch button being edited
|
||||
var _modal_hex_input: LineEdit = null
|
||||
var _modal_swatch_btns: Array[Button] = [] # 54 palette swatches
|
||||
var _recent_colors: Array[Color] = [] # persisted recent custom colors
|
||||
var _recent_colors: Array[Color] = [] # persisted recent custom colors
|
||||
var _modal_recent_btns: Array[Button] = []
|
||||
|
||||
# --- Dock container references (set during tab build; avoid fragile child-index traversal) ---
|
||||
@@ -207,25 +283,31 @@ var _tab_container: TabContainer = null
|
||||
|
||||
# --- @onready references to .tscn nodes ---
|
||||
@onready var _viewport: SubViewport = $Layout/PreviewPanel/SubViewportContainer/SubViewport
|
||||
@onready var _char_anchor: Node3D = $Layout/PreviewPanel/SubViewportContainer/SubViewport/CharacterVisualAnchor
|
||||
@onready var _preview_camera: Camera3D = $Layout/PreviewPanel/SubViewportContainer/SubViewport/PreviewCamera
|
||||
@onready var _rotate_left_btn: Button = $Layout/PreviewPanel/PreviewOverlay/RotateButtons/RotateLeftBtn
|
||||
@onready var _rotate_right_btn: Button = $Layout/PreviewPanel/PreviewOverlay/RotateButtons/RotateRightBtn
|
||||
@onready
|
||||
var _char_anchor: Node3D = $Layout/PreviewPanel/SubViewportContainer/SubViewport/CharacterVisualAnchor
|
||||
@onready
|
||||
var _preview_camera: Camera3D = $Layout/PreviewPanel/SubViewportContainer/SubViewport/PreviewCamera
|
||||
@onready
|
||||
var _rotate_left_btn: Button = $Layout/PreviewPanel/PreviewOverlay/RotateButtons/RotateLeftBtn
|
||||
@onready
|
||||
var _rotate_right_btn: Button = $Layout/PreviewPanel/PreviewOverlay/RotateButtons/RotateRightBtn
|
||||
@onready var _cam_angle_btn: Button = $Layout/PreviewPanel/PreviewOverlay/CamAngleBtn
|
||||
@onready var _footer_back: Button = $Footer/BackBtn
|
||||
@onready var _footer_randomize: Button = $Footer/RandomizeBtn
|
||||
@onready var _footer_start: Button = $Footer/StartBtn
|
||||
@onready var _modal_root: Control = $ColorPickerModal
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Lifecycle
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _load_manifest() -> void:
|
||||
var file := FileAccess.open(MANIFEST_PATH, FileAccess.READ)
|
||||
if file == null:
|
||||
push_warning("CharacterCreation: manifest not found at %s — using empty defaults" % MANIFEST_PATH)
|
||||
push_warning(
|
||||
"CharacterCreation: manifest not found at %s — using empty defaults" % MANIFEST_PATH
|
||||
)
|
||||
_manifest = {}
|
||||
return
|
||||
var parsed: Variant = JSON.parse_string(file.get_as_text())
|
||||
@@ -277,12 +359,16 @@ func _ready() -> void:
|
||||
if has_hair:
|
||||
tab_builders.append({"name": "Hair", "build": _build_hair_tab, "always": false})
|
||||
var clothing_data: Variant = _manifest.get("clothing", {})
|
||||
var has_clothing: bool = clothing_data is Dictionary and not (clothing_data as Dictionary).is_empty()
|
||||
var has_clothing: bool = (
|
||||
clothing_data is Dictionary and not (clothing_data as Dictionary).is_empty()
|
||||
)
|
||||
if has_clothing:
|
||||
tab_builders.append({"name": "Clothing", "build": _build_clothing_tab, "always": false})
|
||||
var has_accessories := not _manifest_array("accessories").is_empty()
|
||||
if has_accessories:
|
||||
tab_builders.append({"name": "Accessories", "build": _build_accessories_tab, "always": false})
|
||||
tab_builders.append(
|
||||
{"name": "Accessories", "build": _build_accessories_tab, "always": false}
|
||||
)
|
||||
tab_builders.append({"name": "Debug", "build": _build_debug_tab, "always": true})
|
||||
|
||||
for tb in tab_builders:
|
||||
@@ -337,6 +423,7 @@ func _ready() -> void:
|
||||
# Camera management (D-158)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _update_camera_angle() -> void:
|
||||
var pitch_deg := CAM_PITCHES[_cam_pitch_idx]
|
||||
var pitch_rad := deg_to_rad(-pitch_deg) # negative = looking downward
|
||||
@@ -359,8 +446,10 @@ func _cam_zoom_toward_cursor(_screen_pos: Vector2, zoom_delta: float) -> void:
|
||||
if _char_visual and _char_visual._skeleton:
|
||||
var head_idx := _char_visual._skeleton.find_bone("Head")
|
||||
if head_idx >= 0:
|
||||
var head_pos := _char_visual._skeleton.global_transform \
|
||||
* _char_visual._skeleton.get_bone_global_pose(head_idx).origin
|
||||
var head_pos := (
|
||||
_char_visual._skeleton.global_transform
|
||||
* _char_visual._skeleton.get_bone_global_pose(head_idx).origin
|
||||
)
|
||||
# Blend from body center toward head as zoom increases
|
||||
var blend := 1.0 - _cam_zoom # 0 at default, 0.75 at max zoom
|
||||
_cam_zoom_offset.y = (head_pos.y - CAM_TARGET_HEIGHT) * blend
|
||||
@@ -384,6 +473,7 @@ func _on_cam_angle_toggle() -> void:
|
||||
# Rotation (D-155: cardinal only, Q=CCW, E=CW)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _on_rotate_left() -> void:
|
||||
_facing_idx = (_facing_idx + 1) % CARDINAL_DIRS.size()
|
||||
_char_visual.set_facing(CARDINAL_DIRS[_facing_idx])
|
||||
@@ -398,6 +488,7 @@ func _on_rotate_right() -> void:
|
||||
# Preview refresh
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _refresh_preview() -> void:
|
||||
if _char_visual and is_instance_valid(_char_visual):
|
||||
_char_visual.load_descriptor(_descriptor)
|
||||
@@ -408,6 +499,7 @@ func _refresh_preview() -> void:
|
||||
# Tab: Body (Task #2)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _build_body_tab(tab: Control) -> void:
|
||||
var vbox := _make_tab_vbox(tab)
|
||||
|
||||
@@ -475,10 +567,13 @@ func _build_body_tab(tab: Control) -> void:
|
||||
var eye_row := HBoxContainer.new()
|
||||
eye_row.add_theme_constant_override("separation", 8)
|
||||
vbox.add_child(eye_row)
|
||||
var eye_swatch := _make_color_swatch(_descriptor.eye_color, "Iris",
|
||||
var eye_swatch := _make_color_swatch(
|
||||
_descriptor.eye_color,
|
||||
"Iris",
|
||||
func(c: Color) -> void:
|
||||
_descriptor.eye_color = c
|
||||
_refresh_preview())
|
||||
_refresh_preview()
|
||||
)
|
||||
eye_row.add_child(eye_swatch)
|
||||
|
||||
_update_body_type_btns()
|
||||
@@ -527,6 +622,7 @@ func _update_body_type_btns() -> void:
|
||||
# Tab: Head (Task #3)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _build_head_tab(tab: Control) -> void:
|
||||
var vbox := _make_tab_vbox(tab)
|
||||
|
||||
@@ -564,10 +660,13 @@ func _build_head_tab(tab: Control) -> void:
|
||||
var eye_row := HBoxContainer.new()
|
||||
eye_row.add_theme_constant_override("separation", 8)
|
||||
vbox.add_child(eye_row)
|
||||
var eye_swatch := _make_color_swatch(_descriptor.eye_color, "Iris",
|
||||
var eye_swatch := _make_color_swatch(
|
||||
_descriptor.eye_color,
|
||||
"Iris",
|
||||
func(c: Color) -> void:
|
||||
_descriptor.eye_color = c
|
||||
_refresh_preview())
|
||||
_refresh_preview()
|
||||
)
|
||||
eye_row.add_child(eye_swatch)
|
||||
|
||||
_update_head_btns()
|
||||
@@ -589,6 +688,7 @@ func _update_head_btns() -> void:
|
||||
# Tab: Hair (Task #4)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _build_hair_tab(tab: Control) -> void:
|
||||
var vbox := _make_tab_vbox(tab)
|
||||
|
||||
@@ -669,7 +769,9 @@ func _update_hair_btns() -> void:
|
||||
for hid: String in _hair_item_btns:
|
||||
_set_item_selected(_hair_item_btns[hid], hid == _descriptor.hair_id)
|
||||
for i in FACIAL_HAIR_OPTIONS.size():
|
||||
_set_item_selected(_facial_hair_btns[i], FACIAL_HAIR_OPTIONS[i] == _descriptor.facial_hair_id)
|
||||
_set_item_selected(
|
||||
_facial_hair_btns[i], FACIAL_HAIR_OPTIONS[i] == _descriptor.facial_hair_id
|
||||
)
|
||||
# Eyebrow buttons removed — eyebrows come from body segment only
|
||||
|
||||
|
||||
@@ -684,21 +786,25 @@ func _build_hair_color_dock() -> Control:
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
dock.add_child(row)
|
||||
|
||||
_hair_primary_swatch = _make_color_swatch(Color(0.55, 0.35, 0.20), "Primary",
|
||||
func(c): _on_hair_primary_changed(c))
|
||||
_hair_primary_swatch = _make_color_swatch(
|
||||
Color(0.55, 0.35, 0.20), "Primary", func(c): _on_hair_primary_changed(c)
|
||||
)
|
||||
row.add_child(_hair_primary_swatch)
|
||||
|
||||
# #719 (Option B): highlight is auto-derived from primary — display-only, not editable.
|
||||
_hair_highlight_swatch = _make_display_swatch(
|
||||
_derive_hair_highlight(_descriptor.hair_tint), "Highlight")
|
||||
_derive_hair_highlight(_descriptor.hair_tint), "Highlight"
|
||||
)
|
||||
row.add_child(_hair_highlight_swatch)
|
||||
|
||||
_eyebrow_tint_swatch = _make_color_swatch(_descriptor.hair_tint, "Brows ●",
|
||||
func(c): _on_eyebrow_tint_changed(c))
|
||||
_eyebrow_tint_swatch = _make_color_swatch(
|
||||
_descriptor.hair_tint, "Brows ●", func(c): _on_eyebrow_tint_changed(c)
|
||||
)
|
||||
row.add_child(_eyebrow_tint_swatch)
|
||||
|
||||
_facial_hair_tint_swatch = _make_color_swatch(_descriptor.hair_tint, "Facial ●",
|
||||
func(c): _on_facial_hair_tint_changed(c))
|
||||
_facial_hair_tint_swatch = _make_color_swatch(
|
||||
_descriptor.hair_tint, "Facial ●", func(c): _on_facial_hair_tint_changed(c)
|
||||
)
|
||||
row.add_child(_facial_hair_tint_swatch)
|
||||
|
||||
return dock
|
||||
@@ -743,6 +849,7 @@ func _on_facial_hair_tint_changed(color: Color) -> void:
|
||||
# Tab: Clothing (Task #5)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _build_clothing_tab(tab: Control) -> void:
|
||||
var vbox := _make_tab_vbox(tab)
|
||||
|
||||
@@ -851,23 +958,25 @@ func _rebuild_clothing_color_dock(container: Control) -> void:
|
||||
container.add_child(row)
|
||||
|
||||
var item_id: String = str(_descriptor.clothing_slots.get(_active_clothing_slot, ""))
|
||||
var tints: Array = _descriptor.clothing_tints.get(item_id, []) as Array if not item_id.is_empty() else [] as Array
|
||||
var tints: Array = (
|
||||
_descriptor.clothing_tints.get(item_id, []) as Array if not item_id.is_empty() else []
|
||||
as Array
|
||||
)
|
||||
var primary: Color = tints[0] as Color if tints.size() > 0 else Color(0.7, 0.65, 0.6)
|
||||
var secondary: Color = tints[1] as Color if tints.size() > 1 else _derive_secondary(primary)
|
||||
var accent: Color = tints[2] as Color if tints.size() > 2 else _derive_accent(primary)
|
||||
|
||||
var p_swatch := _make_color_swatch(primary, "Primary",
|
||||
func(c): _on_clothing_primary_changed(c))
|
||||
var p_swatch := _make_color_swatch(primary, "Primary", func(c): _on_clothing_primary_changed(c))
|
||||
row.add_child(p_swatch)
|
||||
_clothing_primary_swatches[_active_clothing_slot] = p_swatch
|
||||
|
||||
var s_swatch := _make_color_swatch(secondary, "Secondary ●",
|
||||
func(c): _on_clothing_secondary_changed(c))
|
||||
var s_swatch := _make_color_swatch(
|
||||
secondary, "Secondary ●", func(c): _on_clothing_secondary_changed(c)
|
||||
)
|
||||
row.add_child(s_swatch)
|
||||
_clothing_secondary_swatches[_active_clothing_slot] = s_swatch
|
||||
|
||||
var a_swatch := _make_color_swatch(accent, "Accent ●",
|
||||
func(c): _on_clothing_accent_changed(c))
|
||||
var a_swatch := _make_color_swatch(accent, "Accent ●", func(c): _on_clothing_accent_changed(c))
|
||||
row.add_child(a_swatch)
|
||||
_clothing_accent_swatches[_active_clothing_slot] = a_swatch
|
||||
|
||||
@@ -902,7 +1011,9 @@ func _on_clothing_primary_changed(color: Color) -> void:
|
||||
tints[0] = color
|
||||
_descriptor.clothing_tints[item_id] = tints
|
||||
if _clothing_secondary_auto.get(_active_clothing_slot, true):
|
||||
_set_swatch_color(_clothing_secondary_swatches.get(_active_clothing_slot), _derive_secondary(color))
|
||||
_set_swatch_color(
|
||||
_clothing_secondary_swatches.get(_active_clothing_slot), _derive_secondary(color)
|
||||
)
|
||||
_refresh_preview()
|
||||
|
||||
|
||||
@@ -953,6 +1064,7 @@ func _update_clothing_item_btns() -> void:
|
||||
# Tab: Accessories (Task #6)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _build_accessories_tab(tab: Control) -> void:
|
||||
var vbox := _make_tab_vbox(tab)
|
||||
|
||||
@@ -1032,17 +1144,22 @@ func _rebuild_accessory_color_dock(container: Control) -> void:
|
||||
container.add_child(row)
|
||||
|
||||
var item_id: String = str(_descriptor.accessory_slots.get(_active_accessory_slot, ""))
|
||||
var tints_arr: Array = _descriptor.accessory_tints.get(item_id, []) as Array if not item_id.is_empty() else [] as Array
|
||||
var tints_arr: Array = (
|
||||
_descriptor.accessory_tints.get(item_id, []) as Array if not item_id.is_empty() else []
|
||||
as Array
|
||||
)
|
||||
var primary: Color = tints_arr[0] as Color if not tints_arr.is_empty() else Color.WHITE
|
||||
var secondary: Color = _derive_secondary(primary)
|
||||
|
||||
var p_swatch := _make_color_swatch(primary, "Primary",
|
||||
func(c): _on_accessory_primary_changed(c))
|
||||
var p_swatch := _make_color_swatch(
|
||||
primary, "Primary", func(c): _on_accessory_primary_changed(c)
|
||||
)
|
||||
row.add_child(p_swatch)
|
||||
_accessory_primary_swatches[_active_accessory_slot] = p_swatch
|
||||
|
||||
var s_swatch := _make_color_swatch(secondary, "Secondary ●",
|
||||
func(c): _on_accessory_secondary_changed(c))
|
||||
var s_swatch := _make_color_swatch(
|
||||
secondary, "Secondary ●", func(c): _on_accessory_secondary_changed(c)
|
||||
)
|
||||
row.add_child(s_swatch)
|
||||
_accessory_secondary_swatches[_active_accessory_slot] = s_swatch
|
||||
|
||||
@@ -1103,6 +1220,7 @@ func _update_accessory_item_btns() -> void:
|
||||
# Screenshot & automated testing
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _schedule_screenshot() -> void:
|
||||
_screenshot_pending = true
|
||||
_screenshot_frame_count = 0
|
||||
@@ -1125,10 +1243,10 @@ func _take_screenshot(suffix: String = "") -> void:
|
||||
_char_visual.set_facing(CARDINAL_DIRS[_screenshot_cardinal_idx])
|
||||
suffix = dir_name
|
||||
|
||||
var filename := "charcreator_%s_%s.png" % [
|
||||
_descriptor.body_type_key(),
|
||||
suffix if not suffix.is_empty() else "default"
|
||||
]
|
||||
var filename := (
|
||||
"charcreator_%s_%s.png"
|
||||
% [_descriptor.body_type_key(), suffix if not suffix.is_empty() else "default"]
|
||||
)
|
||||
var path := SCREENSHOT_DIR + filename
|
||||
var img := get_viewport().get_texture().get_image()
|
||||
img.save_png(path)
|
||||
@@ -1212,6 +1330,7 @@ func _load_test_config() -> void:
|
||||
# Debug tab — segment visibility toggles
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _build_debug_tab(tab: Control) -> void:
|
||||
var vbox := _make_tab_vbox(tab)
|
||||
|
||||
@@ -1308,6 +1427,7 @@ func _on_debug_toggle_outlines(pressed: bool) -> void:
|
||||
# Color picker modal (Task #7) — D-165 palette
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _build_color_picker_modal() -> void:
|
||||
var modal := _modal_root
|
||||
|
||||
@@ -1496,6 +1616,7 @@ func _update_modal_recent_btns() -> void:
|
||||
# Input handling (Task #8 keyboard nav)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not visible:
|
||||
return
|
||||
@@ -1547,7 +1668,9 @@ func _input(event: InputEvent) -> void:
|
||||
KEY_TAB:
|
||||
var count := _tab_container.get_tab_count()
|
||||
if key.shift_pressed:
|
||||
_tab_container.current_tab = ((_tab_container.current_tab - 1) % count + count) % count
|
||||
_tab_container.current_tab = (
|
||||
((_tab_container.current_tab - 1) % count + count) % count
|
||||
)
|
||||
else:
|
||||
_tab_container.current_tab = (_tab_container.current_tab + 1) % count
|
||||
get_viewport().set_input_as_handled()
|
||||
@@ -1557,6 +1680,7 @@ func _input(event: InputEvent) -> void:
|
||||
# Game flow (Task #8)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _on_back() -> void:
|
||||
creation_cancelled.emit()
|
||||
|
||||
@@ -1569,6 +1693,7 @@ func _on_start() -> void:
|
||||
# Randomize
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _on_randomize() -> void:
|
||||
# Body type — pick from manifest only
|
||||
var available_types: Array = _manifest_array("body_types")
|
||||
@@ -1595,7 +1720,9 @@ func _on_randomize() -> void:
|
||||
_descriptor.facial_hair_id = ""
|
||||
|
||||
# Eye color — random natural tones
|
||||
_descriptor.eye_color = Color.from_hsv(randf() * 0.15 + 0.05, 0.3 + randf() * 0.5, 0.2 + randf() * 0.5)
|
||||
_descriptor.eye_color = Color.from_hsv(
|
||||
randf() * 0.15 + 0.05, 0.3 + randf() * 0.5, 0.2 + randf() * 0.5
|
||||
)
|
||||
|
||||
# Sync auto tints
|
||||
_eyebrow_tint_auto = true
|
||||
@@ -1615,6 +1742,7 @@ func _on_randomize() -> void:
|
||||
# Shared helpers — skin tone dock
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _build_skin_tone_dock() -> HBoxContainer:
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 4)
|
||||
@@ -1659,6 +1787,7 @@ func _update_skin_tone_btns() -> void:
|
||||
# Shared helpers — color swatches
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## Get the current Color displayed in a swatch button (VBoxContainer → ColorRect).
|
||||
func _get_swatch_color(btn: Button) -> Color:
|
||||
for child in btn.get_children():
|
||||
@@ -1698,9 +1827,7 @@ func _make_color_swatch(color: Color, label_text: String, callback: Callable) ->
|
||||
vbox.add_child(lbl)
|
||||
|
||||
# Use _get_swatch_color to read current color at click time (avoids stale closure).
|
||||
btn.pressed.connect(func():
|
||||
_open_color_picker(_get_swatch_color(btn), btn, callback)
|
||||
)
|
||||
btn.pressed.connect(func(): _open_color_picker(_get_swatch_color(btn), btn, callback))
|
||||
return btn
|
||||
|
||||
|
||||
@@ -1754,6 +1881,7 @@ func _set_swatch_color(btn: Button, color: Color) -> void:
|
||||
# Shared helpers — layout builders
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _make_tab_vbox(tab: Control) -> VBoxContainer:
|
||||
var margin := MarginContainer.new()
|
||||
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
@@ -1772,15 +1900,16 @@ func _make_search_bar(tab_idx: int) -> LineEdit:
|
||||
search.placeholder_text = UIStrings.get_text("character_creation.search_placeholder")
|
||||
search.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
search.add_theme_font_size_override("font_size", 12)
|
||||
search.text_changed.connect(func(q: String):
|
||||
_tab_search[tab_idx] = q
|
||||
# Tabs 3/4 filter the active slot's grid; others use _tab_grids by index
|
||||
if tab_idx == 3 and _clothing_grids.has(_active_clothing_slot):
|
||||
_apply_search_filter(_clothing_grids[_active_clothing_slot], q)
|
||||
elif tab_idx == 4 and _accessory_grids.has(_active_accessory_slot):
|
||||
_apply_search_filter(_accessory_grids[_active_accessory_slot], q)
|
||||
elif tab_idx < _tab_grids.size() and _tab_grids[tab_idx] != null:
|
||||
_apply_search_filter(_tab_grids[tab_idx], q)
|
||||
search.text_changed.connect(
|
||||
func(q: String):
|
||||
_tab_search[tab_idx] = q
|
||||
# Tabs 3/4 filter the active slot's grid; others use _tab_grids by index
|
||||
if tab_idx == 3 and _clothing_grids.has(_active_clothing_slot):
|
||||
_apply_search_filter(_clothing_grids[_active_clothing_slot], q)
|
||||
elif tab_idx == 4 and _accessory_grids.has(_active_accessory_slot):
|
||||
_apply_search_filter(_accessory_grids[_active_accessory_slot], q)
|
||||
elif tab_idx < _tab_grids.size() and _tab_grids[tab_idx] != null:
|
||||
_apply_search_filter(_tab_grids[tab_idx], q)
|
||||
)
|
||||
return search
|
||||
|
||||
@@ -1849,7 +1978,9 @@ func _apply_search_filter(grid: GridContainer, query: String) -> void:
|
||||
var lower_q := query.to_lower()
|
||||
for child in grid.get_children():
|
||||
if child is Button:
|
||||
child.visible = lower_q.is_empty() or (child as Button).text.to_lower().contains(lower_q)
|
||||
child.visible = (
|
||||
lower_q.is_empty() or (child as Button).text.to_lower().contains(lower_q)
|
||||
)
|
||||
|
||||
|
||||
func _get_clothing_ids_for_slot(slot: String) -> Array:
|
||||
@@ -1872,21 +2003,31 @@ func _get_accessory_ids_for_slot(slot: String) -> Array:
|
||||
# Accessory items from manifest, filtered by slot name prefix convention.
|
||||
var all_ids: Array = _manifest_array("accessories")
|
||||
match slot:
|
||||
"hat": return all_ids.filter(func(id: String) -> bool: return id.begins_with("hat"))
|
||||
"goggles": return all_ids.filter(func(id: String) -> bool: return id.begins_with("goggle"))
|
||||
"mask": return all_ids.filter(func(id: String) -> bool: return id.begins_with("mask"))
|
||||
"backpack": return all_ids.filter(func(id: String) -> bool: return id.begins_with("backpack"))
|
||||
"belt": return all_ids.filter(func(id: String) -> bool: return id.begins_with("belt"))
|
||||
"wrist_l", "wrist_r": return all_ids.filter(func(id: String) -> bool: return id.begins_with("wrist"))
|
||||
"earring_l", "earring_r": return all_ids.filter(func(id: String) -> bool: return id.begins_with("earring"))
|
||||
"necklace": return all_ids.filter(func(id: String) -> bool: return id.begins_with("necklace"))
|
||||
_: return all_ids # gdlint:ignore = max-returns
|
||||
"hat":
|
||||
return all_ids.filter(func(id: String) -> bool: return id.begins_with("hat"))
|
||||
"goggles":
|
||||
return all_ids.filter(func(id: String) -> bool: return id.begins_with("goggle"))
|
||||
"mask":
|
||||
return all_ids.filter(func(id: String) -> bool: return id.begins_with("mask"))
|
||||
"backpack":
|
||||
return all_ids.filter(func(id: String) -> bool: return id.begins_with("backpack"))
|
||||
"belt":
|
||||
return all_ids.filter(func(id: String) -> bool: return id.begins_with("belt"))
|
||||
"wrist_l", "wrist_r":
|
||||
return all_ids.filter(func(id: String) -> bool: return id.begins_with("wrist"))
|
||||
"earring_l", "earring_r":
|
||||
return all_ids.filter(func(id: String) -> bool: return id.begins_with("earring"))
|
||||
"necklace":
|
||||
return all_ids.filter(func(id: String) -> bool: return id.begins_with("necklace"))
|
||||
_:
|
||||
return all_ids # gdlint:ignore = max-returns
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Color derivation helpers
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _derive_hair_highlight(primary: Color) -> Color:
|
||||
return primary.lightened(0.3)
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ extends Control
|
||||
const ChecklistEvaluator = preload("res://scripts/checklist/checklist_evaluator.gd")
|
||||
|
||||
const BG_COLOR := Color(0.05, 0.05, 0.08, 0.45)
|
||||
const MET_COLOR := Color("#6bc9a6") # Friendly green — condition met
|
||||
const UNMET_COLOR := Color("#8890a0") # Dim grey — condition pending
|
||||
const HEADER_COLOR := Color("#c8d0e0") # Insert text color — header/summary
|
||||
const COMPLETE_COLOR := Color("#e8c547") # Amber — all conditions met
|
||||
const MET_COLOR := Color("#6bc9a6") # Friendly green — condition met
|
||||
const UNMET_COLOR := Color("#8890a0") # Dim grey — condition pending
|
||||
const HEADER_COLOR := Color("#c8d0e0") # Insert text color — header/summary
|
||||
const COMPLETE_COLOR := Color("#e8c547") # Amber — all conditions met
|
||||
const FONT_SIZE := 11
|
||||
const LINE_HEIGHT := 16
|
||||
const PADDING := Vector2(8, 6)
|
||||
@@ -79,13 +79,17 @@ func _draw() -> void:
|
||||
var box_height: float = PADDING.y * 2 + line_count * LINE_HEIGHT
|
||||
|
||||
# Calculate box width from longest line
|
||||
var max_width: float = font.get_string_size(header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x
|
||||
var max_width: float = (
|
||||
font.get_string_size(header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x
|
||||
)
|
||||
for r in results:
|
||||
var desc: String = r.get("description", r.get("id", ""))
|
||||
if desc.length() > MAX_DESC_CHARS:
|
||||
desc = desc.substr(0, MAX_DESC_CHARS - 1) + "..."
|
||||
var prefix: String = "[x] " if r.get("met", false) else "[ ] "
|
||||
var line_width: float = font.get_string_size(prefix + desc, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x
|
||||
var line_width: float = (
|
||||
font.get_string_size(prefix + desc, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x
|
||||
)
|
||||
if line_width > max_width:
|
||||
max_width = line_width
|
||||
|
||||
@@ -96,7 +100,15 @@ func _draw() -> void:
|
||||
|
||||
# Header
|
||||
var y: float = PADDING.y + FONT_SIZE
|
||||
draw_string(font, Vector2(PADDING.x, y), header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, header_color)
|
||||
draw_string(
|
||||
font,
|
||||
Vector2(PADDING.x, y),
|
||||
header_text,
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
FONT_SIZE,
|
||||
header_color
|
||||
)
|
||||
|
||||
# Condition lines
|
||||
for r in results:
|
||||
@@ -107,11 +119,20 @@ func _draw() -> void:
|
||||
if desc.length() > MAX_DESC_CHARS:
|
||||
desc = desc.substr(0, MAX_DESC_CHARS - 1) + "..."
|
||||
var color: Color = MET_COLOR if is_met else UNMET_COLOR
|
||||
draw_string(font, Vector2(PADDING.x, y), prefix + desc, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, color)
|
||||
draw_string(
|
||||
font,
|
||||
Vector2(PADDING.x, y),
|
||||
prefix + desc,
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
FONT_SIZE,
|
||||
color
|
||||
)
|
||||
|
||||
|
||||
# -- Public API ---------------------------------------------------------------
|
||||
|
||||
|
||||
func get_evaluator():
|
||||
return _evaluator
|
||||
|
||||
|
||||
+34
-19
@@ -7,7 +7,7 @@ extends Control
|
||||
## Settings-toggled; enabled state persisted in user://settings.cfg.
|
||||
## D-088: triggers Overlay pause while open — sim must not advance during debug input.
|
||||
|
||||
signal pause_requested # D-088: pause sim while console is open
|
||||
signal pause_requested # D-088: pause sim while console is open
|
||||
signal unpause_requested # D-088: unpause sim when console closes
|
||||
|
||||
const PREFS_PATH := "user://settings.cfg"
|
||||
@@ -97,6 +97,7 @@ func _update_panel_layout() -> void:
|
||||
|
||||
# -- Input handling --
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not _enabled:
|
||||
return
|
||||
@@ -155,6 +156,7 @@ func is_open() -> bool:
|
||||
|
||||
# -- Command input --
|
||||
|
||||
|
||||
func _on_input_submitted(text: String) -> void:
|
||||
var trimmed := text.strip_edges()
|
||||
_input_line.clear()
|
||||
@@ -199,7 +201,9 @@ func _dispatch(line: String) -> void:
|
||||
z = int(parts[3])
|
||||
else:
|
||||
_append_text("tp: invalid z '%s' — defaulting to 0" % parts[3], ERROR_COLOR)
|
||||
_send_debug({"TeleportToPosition": {"x": int(parts[1]), "y": int(parts[2]), "z": z}})
|
||||
_send_debug(
|
||||
{"TeleportToPosition": {"x": int(parts[1]), "y": int(parts[2]), "z": z}}
|
||||
)
|
||||
else:
|
||||
var loc := " ".join(PackedStringArray(parts.slice(1)))
|
||||
_send_debug({"TeleportToLocation": loc})
|
||||
@@ -226,17 +230,23 @@ func _dispatch(line: String) -> void:
|
||||
|
||||
|
||||
func _send_debug(kind: Variant) -> void:
|
||||
var err := SimBridge.send_input({
|
||||
"action": InputMapper.Action.DEBUG_COMMAND,
|
||||
"action_data": kind,
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
})
|
||||
var err := (
|
||||
SimBridge
|
||||
. send_input(
|
||||
{
|
||||
"action": InputMapper.Action.DEBUG_COMMAND,
|
||||
"action_data": kind,
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
}
|
||||
)
|
||||
)
|
||||
if err != OK:
|
||||
_append_text("send error: %s" % error_string(err), ERROR_COLOR)
|
||||
|
||||
|
||||
# -- Response display --
|
||||
|
||||
|
||||
## Append a server debug response to the output log. Auto-opens console if closed
|
||||
## (only if console is enabled — respect user's settings toggle).
|
||||
func append_response(response: Dictionary) -> void:
|
||||
@@ -250,6 +260,7 @@ func append_response(response: Dictionary) -> void:
|
||||
|
||||
# -- Log rendering --
|
||||
|
||||
|
||||
func _append_text(text: String, color: Color) -> void:
|
||||
var escaped := text.replace("[", "[lb]").replace("]", "[rb]")
|
||||
_log_lines.append("[color=%s]%s[/color]" % [color.to_html(false), escaped])
|
||||
@@ -261,24 +272,27 @@ func _append_text(text: String, color: Color) -> void:
|
||||
|
||||
func _print_help() -> void:
|
||||
_append_text(
|
||||
"Commands:\n"
|
||||
+ " ticks <n> — fast-forward N ticks\n"
|
||||
+ " contaminate — skip to contamination phase\n"
|
||||
+ " tp <x> <y> [z] — teleport to tile position\n"
|
||||
+ " tp <location> — teleport to named location\n"
|
||||
+ " activate — force contamination activate\n"
|
||||
+ " triangle <id> — force triangle activation\n"
|
||||
+ " npc <entity_id> — inspect NPC state\n"
|
||||
+ " triangles — list all triangles\n"
|
||||
+ " pop — list active NPCs\n"
|
||||
+ " status — contamination status\n"
|
||||
+ " help — this list",
|
||||
(
|
||||
"Commands:\n"
|
||||
+ " ticks <n> — fast-forward N ticks\n"
|
||||
+ " contaminate — skip to contamination phase\n"
|
||||
+ " tp <x> <y> [z] — teleport to tile position\n"
|
||||
+ " tp <location> — teleport to named location\n"
|
||||
+ " activate — force contamination activate\n"
|
||||
+ " triangle <id> — force triangle activation\n"
|
||||
+ " npc <entity_id> — inspect NPC state\n"
|
||||
+ " triangles — list all triangles\n"
|
||||
+ " pop — list active NPCs\n"
|
||||
+ " status — contamination status\n"
|
||||
+ " help — this list"
|
||||
),
|
||||
TEXT_COLOR
|
||||
)
|
||||
|
||||
|
||||
# -- Command history --
|
||||
|
||||
|
||||
func _history_up() -> void:
|
||||
if _history.is_empty():
|
||||
return
|
||||
@@ -299,6 +313,7 @@ func _history_down() -> void:
|
||||
|
||||
# -- Settings --
|
||||
|
||||
|
||||
func set_enabled(enabled: bool) -> void:
|
||||
_enabled = enabled
|
||||
if not _enabled and _open:
|
||||
|
||||
+143
-43
@@ -37,7 +37,7 @@ const GRAPH_WARN_COLOR := Color("#e8c547")
|
||||
# Vision cone geometry (radians)
|
||||
# Forward: ±60° around facing direction (120° total)
|
||||
# Peripheral: ±60° to ±120° on each side (60° band each side)
|
||||
const CONE_FORWARD_HALF: float = PI / 3.0 # 60°
|
||||
const CONE_FORWARD_HALF: float = PI / 3.0 # 60°
|
||||
const CONE_PERIPHERAL_HALF: float = PI * 2.0 / 3.0 # 120°
|
||||
const CONE_ARC_STEPS: int = 20
|
||||
|
||||
@@ -65,14 +65,14 @@ var _npc_paths: Dictionary = {}
|
||||
var _last_tick_processed: int = -1
|
||||
|
||||
# Tick timing ring
|
||||
var _tick_times: Array = [] # Time.get_ticks_msec() on each snapshot
|
||||
var _tick_times: Array = [] # Time.get_ticks_msec() on each snapshot
|
||||
var _tick_deltas: Array = [] # ms between consecutive snapshots
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_dev_mode = OS.is_debug_build()
|
||||
visible = false
|
||||
@@ -147,6 +147,7 @@ func _update_npc_paths() -> void:
|
||||
# Draw dispatch
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if not visible:
|
||||
return
|
||||
@@ -159,6 +160,7 @@ func _draw() -> void:
|
||||
# Panel 1: Stats text (top-left)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
func _draw_stats_panel() -> void:
|
||||
var font: Font = _cached_font if _cached_font else ThemeDB.fallback_font
|
||||
|
||||
@@ -173,7 +175,9 @@ func _draw_stats_panel() -> void:
|
||||
right_lines.append(["facing", GameState.player_facing])
|
||||
|
||||
left_lines.append(["stance", GameState.player_stance])
|
||||
right_lines.append(["zone", GameState.current_zone_id if GameState.current_zone_id != "" else "-"])
|
||||
right_lines.append(
|
||||
["zone", GameState.current_zone_id if GameState.current_zone_id != "" else "-"]
|
||||
)
|
||||
|
||||
left_lines.append(["entities", str(GameState.visible_entities.size())])
|
||||
right_lines.append(["tiles", str(GameState.visible_tiles.size())])
|
||||
@@ -190,7 +194,9 @@ func _draw_stats_panel() -> void:
|
||||
right_lines.append(["insert", "ON" if GameState.insert_active else "OFF"])
|
||||
|
||||
var gt := GameState.game_time
|
||||
var time_str := "%s d%s" % [gt.get("day_phase", "-"), str(gt.get("day", "-"))] if gt.size() > 0 else "-"
|
||||
var time_str := (
|
||||
"%s d%s" % [gt.get("day_phase", "-"), str(gt.get("day", "-"))] if gt.size() > 0 else "-"
|
||||
)
|
||||
var rate_str: String = gt.get("tick_rate", "-") if gt.size() > 0 else "-"
|
||||
left_lines.append(["time", time_str])
|
||||
right_lines.append(["tick_rate", rate_str])
|
||||
@@ -210,14 +216,26 @@ func _draw_stats_panel() -> void:
|
||||
var right_value_w: float = 0.0
|
||||
|
||||
for line in left_lines:
|
||||
left_label_w = max(left_label_w, font.get_string_size(line[0] + ": ", HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x)
|
||||
left_value_w = max(left_value_w, font.get_string_size(line[1], HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x)
|
||||
left_label_w = max(
|
||||
left_label_w,
|
||||
font.get_string_size(line[0] + ": ", HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x
|
||||
)
|
||||
left_value_w = max(
|
||||
left_value_w, font.get_string_size(line[1], HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x
|
||||
)
|
||||
for line in right_lines:
|
||||
right_label_w = max(right_label_w, font.get_string_size(line[0] + ": ", HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x)
|
||||
right_value_w = max(right_value_w, font.get_string_size(line[1], HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x)
|
||||
right_label_w = max(
|
||||
right_label_w,
|
||||
font.get_string_size(line[0] + ": ", HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x
|
||||
)
|
||||
right_value_w = max(
|
||||
right_value_w, font.get_string_size(line[1], HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x
|
||||
)
|
||||
|
||||
var header_text := "F3 DEBUG"
|
||||
var header_w := font.get_string_size(header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE + 1).x
|
||||
var header_w := (
|
||||
font.get_string_size(header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE + 1).x
|
||||
)
|
||||
var content_w := left_label_w + left_value_w + COL_GAP + right_label_w + right_value_w
|
||||
var box_w: float = max(header_w, content_w) + PADDING.x * 2
|
||||
var line_count: int = maxi(left_lines.size(), right_lines.size())
|
||||
@@ -226,21 +244,57 @@ func _draw_stats_panel() -> void:
|
||||
draw_rect(Rect2(Vector2.ZERO, Vector2(box_w, box_h)), BG_COLOR)
|
||||
|
||||
var y: float = PADDING.y + FONT_SIZE
|
||||
draw_string(font, Vector2(PADDING.x, y), header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE + 1, HEADER_COLOR)
|
||||
draw_string(
|
||||
font,
|
||||
Vector2(PADDING.x, y),
|
||||
header_text,
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
FONT_SIZE + 1,
|
||||
HEADER_COLOR
|
||||
)
|
||||
y += LINE_HEIGHT
|
||||
|
||||
var right_x: float = PADDING.x + left_label_w + left_value_w + COL_GAP
|
||||
for i in range(line_count):
|
||||
if i < left_lines.size():
|
||||
draw_string(font, Vector2(PADDING.x, y), left_lines[i][0] + ": ",
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, LABEL_COLOR)
|
||||
draw_string(font, Vector2(PADDING.x + left_label_w, y), left_lines[i][1],
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, VALUE_COLOR)
|
||||
draw_string(
|
||||
font,
|
||||
Vector2(PADDING.x, y),
|
||||
left_lines[i][0] + ": ",
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
FONT_SIZE,
|
||||
LABEL_COLOR
|
||||
)
|
||||
draw_string(
|
||||
font,
|
||||
Vector2(PADDING.x + left_label_w, y),
|
||||
left_lines[i][1],
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
FONT_SIZE,
|
||||
VALUE_COLOR
|
||||
)
|
||||
if i < right_lines.size():
|
||||
draw_string(font, Vector2(right_x, y), right_lines[i][0] + ": ",
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, LABEL_COLOR)
|
||||
draw_string(font, Vector2(right_x + right_label_w, y), right_lines[i][1],
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, VALUE_COLOR)
|
||||
draw_string(
|
||||
font,
|
||||
Vector2(right_x, y),
|
||||
right_lines[i][0] + ": ",
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
FONT_SIZE,
|
||||
LABEL_COLOR
|
||||
)
|
||||
draw_string(
|
||||
font,
|
||||
Vector2(right_x + right_label_w, y),
|
||||
right_lines[i][1],
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
FONT_SIZE,
|
||||
VALUE_COLOR
|
||||
)
|
||||
y += LINE_HEIGHT
|
||||
|
||||
|
||||
@@ -248,6 +302,7 @@ func _draw_stats_panel() -> void:
|
||||
# Panel 2: World overlays
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
func _draw_world_overlays() -> void:
|
||||
var vp := get_viewport()
|
||||
if vp == null:
|
||||
@@ -291,9 +346,15 @@ func _draw_vision_cone(player_screen: Vector2, canvas_xf: Transform2D) -> void:
|
||||
var perip_r_from := facing_angle + CONE_FORWARD_HALF
|
||||
var perip_r_to := facing_angle + CONE_PERIPHERAL_HALF
|
||||
|
||||
draw_colored_polygon(_arc_polygon(player_screen, r, forward_from, forward_to), CONE_FORWARD_COLOR)
|
||||
draw_colored_polygon(_arc_polygon(player_screen, r, perip_l_from, perip_l_to), CONE_PERIPHERAL_COLOR)
|
||||
draw_colored_polygon(_arc_polygon(player_screen, r, perip_r_from, perip_r_to), CONE_PERIPHERAL_COLOR)
|
||||
draw_colored_polygon(
|
||||
_arc_polygon(player_screen, r, forward_from, forward_to), CONE_FORWARD_COLOR
|
||||
)
|
||||
draw_colored_polygon(
|
||||
_arc_polygon(player_screen, r, perip_l_from, perip_l_to), CONE_PERIPHERAL_COLOR
|
||||
)
|
||||
draw_colored_polygon(
|
||||
_arc_polygon(player_screen, r, perip_r_from, perip_r_to), CONE_PERIPHERAL_COLOR
|
||||
)
|
||||
|
||||
# Forward arc boundary ring
|
||||
draw_arc(player_screen, r, forward_from, forward_to, CONE_ARC_STEPS, CONE_RING_COLOR, 1.0)
|
||||
@@ -303,7 +364,9 @@ func _draw_vision_cone(player_screen: Vector2, canvas_xf: Transform2D) -> void:
|
||||
|
||||
|
||||
# Build a filled polygon fan from center through an arc
|
||||
func _arc_polygon(center: Vector2, radius: float, angle_from: float, angle_to: float) -> PackedVector2Array:
|
||||
func _arc_polygon(
|
||||
center: Vector2, radius: float, angle_from: float, angle_to: float
|
||||
) -> PackedVector2Array:
|
||||
var pts := PackedVector2Array()
|
||||
pts.append(center)
|
||||
for i in range(CONE_ARC_STEPS + 1):
|
||||
@@ -339,8 +402,14 @@ func _draw_npc_paths(canvas_xf: Transform2D) -> void:
|
||||
var a_screen := _w2s(path[i - 1], canvas_xf)
|
||||
var b_screen := _w2s(path[i], canvas_xf)
|
||||
var alpha := float(i) / float(path.size())
|
||||
draw_line(a_screen, b_screen,
|
||||
Color(NPC_PATH_COLOR.r, NPC_PATH_COLOR.g, NPC_PATH_COLOR.b, NPC_PATH_COLOR.a * alpha), 1.5)
|
||||
draw_line(
|
||||
a_screen,
|
||||
b_screen,
|
||||
Color(
|
||||
NPC_PATH_COLOR.r, NPC_PATH_COLOR.g, NPC_PATH_COLOR.b, NPC_PATH_COLOR.a * alpha
|
||||
),
|
||||
1.5
|
||||
)
|
||||
draw_circle(_w2s(path.back(), canvas_xf), NPC_DOT_RADIUS, NPC_DOT_COLOR)
|
||||
|
||||
|
||||
@@ -377,17 +446,25 @@ func _draw_info_tags(canvas_xf: Transform2D) -> void:
|
||||
entity_screen.x - text_w / 2.0 - 3.0,
|
||||
tag_baseline - FONT_SIZE + 2.0,
|
||||
text_w + 6.0,
|
||||
FONT_SIZE)
|
||||
FONT_SIZE
|
||||
)
|
||||
draw_rect(tag_rect, TAG_BG_COLOR)
|
||||
draw_string(font,
|
||||
draw_string(
|
||||
font,
|
||||
Vector2(entity_screen.x - text_w / 2.0, tag_baseline),
|
||||
label, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE - 1, TAG_TEXT_COLOR)
|
||||
label,
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
FONT_SIZE - 1,
|
||||
TAG_TEXT_COLOR
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Panel 3: Tick timing sparkline (bottom-left)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
func _draw_tick_graph() -> void:
|
||||
if _tick_deltas.size() < 2:
|
||||
return
|
||||
@@ -398,10 +475,15 @@ func _draw_tick_graph() -> void:
|
||||
var box_y := vp_size.y - GRAPH_H - label_h - GRAPH_MARGIN
|
||||
|
||||
draw_rect(Rect2(box_x, box_y, GRAPH_W, GRAPH_H + label_h), GRAPH_BG_COLOR)
|
||||
draw_string(font,
|
||||
draw_string(
|
||||
font,
|
||||
Vector2(box_x + 4, box_y + FONT_SIZE + 1),
|
||||
"tick ms (n=%d)" % _tick_deltas.size(),
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE - 1, LABEL_COLOR)
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
FONT_SIZE - 1,
|
||||
LABEL_COLOR
|
||||
)
|
||||
|
||||
var chart_top := box_y + label_h
|
||||
var chart_left := box_x + 4.0
|
||||
@@ -418,9 +500,12 @@ func _draw_tick_graph() -> void:
|
||||
# Warn threshold dashed line
|
||||
var warn_y := chart_top + chart_h * (1.0 - TICK_WARN_MS / max_ms)
|
||||
draw_dashed_line(
|
||||
Vector2(chart_left, warn_y), Vector2(chart_left + chart_w, warn_y),
|
||||
Vector2(chart_left, warn_y),
|
||||
Vector2(chart_left + chart_w, warn_y),
|
||||
Color(GRAPH_WARN_COLOR.r, GRAPH_WARN_COLOR.g, GRAPH_WARN_COLOR.b, 0.3),
|
||||
1.0, 4.0)
|
||||
1.0,
|
||||
4.0
|
||||
)
|
||||
|
||||
# Sparkline
|
||||
var n := _tick_deltas.size()
|
||||
@@ -441,25 +526,40 @@ func _draw_tick_graph() -> void:
|
||||
for d in _tick_deltas:
|
||||
avg_ms += float(d)
|
||||
avg_ms /= float(_tick_deltas.size())
|
||||
draw_string(font,
|
||||
draw_string(
|
||||
font,
|
||||
Vector2(chart_left + chart_w - 54.0, chart_top + chart_h + FONT_SIZE - 2),
|
||||
"avg %.0fms" % avg_ms,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE - 1, LABEL_COLOR)
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
FONT_SIZE - 1,
|
||||
LABEL_COLOR
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
# Convert facing string to angle in radians (Godot 2D: 0=East, -PI/2=North)
|
||||
static func _facing_to_angle(facing: String) -> float:
|
||||
match facing:
|
||||
"North": return -PI / 2.0
|
||||
"Northeast": return -PI / 4.0
|
||||
"East": return 0.0
|
||||
"Southeast": return PI / 4.0
|
||||
"South": return PI / 2.0
|
||||
"Southwest": return PI * 3.0 / 4.0
|
||||
"West": return PI
|
||||
"Northwest": return -PI * 3.0 / 4.0
|
||||
_: return -PI / 2.0
|
||||
"North":
|
||||
return -PI / 2.0
|
||||
"Northeast":
|
||||
return -PI / 4.0
|
||||
"East":
|
||||
return 0.0
|
||||
"Southeast":
|
||||
return PI / 4.0
|
||||
"South":
|
||||
return PI / 2.0
|
||||
"Southwest":
|
||||
return PI * 3.0 / 4.0
|
||||
"West":
|
||||
return PI
|
||||
"Northwest":
|
||||
return -PI * 3.0 / 4.0
|
||||
_:
|
||||
return -PI / 2.0
|
||||
|
||||
+87
-40
@@ -13,7 +13,7 @@ extends Control
|
||||
signal option_selected(response_id: String, text: String)
|
||||
signal dialogue_dismissed # Walk-away or conversation end
|
||||
signal confrontation_monologue(text: String, duration: float) # D-063: beat monologue
|
||||
signal pause_requested # D-061: auto-pause — main.gd routes through input recording (#507)
|
||||
signal pause_requested # D-061: auto-pause — main.gd routes through input recording (#507)
|
||||
signal unpause_requested # D-061: auto-unpause
|
||||
# D-020 (#558): Decoupled signals — dialogue_box emits, main.gd (coordinator) handles.
|
||||
# Replaces direct GameState.dialogue_active mutation and AudioManager calls.
|
||||
@@ -25,8 +25,8 @@ const THEME_PATH: String = "res://data/dialogue-theme.yaml"
|
||||
|
||||
const FADE_IN: float = 0.2
|
||||
const FADE_OUT: float = 0.3 # D-064: 300ms fade on walk-away
|
||||
const MAX_OPTIONS: int = 3 # D-061: max 3 response options visible
|
||||
const MAX_HEIGHT_RATIO: float = 0.2 # D-061: max 20% viewport height
|
||||
const MAX_OPTIONS: int = 3 # D-061: max 3 response options visible
|
||||
const MAX_HEIGHT_RATIO: float = 0.2 # D-061: max 20% viewport height
|
||||
const MAX_WIDTH_PX: float = Constants.DIALOGUE_MAX_WIDTH # D-076: 640px (OQ-29)
|
||||
const CONFRONTATION_BEAT_DURATION: float = 1.5 # D-063: pause before sending
|
||||
const CONFRONTATION_DIM_ALPHA: float = 0.7 # D-063: dialogue box dims during beat
|
||||
@@ -38,8 +38,14 @@ const MIN_CONTRAST_LUMINANCE: float = 0.25 # Floor for name colour brightness a
|
||||
|
||||
# D-064: movement actions that trigger walk-away
|
||||
const _WALK_AWAY_ACTIONS: Array[StringName] = [
|
||||
&"move_north", &"move_south", &"move_east", &"move_west",
|
||||
&"move_northeast", &"move_southeast", &"move_southwest", &"move_northwest",
|
||||
&"move_north",
|
||||
&"move_south",
|
||||
&"move_east",
|
||||
&"move_west",
|
||||
&"move_northeast",
|
||||
&"move_southeast",
|
||||
&"move_southwest",
|
||||
&"move_northwest",
|
||||
]
|
||||
|
||||
# -- Log state --
|
||||
@@ -54,7 +60,6 @@ var _log_dirty: bool = false # Dirty flag — prevents per-frame O(n) BBCode re
|
||||
# Populated from server events; drives retroactive re-render when NPC names resolve.
|
||||
var _entity_display: Dictionary = {}
|
||||
|
||||
|
||||
# -- Option state --
|
||||
var _option_controls: Array[Control] = []
|
||||
var _option_response_ids: Array[String] = []
|
||||
@@ -68,8 +73,8 @@ var _npc_name: String = ""
|
||||
# v0.1: colors are per-conversation — cleared in _end_player_conversation() to avoid
|
||||
# palette exhaustion (8 entries) across long sessions with 9+ NPCs.
|
||||
var _npc_entity_colors: Dictionary = {} # entity_id -> Color
|
||||
var _npc_entity_id: int = -1 # Entity ID of the current player conversation NPC
|
||||
var _next_npc_color: int = 0 # Round-robin palette index for client-side assignment
|
||||
var _npc_entity_id: int = -1 # Entity ID of the current player conversation NPC
|
||||
var _next_npc_color: int = 0 # Round-robin palette index for client-side assignment
|
||||
|
||||
# -- UI state --
|
||||
var _active_tween: Tween = null
|
||||
@@ -86,7 +91,8 @@ var _entry_fade: float = 5.0
|
||||
|
||||
@onready var panel: PanelContainer = $PanelContainer
|
||||
@onready var dialogue_log: RichTextLabel = $PanelContainer/MarginContainer/VBoxContainer/DialogueLog
|
||||
@onready var options_container: VBoxContainer = $PanelContainer/MarginContainer/VBoxContainer/OptionsContainer
|
||||
@onready
|
||||
var options_container: VBoxContainer = $PanelContainer/MarginContainer/VBoxContainer/OptionsContainer
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -114,15 +120,17 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
# Number keys 1-3 select dialogue options
|
||||
if event is InputEventKey and event.pressed:
|
||||
var key_index := -1
|
||||
if event.keycode == KEY_1: key_index = 0
|
||||
elif event.keycode == KEY_2: key_index = 1
|
||||
elif event.keycode == KEY_3: key_index = 2
|
||||
if event.keycode == KEY_1:
|
||||
key_index = 0
|
||||
elif event.keycode == KEY_2:
|
||||
key_index = 1
|
||||
elif event.keycode == KEY_3:
|
||||
key_index = 2
|
||||
if key_index >= 0 and key_index < _option_controls.size():
|
||||
get_viewport().set_input_as_handled()
|
||||
_on_option_pressed(key_index)
|
||||
return
|
||||
|
||||
|
||||
# D-064: WASD during active player dialogue → walk-away
|
||||
if event is InputEventKey and event.pressed:
|
||||
for action in _WALK_AWAY_ACTIONS:
|
||||
@@ -136,11 +144,18 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
|
||||
# -- Theme loading --
|
||||
|
||||
|
||||
func _load_theme() -> void:
|
||||
# Default NPC palette in case file not found
|
||||
_npc_colors = [
|
||||
Color("#4a9ebb"), Color("#6bc9a6"), Color("#e8c547"), Color("#d49e5d"),
|
||||
Color("#b586d4"), Color("#d45d5d"), Color("#5daa7d"), Color("#7daccc"),
|
||||
Color("#4a9ebb"),
|
||||
Color("#6bc9a6"),
|
||||
Color("#e8c547"),
|
||||
Color("#d49e5d"),
|
||||
Color("#b586d4"),
|
||||
Color("#d45d5d"),
|
||||
Color("#5daa7d"),
|
||||
Color("#7daccc"),
|
||||
]
|
||||
|
||||
if not FileAccess.file_exists(THEME_PATH):
|
||||
@@ -180,6 +195,7 @@ func _load_theme() -> void:
|
||||
|
||||
# -- Responsive layout --
|
||||
|
||||
|
||||
func _update_layout() -> void:
|
||||
var vp := get_viewport_rect().size
|
||||
var max_h := vp.y * MAX_HEIGHT_RATIO
|
||||
@@ -191,14 +207,21 @@ func _update_layout() -> void:
|
||||
|
||||
# -- Log entry management --
|
||||
|
||||
|
||||
## Append a dialogue line to the log.
|
||||
## speaker/target: display names. text: the spoken line.
|
||||
## is_passive: true for overheard NPC-NPC (renders with ┃ prefix + desaturated).
|
||||
## Active conversation entries are pinned (no timeout) while _in_player_conversation.
|
||||
## speaker_entity_id/target_entity_id: optional entity IDs for stable color lookup (#573).
|
||||
## TODO Phase 2: 6 positional params is unwieldy — consider dictionary-options overload.
|
||||
func append_line(speaker: String, target: String, text: String,
|
||||
is_passive: bool = false, speaker_entity_id: int = -1, target_entity_id: int = -1) -> void:
|
||||
func append_line(
|
||||
speaker: String,
|
||||
target: String,
|
||||
text: String,
|
||||
is_passive: bool = false,
|
||||
speaker_entity_id: int = -1,
|
||||
target_entity_id: int = -1
|
||||
) -> void:
|
||||
var pinned := not is_passive and _in_player_conversation
|
||||
var entry: Dictionary = {
|
||||
"speaker": speaker,
|
||||
@@ -294,10 +317,13 @@ func append_dialogue_response(npc_name: String, text: String, entity_id: int = -
|
||||
|
||||
# -- Active player conversation --
|
||||
|
||||
|
||||
## Show dialogue with NPC speech and response options.
|
||||
## npc_name: who is speaking. speech: the NPC's line. options: player choices.
|
||||
## npc_entity_id: entity ID of the NPC for stable color assignment (#573).
|
||||
func show_dialogue(npc_name: String, speech: String, options: Array = [], npc_entity_id: int = -1) -> void:
|
||||
func show_dialogue(
|
||||
npc_name: String, speech: String, options: Array = [], npc_entity_id: int = -1
|
||||
) -> void:
|
||||
_npc_name = npc_name
|
||||
_npc_entity_id = npc_entity_id
|
||||
_cancel_beat()
|
||||
@@ -379,6 +405,7 @@ func has_active_entries() -> bool:
|
||||
|
||||
# -- Options --
|
||||
|
||||
|
||||
func _show_options(options: Array) -> void:
|
||||
var sorted_opts: Array = options.duplicate()
|
||||
sorted_opts.sort_custom(func(a, b): return a.get("priority", 0) < b.get("priority", 0))
|
||||
@@ -398,12 +425,15 @@ func _show_options(options: Array) -> void:
|
||||
rtl.fit_content = true
|
||||
rtl.scroll_active = false
|
||||
rtl.add_theme_font_size_override("normal_font_size", 14)
|
||||
rtl.add_theme_color_override("default_color", Color(
|
||||
Constants.INSERT_COLOR_TEXT.r * 1.08,
|
||||
Constants.INSERT_COLOR_TEXT.g * 0.96,
|
||||
Constants.INSERT_COLOR_TEXT.b * 0.90,
|
||||
1.0
|
||||
)) # Slight warm tint for confrontation weight
|
||||
rtl.add_theme_color_override(
|
||||
"default_color",
|
||||
Color(
|
||||
Constants.INSERT_COLOR_TEXT.r * 1.08,
|
||||
Constants.INSERT_COLOR_TEXT.g * 0.96,
|
||||
Constants.INSERT_COLOR_TEXT.b * 0.90,
|
||||
1.0
|
||||
)
|
||||
) # Slight warm tint for confrontation weight
|
||||
rtl.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
rtl.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
rtl.text = "[i]%d. %s[/i]" % [i + 1, raw_text]
|
||||
@@ -419,9 +449,14 @@ func _show_options(options: Array) -> void:
|
||||
ctrl = label
|
||||
|
||||
var idx := i
|
||||
ctrl.gui_input.connect(func(event: InputEvent):
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
_on_option_pressed(idx)
|
||||
ctrl.gui_input.connect(
|
||||
func(event: InputEvent):
|
||||
if (
|
||||
event is InputEventMouseButton
|
||||
and event.pressed
|
||||
and event.button_index == MOUSE_BUTTON_LEFT
|
||||
):
|
||||
_on_option_pressed(idx)
|
||||
)
|
||||
ctrl.mouse_entered.connect(_make_hover_on(ctrl))
|
||||
ctrl.mouse_exited.connect(_make_hover_off(ctrl))
|
||||
@@ -438,7 +473,9 @@ func _on_option_pressed(index: int) -> void:
|
||||
return
|
||||
var rid: String = _option_response_ids[index] if index < _option_response_ids.size() else ""
|
||||
var text: String = _option_texts[index] if index < _option_texts.size() else ""
|
||||
var is_confront: bool = _option_is_confrontation[index] if index < _option_is_confrontation.size() else false
|
||||
var is_confront: bool = (
|
||||
_option_is_confrontation[index] if index < _option_is_confrontation.size() else false
|
||||
)
|
||||
|
||||
# Append player's chosen response to the log
|
||||
append_player_line(_npc_name, text)
|
||||
@@ -452,6 +489,7 @@ func _on_option_pressed(index: int) -> void:
|
||||
|
||||
# -- Confrontation beat (D-063) --
|
||||
|
||||
|
||||
func _start_confrontation_beat(response_id: String, text: String) -> void:
|
||||
for ctrl in _option_controls:
|
||||
if is_instance_valid(ctrl):
|
||||
@@ -462,15 +500,18 @@ func _start_confrontation_beat(response_id: String, text: String) -> void:
|
||||
_active_tween = create_tween()
|
||||
_active_tween.tween_property(panel, "modulate:a", CONFRONTATION_DIM_ALPHA, 0.2)
|
||||
|
||||
confrontation_monologue.emit(UIStrings.get_text(CONFRONTATION_MONOLOGUE_KEY), CONFRONTATION_BEAT_DURATION)
|
||||
confrontation_monologue.emit(
|
||||
UIStrings.get_text(CONFRONTATION_MONOLOGUE_KEY), CONFRONTATION_BEAT_DURATION
|
||||
)
|
||||
audio_dip_requested.emit("confrontation") # D-069: coordinator routes to AudioManager
|
||||
|
||||
_beat_tween = create_tween()
|
||||
_beat_tween.tween_interval(CONFRONTATION_BEAT_DURATION)
|
||||
_beat_tween.tween_callback(func():
|
||||
audio_dip_cleared.emit() # D-069: coordinator routes to AudioManager
|
||||
option_selected.emit(response_id, text)
|
||||
_end_player_conversation()
|
||||
_beat_tween.tween_callback(
|
||||
func():
|
||||
audio_dip_cleared.emit() # D-069: coordinator routes to AudioManager
|
||||
option_selected.emit(response_id, text)
|
||||
_end_player_conversation()
|
||||
)
|
||||
|
||||
|
||||
@@ -483,6 +524,7 @@ func _cancel_beat() -> void:
|
||||
|
||||
# -- Log rendering --
|
||||
|
||||
|
||||
## Rebuild the dialogue log BBCode from all non-expired entries.
|
||||
func _rebuild_log() -> void:
|
||||
if not dialogue_log:
|
||||
@@ -550,7 +592,9 @@ func _format_entry(entry: Dictionary, alpha: float) -> String:
|
||||
target_color = _npc_entity_colors[tg_eid]
|
||||
else:
|
||||
target_color = _color_for_name(entry.get("target", "?"))
|
||||
involves_player = (entry.get("speaker", "") == PLAYER_NAME) or (entry.get("target", "") == PLAYER_NAME)
|
||||
involves_player = (
|
||||
(entry.get("speaker", "") == PLAYER_NAME) or (entry.get("target", "") == PLAYER_NAME)
|
||||
)
|
||||
|
||||
var text: String = _escape_bbcode(entry.text)
|
||||
var is_passive: bool = entry.is_passive
|
||||
@@ -570,13 +614,14 @@ func _format_entry(entry: Dictionary, alpha: float) -> String:
|
||||
# Non-blocking: simplify 1-on-1 player dialogue — no arrow for Speaker → You or You → Speaker
|
||||
if involves_player and not is_passive:
|
||||
# Just "Speaker: text" or "You: text"
|
||||
return "%s[color=%s][b]%s[/b][/color][color=%s]: %s[/color]" % [
|
||||
prefix, sc, speaker, txc, text
|
||||
]
|
||||
return (
|
||||
"%s[color=%s][b]%s[/b][/color][color=%s]: %s[/color]" % [prefix, sc, speaker, txc, text]
|
||||
)
|
||||
# Full "Speaker → Target: text" for overheard
|
||||
return "%s[color=%s][b]%s[/b][/color][color=%s] \u2192 [/color][color=%s][b]%s[/b][/color][color=%s]: %s[/color]" % [
|
||||
prefix, sc, speaker, ac, tc, target, txc, text
|
||||
]
|
||||
return (
|
||||
"%s[color=%s][b]%s[/b][/color][color=%s] \u2192 [/color][color=%s][b]%s[/b][/color][color=%s]: %s[/color]"
|
||||
% [prefix, sc, speaker, ac, tc, target, txc, text]
|
||||
)
|
||||
|
||||
|
||||
## Escape BBCode bracket characters in server-sourced text (Hoshe #2).
|
||||
@@ -644,6 +689,7 @@ static func _color_with_alpha(base: Color, alpha: float) -> String:
|
||||
|
||||
# -- Entry expiry --
|
||||
|
||||
|
||||
## Remove fully expired entries. Mark dirty if any fading entries exist.
|
||||
## Pinned entries (active conversation) skip expiry entirely (Araminta #2).
|
||||
func _expire_entries() -> void:
|
||||
@@ -689,6 +735,7 @@ func _expire_entries() -> void:
|
||||
|
||||
# -- Visibility --
|
||||
|
||||
|
||||
## No-op — panel is always visible as a permanent insert UI element.
|
||||
func _ensure_visible() -> void:
|
||||
pass
|
||||
|
||||
@@ -9,7 +9,7 @@ extends Control
|
||||
## Positioned in InsertOverlay (CanvasLayer 10, z-layer 6).
|
||||
## Only one examine result is shown at a time — new result replaces old.
|
||||
|
||||
const DISMISS_DELAY: float = 5.0 # Auto-dismiss after 5 seconds
|
||||
const DISMISS_DELAY: float = 5.0 # Auto-dismiss after 5 seconds
|
||||
const FADE_IN: float = 0.18
|
||||
const FADE_OUT: float = 0.35
|
||||
|
||||
@@ -27,6 +27,7 @@ var _active: bool = false
|
||||
@onready var panel: PanelContainer = $PanelContainer
|
||||
@onready var text_label: RichTextLabel = $PanelContainer/MarginContainer/TextLabel
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
modulate.a = 0.0
|
||||
@@ -48,8 +49,12 @@ func show_result(result: Dictionary) -> void:
|
||||
|
||||
# Apply confidence-based alpha to the insert color
|
||||
var alpha: float = CONFIDENCE_ALPHA.get(confidence, 0.75)
|
||||
var col := Color(Constants.INSERT_COLOR_TEXT.r, Constants.INSERT_COLOR_TEXT.g,
|
||||
Constants.INSERT_COLOR_TEXT.b, alpha)
|
||||
var col := Color(
|
||||
Constants.INSERT_COLOR_TEXT.r,
|
||||
Constants.INSERT_COLOR_TEXT.g,
|
||||
Constants.INSERT_COLOR_TEXT.b,
|
||||
alpha
|
||||
)
|
||||
text_label.add_theme_color_override("default_color", col)
|
||||
text_label.text = text
|
||||
|
||||
@@ -69,9 +74,10 @@ func _start_fade_out() -> void:
|
||||
return
|
||||
var t := create_tween()
|
||||
t.tween_property(self, "modulate:a", 0.0, FADE_OUT)
|
||||
t.tween_callback(func():
|
||||
visible = false
|
||||
_active = false
|
||||
t.tween_callback(
|
||||
func():
|
||||
visible = false
|
||||
_active = false
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ extends Control
|
||||
## Hidden in non-gauntlet mode. Stats persisted to user://dev/gauntlet-stats.json.
|
||||
|
||||
const BG_COLOR := Color(0.05, 0.05, 0.08, 0.5)
|
||||
const TIMER_COLOR := Color("#c8d0e0") # Default insert text
|
||||
const PB_COLOR := Color("#6bc9a6") # Friendly green — personal best
|
||||
const NEW_PB_COLOR := Color("#e8c547") # Amber flash on new PB
|
||||
const TIMER_COLOR := Color("#c8d0e0") # Default insert text
|
||||
const PB_COLOR := Color("#6bc9a6") # Friendly green — personal best
|
||||
const NEW_PB_COLOR := Color("#e8c547") # Amber flash on new PB
|
||||
const FONT_SIZE := 13
|
||||
const PADDING := Vector2(10, 6)
|
||||
const STATS_PATH := "user://dev/gauntlet-stats.json"
|
||||
@@ -109,13 +109,28 @@ func _draw() -> void:
|
||||
# Timer text
|
||||
var y_offset := PADDING.y + text_size.y
|
||||
var timer_size := font.get_string_size(timer_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE)
|
||||
draw_string(font, Vector2(PADDING.x, y_offset), timer_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, TIMER_COLOR)
|
||||
draw_string(
|
||||
font,
|
||||
Vector2(PADDING.x, y_offset),
|
||||
timer_text,
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
FONT_SIZE,
|
||||
TIMER_COLOR
|
||||
)
|
||||
|
||||
# PB text (different color)
|
||||
if not pb_text.is_empty():
|
||||
var pb_color: Color = NEW_PB_COLOR if _new_pb_flash > 0.0 else PB_COLOR
|
||||
draw_string(font, Vector2(PADDING.x + timer_size.x, y_offset), pb_text,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, pb_color)
|
||||
draw_string(
|
||||
font,
|
||||
Vector2(PADDING.x + timer_size.x, y_offset),
|
||||
pb_text,
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
FONT_SIZE,
|
||||
pb_color
|
||||
)
|
||||
|
||||
|
||||
static func _format_time(seconds: float) -> String:
|
||||
@@ -154,9 +169,15 @@ func print_session_summary() -> void:
|
||||
for room_id in _session_rooms:
|
||||
var entry: Dictionary = _session_rooms[room_id]
|
||||
var best_str := _format_time(entry["best"]) if entry["best"] != INF else "--:--"
|
||||
var pb_str := _format_time(_personal_bests[room_id]) if _personal_bests.has(room_id) else "--:--"
|
||||
print(" Room %s: %d attempts, session best %s, all-time PB %s" % [
|
||||
room_id, entry["attempts"], best_str, pb_str])
|
||||
var pb_str := (
|
||||
_format_time(_personal_bests[room_id]) if _personal_bests.has(room_id) else "--:--"
|
||||
)
|
||||
print(
|
||||
(
|
||||
" Room %s: %d attempts, session best %s, all-time PB %s"
|
||||
% [room_id, entry["attempts"], best_str, pb_str]
|
||||
)
|
||||
)
|
||||
print("================================")
|
||||
|
||||
|
||||
@@ -170,14 +191,18 @@ func finalize() -> void:
|
||||
|
||||
# -- Public API ---------------------------------------------------------------
|
||||
|
||||
|
||||
func get_timer_seconds() -> float:
|
||||
return _timer_seconds
|
||||
|
||||
|
||||
func is_timer_running() -> bool:
|
||||
return _timer_running
|
||||
|
||||
|
||||
func get_current_room_id() -> Variant:
|
||||
return _current_room_id
|
||||
|
||||
|
||||
func get_personal_best(for_room_id: String) -> float:
|
||||
return _personal_bests.get(for_room_id, INF)
|
||||
|
||||
@@ -8,10 +8,10 @@ extends Node
|
||||
## Loaded as HardwareDetector autoload. Tests access via get_node("/root/HardwareDetector").
|
||||
## Settings dialog reads classification strings and power-state methods to configure the UI.
|
||||
|
||||
const RAM_PASS_THRESHOLD_MB := 2000.0
|
||||
const RAM_PASS_THRESHOLD_MB := 2000.0
|
||||
const RAM_MARGINAL_THRESHOLD_MB := 1600.0
|
||||
const TPT_GREEN_THRESHOLD := 6.0 # tokens/sec — enable silently
|
||||
const TPT_YELLOW_THRESHOLD := 3.0 # tokens/sec — partial pre-voicing note
|
||||
const TPT_GREEN_THRESHOLD := 6.0 # tokens/sec — enable silently
|
||||
const TPT_YELLOW_THRESHOLD := 3.0 # tokens/sec — partial pre-voicing note
|
||||
const TPT_DEGRADATION_THRESHOLD := 0.4 # fraction — >40% sustained drop → yellow
|
||||
|
||||
## Benchmark cache path — reads from PlatformInfo for cross-platform correctness.
|
||||
@@ -28,9 +28,9 @@ var inference_suspended: bool = false
|
||||
# User's preference at the moment battery suspension activated (to restore on plug-in).
|
||||
var _pre_battery_pref: bool = true
|
||||
|
||||
|
||||
# -- Lifecycle ----------------------------------------------------------------
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
assert(PlatformInfo != null, "PlatformInfo must load before HardwareDetector")
|
||||
load_ai_pref()
|
||||
@@ -40,6 +40,7 @@ func _ready() -> void:
|
||||
|
||||
# -- Layer 1: RAM classification ----------------------------------------------
|
||||
|
||||
|
||||
## Layer 1 — RAM classification.
|
||||
## Accepts free MB as input; returns "pass" | "marginal" | "fail".
|
||||
func classify_ram(free_mb: float) -> String:
|
||||
@@ -54,11 +55,15 @@ func classify_ram(free_mb: float) -> String:
|
||||
## Returns: {classification: String, free_mb: float}
|
||||
func check_ram() -> Dictionary:
|
||||
PlatformInfo.refresh_memory()
|
||||
return {"classification": classify_ram(PlatformInfo.free_memory_mb), "free_mb": PlatformInfo.free_memory_mb}
|
||||
return {
|
||||
"classification": classify_ram(PlatformInfo.free_memory_mb),
|
||||
"free_mb": PlatformInfo.free_memory_mb
|
||||
}
|
||||
|
||||
|
||||
# -- Layer 2: TPT benchmark classification ------------------------------------
|
||||
|
||||
|
||||
## Layer 2 — TPT benchmark classification.
|
||||
## Accepts tokens/sec as input; returns "green" | "yellow" | "red".
|
||||
func classify_tpt(tps: float) -> String:
|
||||
@@ -90,6 +95,7 @@ func read_benchmark_cache() -> Variant:
|
||||
|
||||
# -- Layer 3: degradation + battery -------------------------------------------
|
||||
|
||||
|
||||
## Layer 3 — Ongoing TPT degradation classification.
|
||||
## D-138 §8: >40% sustained drop from baseline → yellow (not forced disable).
|
||||
## Returns "ok" | "yellow".
|
||||
@@ -130,6 +136,7 @@ func is_inference_suspended() -> bool:
|
||||
|
||||
# -- Startup pref load --------------------------------------------------------
|
||||
|
||||
|
||||
## Load AI dialogue preference from settings.cfg and apply to GameState.
|
||||
## Called from _ready() to restore the toggle state across session restarts.
|
||||
## Spec: D-138 §8 — toggle persists via ConfigFile (client-local) + server SQLite (#627).
|
||||
@@ -144,6 +151,7 @@ func load_ai_pref() -> void:
|
||||
|
||||
# -- Battery suspend/resume ---------------------------------------------------
|
||||
|
||||
|
||||
func _on_power_profile_changed(old_profile: int, new_profile: int) -> void:
|
||||
if new_profile == PlatformInfo.PowerProfile.BATTERY:
|
||||
_suspend_for_battery()
|
||||
@@ -176,34 +184,49 @@ func _resume_from_battery() -> void:
|
||||
func _send_settings_change(enabled: bool) -> void:
|
||||
if SimBridge.state != SimBridge.ConnectionState.CONNECTED:
|
||||
return
|
||||
SimBridge.send_input({
|
||||
"action": InputMapper.Action.CHANGE_SETTINGS,
|
||||
"action_data": {"ai_enhanced_dialogue": enabled},
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
})
|
||||
(
|
||||
SimBridge
|
||||
. send_input(
|
||||
{
|
||||
"action": InputMapper.Action.CHANGE_SETTINGS,
|
||||
"action_data": {"ai_enhanced_dialogue": enabled},
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# -- Status message -----------------------------------------------------------
|
||||
|
||||
|
||||
## Compose a human-readable status message for the settings dialog.
|
||||
## classification: "pass" | "marginal" | "fail" (RAM) or "green" | "yellow" | "red" (TPT).
|
||||
## Returns empty string when no message is needed.
|
||||
func status_message(ram_classification: String, tpt_classification: String, free_mb: float, tps: float) -> String:
|
||||
func status_message(
|
||||
ram_classification: String, tpt_classification: String, free_mb: float, tps: float
|
||||
) -> String:
|
||||
if ram_classification == "fail":
|
||||
return (
|
||||
"AI-Enhanced Dialogue requires 2 GB of free memory. Your system currently has %.0f MB available."
|
||||
(
|
||||
"AI-Enhanced Dialogue requires 2 GB of free memory. Your system currently has %.0f MB available."
|
||||
+ " Close other applications and try again, or leave the setting off — the game is complete either way."
|
||||
) % free_mb
|
||||
)
|
||||
% free_mb
|
||||
)
|
||||
if ram_classification == "marginal":
|
||||
return "Only %.0f MB free — performance may vary. You can still enable it." % free_mb
|
||||
match tpt_classification:
|
||||
"yellow":
|
||||
return (
|
||||
"Running at %.0f t/s — pre-voicing will work for main characters and key scenes."
|
||||
(
|
||||
"Running at %.0f t/s — pre-voicing will work for main characters and key scenes."
|
||||
+ " Background NPCs may show base text until the queue catches up."
|
||||
) % tps
|
||||
)
|
||||
% tps
|
||||
)
|
||||
"red":
|
||||
return (
|
||||
"Running very slowly at %.0f t/s — we recommend leaving this off, but the choice is yours."
|
||||
) % tps
|
||||
("Running very slowly at %.0f t/s — we recommend leaving this off, but the choice is yours.")
|
||||
% tps
|
||||
)
|
||||
return ""
|
||||
|
||||
@@ -6,9 +6,11 @@ extends Control
|
||||
@onready var perception_label: Label = $MarginContainer/VBoxContainer/PerceptionLabel
|
||||
@onready var time_label: Label = $MarginContainer/VBoxContainer/TimeLabel
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
print("HUD: Initialized")
|
||||
|
||||
|
||||
# Update HUD from snapshot data
|
||||
func update_from_hud_data(data: Dictionary) -> void:
|
||||
# Update perception mode display
|
||||
@@ -27,6 +29,7 @@ func update_from_hud_data(data: Dictionary) -> void:
|
||||
else:
|
||||
time_label.text = prefix + ": " + data.time
|
||||
|
||||
|
||||
# Update player health (from player data, not hud_data)
|
||||
func update_health(health: int) -> void:
|
||||
health_label.text = UIStrings.get_text("hud.health") + ": " + str(health)
|
||||
|
||||
@@ -20,9 +20,4 @@ func _draw() -> void:
|
||||
if not _theme:
|
||||
return
|
||||
var y: float = _theme.sep_above
|
||||
draw_line(
|
||||
Vector2(0, y),
|
||||
Vector2(size.x, y),
|
||||
_theme.separator,
|
||||
1.0
|
||||
)
|
||||
draw_line(Vector2(0, y), Vector2(size.x, y), _theme.separator, 1.0)
|
||||
|
||||
@@ -25,5 +25,6 @@ func set_content(content: String, max_lines: int = -1) -> void:
|
||||
|
||||
func apply_implant_theme(t: ImplantTheme) -> void:
|
||||
add_theme_font_size_override("normal_font_size", t.font_small)
|
||||
add_theme_color_override("default_color",
|
||||
Color(t.text_primary.r, t.text_primary.g, t.text_primary.b, _text_alpha))
|
||||
add_theme_color_override(
|
||||
"default_color", Color(t.text_primary.r, t.text_primary.g, t.text_primary.b, _text_alpha)
|
||||
)
|
||||
|
||||
@@ -42,6 +42,7 @@ func _ready() -> void:
|
||||
visible = false
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _showing:
|
||||
_update_screen_position()
|
||||
@@ -113,14 +114,21 @@ func _rebuild_labels() -> void:
|
||||
var lbl := Label.new()
|
||||
lbl.text = " %s " % verb.get("label", "")
|
||||
lbl.add_theme_font_size_override("font_size", 14)
|
||||
lbl.add_theme_color_override("font_color", INSERT_FG if i == _selected_index else INSERT_DIM)
|
||||
lbl.add_theme_color_override(
|
||||
"font_color", INSERT_FG if i == _selected_index else INSERT_DIM
|
||||
)
|
||||
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
lbl.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
lbl.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
var idx := i
|
||||
lbl.gui_input.connect(func(event: InputEvent):
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
_select_and_interact(idx)
|
||||
lbl.gui_input.connect(
|
||||
func(event: InputEvent):
|
||||
if (
|
||||
event is InputEventMouseButton
|
||||
and event.pressed
|
||||
and event.button_index == MOUSE_BUTTON_LEFT
|
||||
):
|
||||
_select_and_interact(idx)
|
||||
)
|
||||
lbl.mouse_entered.connect(func(): _hover_index(idx))
|
||||
lbl.mouse_exited.connect(func(): _unhover_index(idx))
|
||||
@@ -155,7 +163,9 @@ func _update_screen_position() -> void:
|
||||
return
|
||||
var viewport_size := get_viewport_rect().size
|
||||
var cam_center := camera.get_screen_center_position()
|
||||
var zoom: Vector2 = camera.zoom if camera.zoom.length_squared() > 0.01 else Constants.CAMERA_DEFAULT_ZOOM
|
||||
var zoom: Vector2 = (
|
||||
camera.zoom if camera.zoom.length_squared() > 0.01 else Constants.CAMERA_DEFAULT_ZOOM
|
||||
)
|
||||
var world_px := _entity_world_pos * Constants.TILE_SIZE
|
||||
var screen_pos := (world_px - cam_center) * zoom + viewport_size / 2.0
|
||||
# Anchor above the entity, centered horizontally
|
||||
@@ -184,17 +194,19 @@ func _hide() -> void:
|
||||
_active_tween = create_tween()
|
||||
_active_tween.tween_property(self, "modulate:a", 0.0, FADE_OUT)
|
||||
# Guard: tween callback from previous _hide() may fire after labels already freed
|
||||
_active_tween.tween_callback(func():
|
||||
visible = false
|
||||
for lbl in _verb_labels:
|
||||
if is_instance_valid(lbl):
|
||||
lbl.queue_free()
|
||||
_verb_labels.clear()
|
||||
_active_tween.tween_callback(
|
||||
func():
|
||||
visible = false
|
||||
for lbl in _verb_labels:
|
||||
if is_instance_valid(lbl):
|
||||
lbl.queue_free()
|
||||
_verb_labels.clear()
|
||||
)
|
||||
|
||||
|
||||
# -- Lazy sync for getters (tests may set GameState without calling update) --
|
||||
|
||||
|
||||
func _ensure_synced() -> void:
|
||||
if _current_target_id == -1 and not GameState.nearby_interactions.is_empty():
|
||||
update_from_state()
|
||||
@@ -202,6 +214,7 @@ func _ensure_synced() -> void:
|
||||
|
||||
# -- Public API (QA test contract) ------------------------------------------
|
||||
|
||||
|
||||
func get_visible_verb_count() -> int:
|
||||
_ensure_synced()
|
||||
return _verb_items.size()
|
||||
@@ -254,7 +267,8 @@ func _update_label_colors() -> void:
|
||||
for i in range(_verb_labels.size()):
|
||||
if is_instance_valid(_verb_labels[i]):
|
||||
_verb_labels[i].add_theme_color_override(
|
||||
"font_color", INSERT_FG if i == _selected_index else INSERT_DIM)
|
||||
"font_color", INSERT_FG if i == _selected_index else INSERT_DIM
|
||||
)
|
||||
|
||||
|
||||
func set_insert_active(active: bool) -> void:
|
||||
|
||||
@@ -19,11 +19,13 @@ var _insert_active: bool = true
|
||||
|
||||
@onready var prompt_label: Label = $MarginContainer/PromptLabel
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
modulate.a = 0.0
|
||||
visible = false
|
||||
_is_showing = false
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
# OQ-07: insert off means no verb labels (z-layer 6 insert overlay suppressed)
|
||||
if not _insert_active:
|
||||
@@ -36,6 +38,7 @@ func _process(_delta: float) -> void:
|
||||
elif _is_showing:
|
||||
_hide_prompt()
|
||||
|
||||
|
||||
func _show_prompt(interaction: Dictionary) -> void:
|
||||
var target_id: int = interaction.get("entity_id", -1)
|
||||
var verbs: Array = interaction.get("verbs", [])
|
||||
@@ -61,6 +64,7 @@ func _show_prompt(interaction: Dictionary) -> void:
|
||||
_active_tween = create_tween()
|
||||
_active_tween.tween_property(self, "modulate:a", 1.0, FADE_IN)
|
||||
|
||||
|
||||
func _hide_prompt() -> void:
|
||||
if not _is_showing:
|
||||
return
|
||||
@@ -72,10 +76,12 @@ func _hide_prompt() -> void:
|
||||
_active_tween.tween_property(self, "modulate:a", 0.0, FADE_OUT)
|
||||
_active_tween.tween_callback(func(): visible = false)
|
||||
|
||||
|
||||
## Returns the current interaction target entity ID, or -1 if no interaction.
|
||||
func get_interaction_target() -> int:
|
||||
return _current_target_id
|
||||
|
||||
|
||||
## OQ-07 (#522): insert off hides prompt (diegetic: no insert data on z-layer 6).
|
||||
## Cursor shape changes still fire on cursor_renderer.gd.
|
||||
func set_insert_active(active: bool) -> void:
|
||||
|
||||
@@ -67,10 +67,7 @@ func _draw() -> void:
|
||||
|
||||
var col: int = slot_idx % GRID_COLS
|
||||
var row: int = slot_idx / GRID_COLS
|
||||
var pos := Vector2(
|
||||
col * (SLOT_SIZE + SLOT_GAP),
|
||||
row * (SLOT_SIZE + SLOT_GAP)
|
||||
)
|
||||
var pos := Vector2(col * (SLOT_SIZE + SLOT_GAP), row * (SLOT_SIZE + SLOT_GAP))
|
||||
var rect := Rect2(pos, Vector2(SLOT_SIZE, SLOT_SIZE))
|
||||
|
||||
# Slot background
|
||||
@@ -89,13 +86,22 @@ func _draw() -> void:
|
||||
var font := ThemeDB.fallback_font
|
||||
var font_size := 11
|
||||
var text_size := font.get_string_size(item_name, HORIZONTAL_ALIGNMENT_CENTER, -1, font_size)
|
||||
var text_pos := pos + Vector2((SLOT_SIZE - text_size.x) / 2.0, SLOT_SIZE / 2.0 + text_size.y / 4.0)
|
||||
var text_pos := (
|
||||
pos + Vector2((SLOT_SIZE - text_size.x) / 2.0, SLOT_SIZE / 2.0 + text_size.y / 4.0)
|
||||
)
|
||||
draw_string(font, text_pos, item_name, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, SLOT_TEXT)
|
||||
|
||||
# Hotkey number (top-left corner)
|
||||
var hotkey := str(slot_idx + 1)
|
||||
draw_string(font, pos + Vector2(3, HOTKEY_SIZE + 2), hotkey,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, HOTKEY_SIZE, SLOT_TEXT_DIM)
|
||||
draw_string(
|
||||
font,
|
||||
pos + Vector2(3, HOTKEY_SIZE + 2),
|
||||
hotkey,
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
HOTKEY_SIZE,
|
||||
SLOT_TEXT_DIM
|
||||
)
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
@@ -125,6 +131,7 @@ func _select_slot(slot_idx: int) -> void:
|
||||
|
||||
# -- Public API ---------------------------------------------------------------
|
||||
|
||||
|
||||
func get_slot_count() -> int:
|
||||
return _slots.size()
|
||||
|
||||
|
||||
+19
-12
@@ -29,8 +29,9 @@ var _last_rendered_tick: int = -1
|
||||
@onready var panel: PanelContainer = $PanelContainer
|
||||
@onready var title_label: Label = $PanelContainer/MarginContainer/VBoxContainer/TitleLabel
|
||||
@onready var scroll: ScrollContainer = $PanelContainer/MarginContainer/VBoxContainer/ScrollContainer
|
||||
@onready var entries_container: VBoxContainer = \
|
||||
$PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/EntriesContainer
|
||||
@onready
|
||||
var entries_container: VBoxContainer = $PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/EntriesContainer
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
@@ -118,12 +119,16 @@ func _add_entity_entry(entity: Dictionary) -> void:
|
||||
header_rtl.add_theme_font_size_override("normal_font_size", 14)
|
||||
|
||||
var rel_color: Color = Constants.color_for_relationship(relationship)
|
||||
var rel_label: String = UIStrings.get_text("relationship_states.%s.label" % relationship.to_lower())
|
||||
var rel_label: String = UIStrings.get_text(
|
||||
"relationship_states.%s.label" % relationship.to_lower()
|
||||
)
|
||||
if rel_label == "relationship_states.%s.label" % relationship.to_lower():
|
||||
rel_label = relationship # fallback if key missing
|
||||
|
||||
var state_color: Color = _state_color(state)
|
||||
var conf_label: String = UIStrings.get_text("knowledge_panel.confidence_%s" % confidence.to_lower())
|
||||
var conf_label: String = UIStrings.get_text(
|
||||
"knowledge_panel.confidence_%s" % confidence.to_lower()
|
||||
)
|
||||
var src_label: String = _resolve_source_label(source)
|
||||
|
||||
# Build BBCode:
|
||||
@@ -133,13 +138,15 @@ func _add_entity_entry(entity: Dictionary) -> void:
|
||||
|
||||
var header_bbcode: String
|
||||
if state == "Contradicted":
|
||||
header_bbcode = "[color=#%s][b][s]%s[/s][/b][/color] [color=#%s]%s[/color]" % [
|
||||
name_hex, name_str, rel_hex, rel_label
|
||||
]
|
||||
header_bbcode = (
|
||||
"[color=#%s][b][s]%s[/s][/b][/color] [color=#%s]%s[/color]"
|
||||
% [name_hex, name_str, rel_hex, rel_label]
|
||||
)
|
||||
else:
|
||||
header_bbcode = "[color=#%s][b]%s[/b][/color] [color=#%s]%s[/color]" % [
|
||||
name_hex, name_str, rel_hex, rel_label
|
||||
]
|
||||
header_bbcode = (
|
||||
"[color=#%s][b]%s[/b][/color] [color=#%s]%s[/color]"
|
||||
% [name_hex, name_str, rel_hex, rel_label]
|
||||
)
|
||||
header_rtl.text = header_bbcode
|
||||
entries_container.add_child(header_rtl)
|
||||
|
||||
@@ -198,9 +205,9 @@ func _entity_name_for_id(entity_id: int) -> String:
|
||||
func _state_color(state: String) -> Color:
|
||||
match state:
|
||||
"Contradicted":
|
||||
return Constants.ENTITY_COLOR_POI # amber — THE FRIEND arc
|
||||
return Constants.ENTITY_COLOR_POI # amber — THE FRIEND arc
|
||||
"Stale":
|
||||
return Constants.IMPLANT_TEXT_DIM # dimmed
|
||||
return Constants.IMPLANT_TEXT_DIM # dimmed
|
||||
_:
|
||||
return Constants.INSERT_COLOR_TEXT # active — normal
|
||||
|
||||
|
||||
+11
-4
@@ -156,8 +156,15 @@ static func _format_save_entry(save: Dictionary) -> String:
|
||||
if parts.size() >= 2 and parts[0].length() == 8 and parts[1].length() == 6:
|
||||
var d := parts[0]
|
||||
var t := parts[1]
|
||||
return "%s-%s-%s %s:%s:%s" % [
|
||||
d.substr(0, 4), d.substr(4, 2), d.substr(6, 2),
|
||||
t.substr(0, 2), t.substr(2, 2), t.substr(4, 2),
|
||||
]
|
||||
return (
|
||||
"%s-%s-%s %s:%s:%s"
|
||||
% [
|
||||
d.substr(0, 4),
|
||||
d.substr(4, 2),
|
||||
d.substr(6, 2),
|
||||
t.substr(0, 2),
|
||||
t.substr(2, 2),
|
||||
t.substr(4, 2),
|
||||
]
|
||||
)
|
||||
return game_id
|
||||
|
||||
+34
-14
@@ -29,27 +29,31 @@ const CARDINAL_TICK_LEN: float = 4.0
|
||||
|
||||
# Colors — insert palette from Constants, tuned for the circular minimap frame
|
||||
const COLOR_BG: Color = Color(0.04, 0.07, 0.12, 0.82)
|
||||
const COLOR_FRAME: Color = Color(0.784, 0.816, 0.878, 0.55) # INSERT_COLOR_TEXT at reduced alpha
|
||||
const COLOR_NORTH: Color = Color(0.784, 0.816, 0.878, 0.9) # Brighter for N tick
|
||||
const COLOR_FRAME: Color = Color(0.784, 0.816, 0.878, 0.55) # INSERT_COLOR_TEXT at reduced alpha
|
||||
const COLOR_NORTH: Color = Color(0.784, 0.816, 0.878, 0.9) # Brighter for N tick
|
||||
const COLOR_CARDINAL: Color = Color(0.784, 0.816, 0.878, 0.4) # Dimmer E/S/W ticks
|
||||
const COLOR_PLAYER: Color = Constants.ENTITY_COLOR_PLAYER
|
||||
|
||||
var _insert_active: bool = true
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
set_process(true)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _insert_active:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
## Called from main.gd when GameState.insert_active changes.
|
||||
## Hides the minimap overlay when the neural insert is inactive.
|
||||
func set_insert_active(active: bool) -> void:
|
||||
_insert_active = active
|
||||
visible = active
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var sz: Vector2 = get_rect().size
|
||||
var center := sz / 2.0
|
||||
@@ -68,8 +72,16 @@ func _draw() -> void:
|
||||
# --- Player dot at center ---
|
||||
draw_circle(center, PLAYER_DOT_RADIUS, COLOR_PLAYER)
|
||||
# Soft bloom ring
|
||||
draw_arc(center, PLAYER_DOT_RADIUS + 1.5, 0.0, TAU, 32,
|
||||
Color(COLOR_PLAYER.r, COLOR_PLAYER.g, COLOR_PLAYER.b, 0.22), 1.0, true)
|
||||
draw_arc(
|
||||
center,
|
||||
PLAYER_DOT_RADIUS + 1.5,
|
||||
0.0,
|
||||
TAU,
|
||||
32,
|
||||
Color(COLOR_PLAYER.r, COLOR_PLAYER.g, COLOR_PLAYER.b, 0.22),
|
||||
1.0,
|
||||
true
|
||||
)
|
||||
|
||||
# --- POIs ---
|
||||
var pois: Array = GameState.discovered_pois
|
||||
@@ -118,7 +130,9 @@ func _draw_cardinal_ticks(center: Vector2, outer_r: float) -> void:
|
||||
draw_line(
|
||||
center + n_dir * (outer_r - NORTH_TICK_LEN),
|
||||
center + n_dir * outer_r,
|
||||
COLOR_NORTH, FRAME_WIDTH + 0.5, true
|
||||
COLOR_NORTH,
|
||||
FRAME_WIDTH + 0.5,
|
||||
true
|
||||
)
|
||||
# East (PI/2), South (PI), West (3PI/2) — shorter, dimmer
|
||||
for angle in [PI / 2.0, PI, 3.0 * PI / 2.0]:
|
||||
@@ -126,7 +140,9 @@ func _draw_cardinal_ticks(center: Vector2, outer_r: float) -> void:
|
||||
draw_line(
|
||||
center + dir * (outer_r - CARDINAL_TICK_LEN),
|
||||
center + dir * outer_r,
|
||||
COLOR_CARDINAL, FRAME_WIDTH, true
|
||||
COLOR_CARDINAL,
|
||||
FRAME_WIDTH,
|
||||
true
|
||||
)
|
||||
|
||||
|
||||
@@ -136,10 +152,14 @@ func _draw_poi_shape(pos: Vector2, color: Color, category: String) -> void:
|
||||
# Diamond for danger
|
||||
var s: float = POI_DOT_RADIUS + 1.0
|
||||
draw_polygon(
|
||||
PackedVector2Array([
|
||||
pos + Vector2(0.0, -s), pos + Vector2(s, 0.0),
|
||||
pos + Vector2(0.0, s), pos + Vector2(-s, 0.0)
|
||||
]),
|
||||
PackedVector2Array(
|
||||
[
|
||||
pos + Vector2(0.0, -s),
|
||||
pos + Vector2(s, 0.0),
|
||||
pos + Vector2(0.0, s),
|
||||
pos + Vector2(-s, 0.0)
|
||||
]
|
||||
),
|
||||
PackedColorArray([color, color, color, color])
|
||||
)
|
||||
"evidence", "note", "clue":
|
||||
@@ -163,10 +183,10 @@ func _draw_border_arrow(tip: Vector2, dir: Vector2, color: Color) -> void:
|
||||
func _category_color(category: String) -> Color:
|
||||
match category.to_lower():
|
||||
"danger", "threat", "hostile":
|
||||
return Constants.ENTITY_COLOR_HOSTILE # #d45d5d — red
|
||||
return Constants.ENTITY_COLOR_HOSTILE # #d45d5d — red
|
||||
"evidence", "note", "clue":
|
||||
return Constants.ENTITY_COLOR_POI # #e8c547 — amber
|
||||
return Constants.ENTITY_COLOR_POI # #e8c547 — amber
|
||||
"contact", "npc", "person":
|
||||
return Constants.ENTITY_COLOR_UNKNOWN # #4a9ebb — teal
|
||||
return Constants.ENTITY_COLOR_UNKNOWN # #4a9ebb — teal
|
||||
_:
|
||||
return Constants.INSERT_COLOR_TEXT # #c8d0e0 — white-blue
|
||||
return Constants.INSERT_COLOR_TEXT # #c8d0e0 — white-blue
|
||||
|
||||
@@ -13,28 +13,30 @@ extends Control
|
||||
# is_urgent=true → opacity 1.0 and elevated colour variant (bloom deferred).
|
||||
|
||||
const MAX_VISIBLE: int = 3
|
||||
const MAX_QUEUE: int = 5
|
||||
const MAX_QUEUE: int = 5
|
||||
|
||||
const STAGGER_SEC: float = 0.15
|
||||
const FADE_IN_SEC: float = 0.3
|
||||
const FADE_OUT_SEC: float = 0.5
|
||||
const MIN_DURATION: float = FADE_IN_SEC + 0.1 # clamp: line must survive its own fade-in
|
||||
const STAGGER_SEC: float = 0.15
|
||||
const FADE_IN_SEC: float = 0.3
|
||||
const FADE_OUT_SEC: float = 0.5
|
||||
const MIN_DURATION: float = FADE_IN_SEC + 0.1 # clamp: line must survive its own fade-in
|
||||
|
||||
# Lattice colour palette — keyed by lattice_profile passed from GameState at show time.
|
||||
# standard opacity = 0.85, urgent opacity = 1.0.
|
||||
# Source: Tyre architecture review, Sprint 14.
|
||||
const _LATTICE_COLORS: Dictionary = {
|
||||
"lattice_augmented": { # detective
|
||||
"lattice_augmented":
|
||||
{ # detective
|
||||
"standard": Color("#d0d4e0"),
|
||||
"urgent": Color("#e0e8f8"),
|
||||
"urgent": Color("#e0e8f8"),
|
||||
},
|
||||
"lattice_baseline": { # smuggler
|
||||
"lattice_baseline":
|
||||
{ # smuggler
|
||||
"standard": Color("#d8d0c4"),
|
||||
"urgent": Color("#f0e4d4"),
|
||||
"urgent": Color("#f0e4d4"),
|
||||
},
|
||||
}
|
||||
const _FALLBACK_STANDARD: Color = Color("#c8d0e0")
|
||||
const _FALLBACK_URGENT: Color = Color("#e0e8f8")
|
||||
const _FALLBACK_URGENT: Color = Color("#e0e8f8")
|
||||
const _NOTIFICATION_COLOR: Color = Color("#8890a0") # #554: neutral system notification
|
||||
const _NOTIFICATION_DURATION: float = 2.5
|
||||
|
||||
@@ -67,7 +69,9 @@ func _process(delta: float) -> void:
|
||||
if next.get("is_notification", false):
|
||||
_show_notification_line(next.text)
|
||||
else:
|
||||
_show_line(next.text, next.duration, next.priority, next.is_urgent, next.lattice_profile)
|
||||
_show_line(
|
||||
next.text, next.duration, next.priority, next.is_urgent, next.lattice_profile
|
||||
)
|
||||
|
||||
|
||||
# Display a monologue line.
|
||||
@@ -86,20 +90,30 @@ func show_notification(text: String) -> void:
|
||||
_show_notification_line(text)
|
||||
else:
|
||||
var entry := {
|
||||
text = text, duration = _NOTIFICATION_DURATION, priority = 1,
|
||||
is_urgent = false, lattice_profile = "", is_notification = true
|
||||
text = text,
|
||||
duration = _NOTIFICATION_DURATION,
|
||||
priority = 1,
|
||||
is_urgent = false,
|
||||
lattice_profile = "",
|
||||
is_notification = true
|
||||
}
|
||||
if _queue.size() < MAX_QUEUE:
|
||||
_queue.append(entry)
|
||||
_queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a.priority > b.priority)
|
||||
_queue.sort_custom(
|
||||
func(a: Dictionary, b: Dictionary) -> bool: return a.priority > b.priority
|
||||
)
|
||||
else:
|
||||
var lowest := _lowest_priority_idx()
|
||||
if 1 >= _queue[lowest].priority:
|
||||
_queue[lowest] = entry
|
||||
_queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a.priority > b.priority)
|
||||
_queue.sort_custom(
|
||||
func(a: Dictionary, b: Dictionary) -> bool: return a.priority > b.priority
|
||||
)
|
||||
|
||||
|
||||
func show_monologue(text: String, duration: float, priority: int = 2, is_urgent: bool = false) -> void:
|
||||
func show_monologue(
|
||||
text: String, duration: float, priority: int = 2, is_urgent: bool = false
|
||||
) -> void:
|
||||
if text.is_empty():
|
||||
return
|
||||
var profile := GameState.lattice_profile
|
||||
@@ -114,11 +128,12 @@ func show_monologue(text: String, duration: float, priority: int = 2, is_urgent:
|
||||
# Internal
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
func _show_notification_line(text: String) -> void:
|
||||
var container := MarginContainer.new()
|
||||
container.add_theme_constant_override("margin_left", 4)
|
||||
container.add_theme_constant_override("margin_right", 4)
|
||||
container.add_theme_constant_override("margin_top", 2)
|
||||
container.add_theme_constant_override("margin_left", 4)
|
||||
container.add_theme_constant_override("margin_right", 4)
|
||||
container.add_theme_constant_override("margin_top", 2)
|
||||
container.add_theme_constant_override("margin_bottom", 2)
|
||||
var label := RichTextLabel.new()
|
||||
label.bbcode_enabled = true
|
||||
@@ -130,10 +145,10 @@ func _show_notification_line(text: String) -> void:
|
||||
container.add_child(label)
|
||||
_vbox.add_child(container)
|
||||
var slot := {
|
||||
node = container,
|
||||
node = container,
|
||||
expire_timer = _NOTIFICATION_DURATION,
|
||||
priority = 1,
|
||||
tween = null,
|
||||
priority = 1,
|
||||
tween = null,
|
||||
}
|
||||
_visible.append(slot)
|
||||
_next_fade_in_msec = float(Time.get_ticks_msec()) + STAGGER_SEC * 1000.0
|
||||
@@ -143,15 +158,17 @@ func _show_notification_line(text: String) -> void:
|
||||
tween.tween_property(container, "modulate:a", 0.85, FADE_IN_SEC)
|
||||
|
||||
|
||||
func _show_line(text: String, duration: float, priority: int, is_urgent: bool, lattice_profile: String) -> void:
|
||||
func _show_line(
|
||||
text: String, duration: float, priority: int, is_urgent: bool, lattice_profile: String
|
||||
) -> void:
|
||||
var line_node := _build_line_node(text, is_urgent, lattice_profile)
|
||||
_vbox.add_child(line_node)
|
||||
|
||||
var slot := {
|
||||
node = line_node,
|
||||
node = line_node,
|
||||
expire_timer = maxf(duration, MIN_DURATION), # clamp: survives own fade-in
|
||||
priority = priority,
|
||||
tween = null,
|
||||
priority = priority,
|
||||
tween = null,
|
||||
}
|
||||
_visible.append(slot)
|
||||
_next_fade_in_msec = float(Time.get_ticks_msec()) + STAGGER_SEC * 1000.0
|
||||
@@ -174,22 +191,36 @@ func _retire_slot(slot: Dictionary) -> void:
|
||||
tween.tween_callback(node.queue_free)
|
||||
|
||||
|
||||
func _enqueue(text: String, duration: float, priority: int, is_urgent: bool, lattice_profile: String) -> void:
|
||||
func _enqueue(
|
||||
text: String, duration: float, priority: int, is_urgent: bool, lattice_profile: String
|
||||
) -> void:
|
||||
if _queue.size() < MAX_QUEUE:
|
||||
_queue.append({
|
||||
text = text, duration = duration, priority = priority,
|
||||
is_urgent = is_urgent, lattice_profile = lattice_profile
|
||||
})
|
||||
_queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a.priority > b.priority)
|
||||
_queue.append(
|
||||
{
|
||||
text = text,
|
||||
duration = duration,
|
||||
priority = priority,
|
||||
is_urgent = is_urgent,
|
||||
lattice_profile = lattice_profile
|
||||
}
|
||||
)
|
||||
_queue.sort_custom(
|
||||
func(a: Dictionary, b: Dictionary) -> bool: return a.priority > b.priority
|
||||
)
|
||||
else:
|
||||
# >= tiebreak: newest replaces oldest at equal priority (FIFO for equal ranks)
|
||||
var lowest := _lowest_priority_idx()
|
||||
if priority >= _queue[lowest].priority:
|
||||
_queue[lowest] = {
|
||||
text = text, duration = duration, priority = priority,
|
||||
is_urgent = is_urgent, lattice_profile = lattice_profile
|
||||
text = text,
|
||||
duration = duration,
|
||||
priority = priority,
|
||||
is_urgent = is_urgent,
|
||||
lattice_profile = lattice_profile
|
||||
}
|
||||
_queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a.priority > b.priority)
|
||||
_queue.sort_custom(
|
||||
func(a: Dictionary, b: Dictionary) -> bool: return a.priority > b.priority
|
||||
)
|
||||
# else: incoming line is strictly lower priority — silently drop; no sort needed
|
||||
|
||||
|
||||
@@ -203,13 +234,16 @@ func _lowest_priority_idx() -> int:
|
||||
|
||||
func _build_line_node(text: String, is_urgent: bool, lattice_profile: String) -> Control:
|
||||
var palette: Dictionary = _LATTICE_COLORS.get(lattice_profile, {})
|
||||
var color: Color = palette.get("urgent", _FALLBACK_URGENT) if is_urgent \
|
||||
var color: Color = (
|
||||
palette.get("urgent", _FALLBACK_URGENT)
|
||||
if is_urgent
|
||||
else palette.get("standard", _FALLBACK_STANDARD)
|
||||
)
|
||||
|
||||
var container := MarginContainer.new()
|
||||
container.add_theme_constant_override("margin_left", 4)
|
||||
container.add_theme_constant_override("margin_right", 4)
|
||||
container.add_theme_constant_override("margin_top", 2)
|
||||
container.add_theme_constant_override("margin_left", 4)
|
||||
container.add_theme_constant_override("margin_right", 4)
|
||||
container.add_theme_constant_override("margin_top", 2)
|
||||
container.add_theme_constant_override("margin_bottom", 2)
|
||||
|
||||
var label := RichTextLabel.new()
|
||||
|
||||
@@ -8,33 +8,33 @@ extends Control
|
||||
|
||||
signal closed
|
||||
signal debug_console_toggled(enabled: bool) # #581: debug console enabled/disabled
|
||||
signal ai_dialogue_toggled(enabled: bool) # #646: AI-Enhanced Dialogue enabled/disabled
|
||||
signal ai_dialogue_toggled(enabled: bool) # #646: AI-Enhanced Dialogue enabled/disabled
|
||||
|
||||
const BG_COLOR := Color(0.05, 0.05, 0.08, 0.90)
|
||||
const BORDER_COLOR := Color("#4a9ebb")
|
||||
const TEXT_COLOR := Color(0.878, 0.969, 0.98, 1)
|
||||
const TITLE_COLOR := Color("#4a9ebb")
|
||||
const STATUS_GREEN := Color("#6bc9a6")
|
||||
const STATUS_GREEN := Color("#6bc9a6")
|
||||
const STATUS_YELLOW := Color("#e8c547")
|
||||
const STATUS_RED := Color("#c84040")
|
||||
const STATUS_RED := Color("#c84040")
|
||||
const FONT_SIZE := 14
|
||||
const FONT_SIZE_SMALL := 11
|
||||
|
||||
## Shared client preferences file — avoids compile-time dependency on DebugConsole for path.
|
||||
const SETTINGS_CFG_PATH := "user://settings.cfg"
|
||||
|
||||
const BOX_WIDTH := 460
|
||||
const BOX_WIDTH := 460
|
||||
const BOX_HEIGHT := 500 # +36 debug console, +88 AI dialogue section
|
||||
const PADDING := 20
|
||||
const PADDING := 20
|
||||
const ROW_HEIGHT := 36
|
||||
|
||||
# Bus display labels → bus name strings (must match AudioManager BUS_* constants)
|
||||
const BUS_ROWS: Array = [
|
||||
["Music", "Music"],
|
||||
["Ambient", "Ambient"],
|
||||
["World SFX", "WorldSFX"],
|
||||
["Music", "Music"],
|
||||
["Ambient", "Ambient"],
|
||||
["World SFX", "WorldSFX"],
|
||||
["Player Actions", "PlayerActions"],
|
||||
["UI Sounds", "UISounds"],
|
||||
["UI Sounds", "UISounds"],
|
||||
]
|
||||
|
||||
var _active: bool = false
|
||||
@@ -77,10 +77,7 @@ func is_open() -> bool:
|
||||
|
||||
func _build_ui() -> void:
|
||||
var vp_size := get_viewport_rect().size
|
||||
var box_pos := Vector2(
|
||||
(vp_size.x - BOX_WIDTH) / 2.0,
|
||||
(vp_size.y - BOX_HEIGHT) / 2.0
|
||||
)
|
||||
var box_pos := Vector2((vp_size.x - BOX_WIDTH) / 2.0, (vp_size.y - BOX_HEIGHT) / 2.0)
|
||||
|
||||
_container = VBoxContainer.new()
|
||||
_container.position = box_pos + Vector2(PADDING, PADDING + 28)
|
||||
@@ -121,9 +118,10 @@ func _build_ui() -> void:
|
||||
db_label.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
hbox.add_child(db_label)
|
||||
|
||||
slider.value_changed.connect(func(value: float) -> void:
|
||||
AudioManager.set_volume(bus_name, value)
|
||||
db_label.text = _format_db(value)
|
||||
slider.value_changed.connect(
|
||||
func(value: float) -> void:
|
||||
AudioManager.set_volume(bus_name, value)
|
||||
db_label.text = _format_db(value)
|
||||
)
|
||||
|
||||
# #581: Debug Console toggle
|
||||
@@ -148,10 +146,10 @@ func _build_ui() -> void:
|
||||
var cfg := ConfigFile.new()
|
||||
debug_check.button_pressed = true
|
||||
if cfg.load(DebugConsole.PREFS_PATH) == OK:
|
||||
debug_check.button_pressed = cfg.get_value(DebugConsole.PREFS_SECTION, DebugConsole.PREFS_KEY_ENABLED, true)
|
||||
debug_check.toggled.connect(func(enabled: bool) -> void:
|
||||
debug_console_toggled.emit(enabled)
|
||||
)
|
||||
debug_check.button_pressed = cfg.get_value(
|
||||
DebugConsole.PREFS_SECTION, DebugConsole.PREFS_KEY_ENABLED, true
|
||||
)
|
||||
debug_check.toggled.connect(func(enabled: bool) -> void: debug_console_toggled.emit(enabled))
|
||||
debug_hbox.add_child(debug_check)
|
||||
|
||||
# #646: AI Dialogue section divider
|
||||
@@ -223,7 +221,8 @@ func _build_ui() -> void:
|
||||
|
||||
# Status message label — only shown when non-empty
|
||||
var status_msg: String = HardwareDetector.status_message(
|
||||
ram_class, tpt_class, ram_result["free_mb"], tpt_tps)
|
||||
ram_class, tpt_class, ram_result["free_mb"], tpt_tps
|
||||
)
|
||||
var ai_status_label := Label.new()
|
||||
ai_status_label.text = status_msg
|
||||
ai_status_label.add_theme_font_size_override("font_size", FONT_SIZE_SMALL)
|
||||
@@ -247,16 +246,22 @@ func _build_ui() -> void:
|
||||
_ai_battery_warning_label.visible = _ai_inference_suspended
|
||||
_container.add_child(_ai_battery_warning_label)
|
||||
|
||||
_ai_check_node.toggled.connect(func(enabled: bool) -> void:
|
||||
GameState.ai_enhanced_dialogue_enabled = enabled
|
||||
_save_ai_pref(enabled)
|
||||
if SimBridge.state == SimBridge.ConnectionState.CONNECTED:
|
||||
SimBridge.send_input({
|
||||
"action": InputMapper.Action.CHANGE_SETTINGS,
|
||||
"action_data": {"ai_enhanced_dialogue": enabled},
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
})
|
||||
ai_dialogue_toggled.emit(enabled)
|
||||
_ai_check_node.toggled.connect(
|
||||
func(enabled: bool) -> void:
|
||||
GameState.ai_enhanced_dialogue_enabled = enabled
|
||||
_save_ai_pref(enabled)
|
||||
if SimBridge.state == SimBridge.ConnectionState.CONNECTED:
|
||||
(
|
||||
SimBridge
|
||||
. send_input(
|
||||
{
|
||||
"action": InputMapper.Action.CHANGE_SETTINGS,
|
||||
"action_data": {"ai_enhanced_dialogue": enabled},
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
}
|
||||
)
|
||||
)
|
||||
ai_dialogue_toggled.emit(enabled)
|
||||
)
|
||||
|
||||
# Spacer
|
||||
@@ -284,12 +289,13 @@ func _destroy_ui() -> void:
|
||||
if _container:
|
||||
_container.queue_free()
|
||||
_container = null
|
||||
_ai_check_node = null # freed with _container
|
||||
_ai_check_node = null # freed with _container
|
||||
_ai_battery_warning_label = null
|
||||
|
||||
|
||||
# -- #646: AI Dialogue testable API -------------------------------------------
|
||||
|
||||
|
||||
## Returns the canonical label text for the AI-Enhanced Dialogue toggle (D-138).
|
||||
## Reads from UIStrings yaml (settings.ai_dialogue_toggle) for localization support.
|
||||
func get_ai_dialogue_label_text() -> String:
|
||||
@@ -354,8 +360,7 @@ func _draw() -> void:
|
||||
|
||||
# Dialog box
|
||||
var box_pos := Vector2(
|
||||
(viewport_size.x - BOX_WIDTH) / 2.0,
|
||||
(viewport_size.y - BOX_HEIGHT) / 2.0
|
||||
(viewport_size.x - BOX_WIDTH) / 2.0, (viewport_size.y - BOX_HEIGHT) / 2.0
|
||||
)
|
||||
var box_rect := Rect2(box_pos, Vector2(BOX_WIDTH, BOX_HEIGHT))
|
||||
draw_rect(box_rect, Color(0.08, 0.08, 0.12, 0.95))
|
||||
@@ -363,10 +368,15 @@ func _draw() -> void:
|
||||
|
||||
# Title
|
||||
var font := ThemeDB.fallback_font
|
||||
draw_string(font,
|
||||
draw_string(
|
||||
font,
|
||||
box_pos + Vector2(PADDING, PADDING + 18),
|
||||
"SETTINGS",
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE + 2, TITLE_COLOR)
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
FONT_SIZE + 2,
|
||||
TITLE_COLOR
|
||||
)
|
||||
|
||||
|
||||
func _on_quit_to_menu() -> void:
|
||||
|
||||
@@ -5,10 +5,10 @@ extends Control
|
||||
## Lives on UILayer (z-layer 7).
|
||||
|
||||
const STANCE_COLORS := {
|
||||
"Sprint": Color("#d45d5d"), # Red — fast, loud, dangerous
|
||||
"Walk": Color("#c8d0e0"), # Default — neutral white-blue
|
||||
"Sprint": Color("#d45d5d"), # Red — fast, loud, dangerous
|
||||
"Walk": Color("#c8d0e0"), # Default — neutral white-blue
|
||||
"Careful": Color("#6bc9a6"), # Green — quiet, observant
|
||||
"Crouch": Color("#e8c547"), # Amber — very quiet, slow
|
||||
"Crouch": Color("#e8c547"), # Amber — very quiet, slow
|
||||
}
|
||||
|
||||
const STANCE_DEFAULT_COLOR := Color("#c8d0e0")
|
||||
@@ -42,10 +42,19 @@ func _draw() -> void:
|
||||
|
||||
# Stance text
|
||||
var color: Color = STANCE_COLORS.get(_current_stance, STANCE_DEFAULT_COLOR)
|
||||
draw_string(font, PADDING + Vector2(0, text_size.y), text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, color)
|
||||
draw_string(
|
||||
font,
|
||||
PADDING + Vector2(0, text_size.y),
|
||||
text,
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
FONT_SIZE,
|
||||
color
|
||||
)
|
||||
|
||||
|
||||
# -- Public API ---------------------------------------------------------------
|
||||
|
||||
|
||||
func get_current_stance() -> String:
|
||||
return _current_stance
|
||||
|
||||
+80
-31
@@ -16,9 +16,9 @@ const DATA_PATH := "res://data/star_map_data.json"
|
||||
|
||||
# Layout
|
||||
const MAP_CENTER_FRACTION := Vector2(0.5, 0.5) # center of control
|
||||
const MIN_RING_RADIUS: float = 30.0 # innermost ring (hop 0 = gateway dot only)
|
||||
const RING_SPACING: float = 22.0 # pixels between hop rings
|
||||
const MAX_HOP_RINGS: int = 24 # max hop distance we render rings for
|
||||
const MIN_RING_RADIUS: float = 30.0 # innermost ring (hop 0 = gateway dot only)
|
||||
const RING_SPACING: float = 22.0 # pixels between hop rings
|
||||
const MAX_HOP_RINGS: int = 24 # max hop distance we render rings for
|
||||
|
||||
# Dot sizing
|
||||
const DOT_RADIUS_HUB: float = 4.5
|
||||
@@ -75,8 +75,8 @@ const SECTOR_ANGLE_CENTER: Dictionary = {
|
||||
"west_reach": PI,
|
||||
}
|
||||
const SECTOR_ANGLE_SPREAD: float = PI / 2.5 # each sector occupies ~72° of arc
|
||||
const CORE_ANGLE_SPREAD: float = TAU # core systems spread full circle
|
||||
const DEEP_FRONTIER_ANGLE_SPREAD: float = TAU # deep frontier wraps entire outer edge
|
||||
const CORE_ANGLE_SPREAD: float = TAU # core systems spread full circle
|
||||
const DEEP_FRONTIER_ANGLE_SPREAD: float = TAU # deep frontier wraps entire outer edge
|
||||
|
||||
# Pan/zoom
|
||||
const ZOOM_MIN: float = 0.3
|
||||
@@ -87,7 +87,7 @@ const ZOOM_STEP: float = 0.15
|
||||
var _nodes: Array = []
|
||||
var _edges: Array = []
|
||||
var _node_positions: Dictionary = {} # system_id -> Vector2 (screen coords relative to map center)
|
||||
var _node_lookup: Dictionary = {} # system_id -> node dict
|
||||
var _node_lookup: Dictionary = {} # system_id -> node dict
|
||||
var _selected_system: String = ""
|
||||
var _hovered_system: String = ""
|
||||
|
||||
@@ -99,8 +99,8 @@ var _pan_start_offset: Vector2 = Vector2.ZERO
|
||||
|
||||
var _data_loaded: bool = false
|
||||
var _insert_active: bool = true
|
||||
var _dirty: bool = true # redraw needed — set by state changes, cleared after _draw
|
||||
var _info_panel: ImplantPanel # D-169: component-based info panel
|
||||
var _dirty: bool = true # redraw needed — set by state changes, cleared after _draw
|
||||
var _info_panel: ImplantPanel # D-169: component-based info panel
|
||||
var _implant_theme: ImplantTheme
|
||||
|
||||
|
||||
@@ -170,6 +170,7 @@ func get_edge_count() -> int:
|
||||
# Data loading
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _load_data() -> void:
|
||||
if not FileAccess.file_exists(DATA_PATH):
|
||||
push_warning("StarMapRenderer: data file not found at %s" % DATA_PATH)
|
||||
@@ -195,6 +196,7 @@ func _load_data() -> void:
|
||||
# Layout — place systems on concentric rings by hop distance
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _compute_layout() -> void:
|
||||
_node_positions.clear()
|
||||
|
||||
@@ -260,7 +262,9 @@ func _compute_layout() -> void:
|
||||
var jitter: float = _system_hash(node["system_id"]) * 0.08
|
||||
angle += jitter
|
||||
# Slight radial variation to avoid perfect circles
|
||||
var r_var: float = radius + _system_hash(node["system_id"] + "r") * RING_SPACING * 0.3
|
||||
var r_var: float = (
|
||||
radius + _system_hash(node["system_id"] + "r") * RING_SPACING * 0.3
|
||||
)
|
||||
_node_positions[node["system_id"]] = Vector2(cos(angle), sin(angle)) * r_var
|
||||
|
||||
|
||||
@@ -275,13 +279,20 @@ func _sort_by_sector_angle(a: Dictionary, b: Dictionary) -> bool:
|
||||
func _sector_sort_key(node: Dictionary) -> float:
|
||||
var sector: String = node.get("geographic_sector", "unknown")
|
||||
match sector:
|
||||
"core": return 0.0
|
||||
"north_reach": return 1.0
|
||||
"east_reach": return 2.0
|
||||
"south_reach": return 3.0
|
||||
"west_reach": return 4.0
|
||||
"deep_frontier": return 5.0
|
||||
_: return 6.0 # gdlint:ignore = max-returns
|
||||
"core":
|
||||
return 0.0
|
||||
"north_reach":
|
||||
return 1.0
|
||||
"east_reach":
|
||||
return 2.0
|
||||
"south_reach":
|
||||
return 3.0
|
||||
"west_reach":
|
||||
return 4.0
|
||||
"deep_frontier":
|
||||
return 5.0
|
||||
_:
|
||||
return 6.0 # gdlint:ignore = max-returns
|
||||
|
||||
|
||||
## Deterministic float in [-1, 1] from a string key.
|
||||
@@ -294,6 +305,7 @@ func _system_hash(key: String) -> float:
|
||||
# Drawing
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if not _data_loaded:
|
||||
return
|
||||
@@ -357,8 +369,12 @@ func _draw_sector_labels(center: Vector2) -> void:
|
||||
var color: Color = SECTOR_COLORS.get(sector, COLOR_TEXT_DIM)
|
||||
var font := get_theme_default_font()
|
||||
var font_size: int = 10
|
||||
var text_size: Vector2 = font.get_string_size(label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size)
|
||||
draw_string(font, pos - text_size / 2.0, label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, color)
|
||||
var text_size: Vector2 = font.get_string_size(
|
||||
label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size
|
||||
)
|
||||
draw_string(
|
||||
font, pos - text_size / 2.0, label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, color
|
||||
)
|
||||
|
||||
|
||||
func _draw_systems(center: Vector2) -> void:
|
||||
@@ -383,7 +399,9 @@ func _draw_systems(center: Vector2) -> void:
|
||||
|
||||
# Hover highlight
|
||||
if sid == _hovered_system and sid != _selected_system:
|
||||
draw_arc(pos, radius + 3.0, 0.0, TAU, 16, Color(color.r, color.g, color.b, 0.4), 1.0, true)
|
||||
draw_arc(
|
||||
pos, radius + 3.0, 0.0, TAU, 16, Color(color.r, color.g, color.b, 0.4), 1.0, true
|
||||
)
|
||||
|
||||
draw_circle(pos, radius, color)
|
||||
|
||||
@@ -415,8 +433,15 @@ func _draw_selection(center: Vector2) -> void:
|
||||
if label.is_empty():
|
||||
label = _selected_system
|
||||
var font := get_theme_default_font()
|
||||
draw_string(font, pos + Vector2(SELECTION_RING_RADIUS + 4, 4), label,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 12, COLOR_SELECTION)
|
||||
draw_string(
|
||||
font,
|
||||
pos + Vector2(SELECTION_RING_RADIUS + 4, 4),
|
||||
label,
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
12,
|
||||
COLOR_SELECTION
|
||||
)
|
||||
|
||||
|
||||
## Rebuild the info panel with components for the selected system (D-169).
|
||||
@@ -462,9 +487,13 @@ func _rebuild_info_panel() -> void:
|
||||
elif not population.is_empty():
|
||||
stat_line_3 = "Population: " + population
|
||||
else:
|
||||
stat_line_3 = "%d aperture%s" % [
|
||||
int(node.get("aperture_count", 0)),
|
||||
"s" if int(node.get("aperture_count", 0)) != 1 else ""]
|
||||
stat_line_3 = (
|
||||
"%d aperture%s"
|
||||
% [
|
||||
int(node.get("aperture_count", 0)),
|
||||
"s" if int(node.get("aperture_count", 0)) != 1 else ""
|
||||
]
|
||||
)
|
||||
_info_panel.add_component(ImplantDataRow.new(stat_line_3))
|
||||
|
||||
# ── GTTR excerpt ─────────────────────────────────────────────────────────
|
||||
@@ -487,23 +516,43 @@ func _rebuild_info_panel() -> void:
|
||||
|
||||
func _draw_title() -> void:
|
||||
var font := get_theme_default_font()
|
||||
draw_string(font, Vector2(16, 28), "THE REACH — NAVIGATOR", HORIZONTAL_ALIGNMENT_LEFT, -1, 16, COLOR_TEXT)
|
||||
draw_string(font, Vector2(16, 44), "Concord Assembly Gate Network — %d Systems" % _nodes.size(),
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 10, COLOR_TEXT_DIM)
|
||||
draw_string(
|
||||
font,
|
||||
Vector2(16, 28),
|
||||
"THE REACH — NAVIGATOR",
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
16,
|
||||
COLOR_TEXT
|
||||
)
|
||||
draw_string(
|
||||
font,
|
||||
Vector2(16, 44),
|
||||
"Concord Assembly Gate Network — %d Systems" % _nodes.size(),
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
10,
|
||||
COLOR_TEXT_DIM
|
||||
)
|
||||
|
||||
|
||||
func _dot_radius(topology: String) -> float:
|
||||
match topology:
|
||||
"hub": return DOT_RADIUS_HUB
|
||||
"junction": return DOT_RADIUS_JUNCTION
|
||||
"dead_end": return DOT_RADIUS_DEAD_END
|
||||
_: return DOT_RADIUS_DEFAULT
|
||||
"hub":
|
||||
return DOT_RADIUS_HUB
|
||||
"junction":
|
||||
return DOT_RADIUS_JUNCTION
|
||||
"dead_end":
|
||||
return DOT_RADIUS_DEAD_END
|
||||
_:
|
||||
return DOT_RADIUS_DEFAULT
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Input — selection, pan, zoom
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton:
|
||||
var mb := event as InputEventMouseButton
|
||||
|
||||
+31
-10
@@ -15,10 +15,10 @@ const BORDER_COLOR := Color(0.10, 0.20, 0.26, 0.65)
|
||||
|
||||
# Day phase colors — station lighting cycle (D-031)
|
||||
const PHASE_COLORS := {
|
||||
"Morning": Color("#aed6dc"), # pale cyan-blue — early light
|
||||
"Afternoon": Color("#E0F7FA"), # bright cyan-white — full day
|
||||
"Evening": Color("#9EBFC4"), # dimmed — dusk transition
|
||||
"Night": Color("#4a7080"), # dark teal — station nightwatch
|
||||
"Morning": Color("#aed6dc"), # pale cyan-blue — early light
|
||||
"Afternoon": Color("#E0F7FA"), # bright cyan-white — full day
|
||||
"Evening": Color("#9EBFC4"), # dimmed — dusk transition
|
||||
"Night": Color("#4a7080"), # dark teal — station nightwatch
|
||||
}
|
||||
|
||||
var _time_str: String = "--:--"
|
||||
@@ -86,12 +86,33 @@ func _draw() -> void:
|
||||
var font := ThemeDB.fallback_font
|
||||
|
||||
# HH:MM (primary, full brightness)
|
||||
draw_string(font, Vector2(PADDING.x, PADDING.y + _time_size.y),
|
||||
_time_str, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE_TIME, Constants.IMPLANT_TEXT_COLOR)
|
||||
draw_string(
|
||||
font,
|
||||
Vector2(PADDING.x, PADDING.y + _time_size.y),
|
||||
_time_str,
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
FONT_SIZE_TIME,
|
||||
Constants.IMPLANT_TEXT_COLOR
|
||||
)
|
||||
|
||||
# Phase + day number (secondary, dimmed + phase-tinted)
|
||||
var meta_y := PADDING.y + _time_size.y + 3 + _meta_h
|
||||
draw_string(font, Vector2(PADDING.x, meta_y),
|
||||
_phase_str, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE_META, _phase_color)
|
||||
draw_string(font, Vector2(PADDING.x + _phase_size.x, meta_y),
|
||||
_day_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE_META, Constants.IMPLANT_TEXT_DIM)
|
||||
draw_string(
|
||||
font,
|
||||
Vector2(PADDING.x, meta_y),
|
||||
_phase_str,
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
FONT_SIZE_META,
|
||||
_phase_color
|
||||
)
|
||||
draw_string(
|
||||
font,
|
||||
Vector2(PADDING.x + _phase_size.x, meta_y),
|
||||
_day_text,
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
FONT_SIZE_META,
|
||||
Constants.IMPLANT_TEXT_DIM
|
||||
)
|
||||
|
||||
@@ -144,20 +144,30 @@ func _activate_insert() -> void:
|
||||
# if already paused (e.g. Gauntlet interlude), server ignores duplicate.
|
||||
if not _insert_active:
|
||||
_insert_active = true
|
||||
SimBridge.send_input({
|
||||
"action": InputMapper.Action.PAUSE,
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
})
|
||||
(
|
||||
SimBridge
|
||||
. send_input(
|
||||
{
|
||||
"action": InputMapper.Action.PAUSE,
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func deactivate_insert() -> void:
|
||||
# #518/D-058: Send ResumeSimulation when insert closes.
|
||||
if _insert_active:
|
||||
_insert_active = false
|
||||
SimBridge.send_input({
|
||||
"action": InputMapper.Action.UNPAUSE,
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
})
|
||||
(
|
||||
SimBridge
|
||||
. send_input(
|
||||
{
|
||||
"action": InputMapper.Action.UNPAUSE,
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
@@ -217,11 +227,17 @@ func _draw_spoke_icon(spoke: int, center: Vector2, color: Color) -> void:
|
||||
var hh := ICON_SIZE * 0.55
|
||||
draw_rect(Rect2(center - Vector2(hw, hh), Vector2(hw * 2, hh * 2)), color, false, 1.0)
|
||||
# Screen line
|
||||
draw_line(center - Vector2(hw * 0.6, hh * 0.3), center + Vector2(hw * 0.6, -hh * 0.3), color, 1.0)
|
||||
draw_line(
|
||||
center - Vector2(hw * 0.6, hh * 0.3),
|
||||
center + Vector2(hw * 0.6, -hh * 0.3),
|
||||
color,
|
||||
1.0
|
||||
)
|
||||
|
||||
|
||||
# -- Public API ---------------------------------------------------------------
|
||||
|
||||
|
||||
func is_open() -> bool:
|
||||
return _open
|
||||
|
||||
|
||||
Reference in New Issue
Block a user