menu interactive, refactor, perf tests, charge bar, debug menu stuff
This commit is contained in:
+428
-177
@@ -1,10 +1,28 @@
|
||||
extends Node
|
||||
## Runtime debug panel. Toggle with F1.
|
||||
## Reads DP.get_all() to build controls — adding a param to DP is all that's needed to show it here.
|
||||
## Quake-style drop-down debug console. Toggle with ~ (Shift+`).
|
||||
## Query a param: type its name. Set it: "name value".
|
||||
## Tab = complete top match · ↑↓ = browse suggestions / history · Esc = close.
|
||||
|
||||
var _panel: Control
|
||||
var _status_label: Label
|
||||
var _prev_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_CAPTURED
|
||||
const CONSOLE_HEIGHT_RATIO := 0.48
|
||||
const SLIDE_DURATION := 0.18
|
||||
const MAX_SUGGESTIONS := 10
|
||||
const LOG_MAX_LINES := 300
|
||||
|
||||
var _canvas: CanvasLayer
|
||||
var _panel: PanelContainer
|
||||
var _log: RichTextLabel
|
||||
var _suggest_panel: PanelContainer
|
||||
var _suggest_box: VBoxContainer
|
||||
var _suggest_labels: Array[Label] = []
|
||||
var _input_field: LineEdit
|
||||
|
||||
var _tween: Tween
|
||||
var _is_open: bool = false
|
||||
var _prev_mouse: Input.MouseMode = Input.MOUSE_MODE_CAPTURED
|
||||
var _suggestions: Array[String] = []
|
||||
var _selected_idx: int = -1
|
||||
var _history: Array[String] = []
|
||||
var _history_idx: int = -1
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -12,205 +30,438 @@ func _ready() -> void:
|
||||
|
||||
|
||||
func _build_ui() -> void:
|
||||
var canvas := CanvasLayer.new()
|
||||
canvas.layer = 128
|
||||
add_child(canvas)
|
||||
_canvas = CanvasLayer.new()
|
||||
_canvas.layer = 128
|
||||
add_child(_canvas)
|
||||
|
||||
_panel = PanelContainer.new()
|
||||
_panel.anchor_left = 1.0
|
||||
_panel.anchor_left = 0.0
|
||||
_panel.anchor_right = 1.0
|
||||
_panel.anchor_top = 0.0
|
||||
_panel.anchor_bottom = 1.0
|
||||
_panel.offset_left = -390.0
|
||||
_panel.offset_right = 0.0
|
||||
_panel.offset_top = 0.0
|
||||
_panel.offset_bottom = 0.0
|
||||
_panel.anchor_bottom = 0.0
|
||||
_panel.offset_bottom = 500.0
|
||||
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_panel.visible = false
|
||||
|
||||
var bg := StyleBoxFlat.new()
|
||||
bg.bg_color = Color(0.08, 0.08, 0.10, 0.95)
|
||||
bg.bg_color = Color(0.04, 0.06, 0.04, 0.94)
|
||||
bg.border_width_bottom = 2
|
||||
bg.border_color = Color(0.2, 0.8, 0.2, 1.0)
|
||||
_panel.add_theme_stylebox_override("panel", bg)
|
||||
canvas.add_child(_panel)
|
||||
_canvas.add_child(_panel)
|
||||
|
||||
var root_vbox := VBoxContainer.new()
|
||||
root_vbox.size_flags_horizontal = Control.SIZE_FILL
|
||||
root_vbox.size_flags_vertical = Control.SIZE_FILL
|
||||
_panel.add_child(root_vbox)
|
||||
|
||||
# ── Toolbar ───────────────────────────────────────────────────────────────
|
||||
var toolbar_margin := MarginContainer.new()
|
||||
toolbar_margin.add_theme_constant_override("margin_left", 8)
|
||||
toolbar_margin.add_theme_constant_override("margin_right", 8)
|
||||
toolbar_margin.add_theme_constant_override("margin_top", 6)
|
||||
toolbar_margin.add_theme_constant_override("margin_bottom", 4)
|
||||
root_vbox.add_child(toolbar_margin)
|
||||
# ── Header ────────────────────────────────────────────────────────────────
|
||||
var header_margin := MarginContainer.new()
|
||||
header_margin.add_theme_constant_override("margin_left", 10)
|
||||
header_margin.add_theme_constant_override("margin_top", 4)
|
||||
header_margin.add_theme_constant_override("margin_bottom", 2)
|
||||
root_vbox.add_child(header_margin)
|
||||
|
||||
var toolbar := HBoxContainer.new()
|
||||
toolbar.add_theme_constant_override("separation", 6)
|
||||
toolbar_margin.add_child(toolbar)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "DEBUG [F1]"
|
||||
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
toolbar.add_child(title)
|
||||
|
||||
_status_label = Label.new()
|
||||
_status_label.modulate = Color(0.4, 1.0, 0.5)
|
||||
toolbar.add_child(_status_label)
|
||||
|
||||
var save_btn := Button.new()
|
||||
save_btn.text = "Save"
|
||||
save_btn.pressed.connect(_on_save)
|
||||
toolbar.add_child(save_btn)
|
||||
|
||||
var rand_all_btn := Button.new()
|
||||
rand_all_btn.text = "Randomize All"
|
||||
rand_all_btn.pressed.connect(_on_randomize_all)
|
||||
toolbar.add_child(rand_all_btn)
|
||||
|
||||
var reset_btn := Button.new()
|
||||
reset_btn.text = "Reset All"
|
||||
reset_btn.pressed.connect(DP.reset_all)
|
||||
toolbar.add_child(reset_btn)
|
||||
var header_lbl := Label.new()
|
||||
header_lbl.text = "BULLOSSEUM CONSOLE [~ close · Tab complete · ↑↓ browse]"
|
||||
header_lbl.modulate = Color(0.35, 0.9, 0.35, 0.60)
|
||||
header_margin.add_child(header_lbl)
|
||||
|
||||
root_vbox.add_child(HSeparator.new())
|
||||
|
||||
# ── Scrollable content ────────────────────────────────────────────────────
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
scroll.size_flags_horizontal = Control.SIZE_FILL
|
||||
root_vbox.add_child(scroll)
|
||||
# ── Log ───────────────────────────────────────────────────────────────────
|
||||
_log = RichTextLabel.new()
|
||||
_log.bbcode_enabled = true
|
||||
_log.scroll_following = true
|
||||
_log.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_log.size_flags_horizontal = Control.SIZE_FILL
|
||||
_log.add_theme_font_size_override("normal_font_size", 13)
|
||||
_log.add_theme_constant_override("line_separation", 1)
|
||||
root_vbox.add_child(_log)
|
||||
|
||||
var content_margin := MarginContainer.new()
|
||||
content_margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
content_margin.add_theme_constant_override("margin_left", 8)
|
||||
content_margin.add_theme_constant_override("margin_right", 8)
|
||||
content_margin.add_theme_constant_override("margin_top", 4)
|
||||
content_margin.add_theme_constant_override("margin_bottom", 8)
|
||||
scroll.add_child(content_margin)
|
||||
# ── Suggestion panel ──────────────────────────────────────────────────────
|
||||
_suggest_panel = PanelContainer.new()
|
||||
_suggest_panel.visible = false
|
||||
_suggest_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
var content := VBoxContainer.new()
|
||||
content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
content_margin.add_child(content)
|
||||
var sg_bg := StyleBoxFlat.new()
|
||||
sg_bg.bg_color = Color(0.06, 0.10, 0.06, 0.97)
|
||||
sg_bg.border_width_top = 1
|
||||
sg_bg.border_color = Color(0.15, 0.55, 0.15, 0.8)
|
||||
_suggest_panel.add_theme_stylebox_override("panel", sg_bg)
|
||||
root_vbox.add_child(_suggest_panel)
|
||||
|
||||
_build_params(content)
|
||||
_suggest_box = VBoxContainer.new()
|
||||
_suggest_box.add_theme_constant_override("separation", 0)
|
||||
_suggest_panel.add_child(_suggest_box)
|
||||
|
||||
for _i in MAX_SUGGESTIONS:
|
||||
var lbl := Label.new()
|
||||
lbl.visible = false
|
||||
lbl.add_theme_font_size_override("font_size", 13)
|
||||
_suggest_box.add_child(lbl)
|
||||
_suggest_labels.append(lbl)
|
||||
|
||||
root_vbox.add_child(HSeparator.new())
|
||||
|
||||
# ── Input row ─────────────────────────────────────────────────────────────
|
||||
var in_margin := MarginContainer.new()
|
||||
in_margin.add_theme_constant_override("margin_left", 10)
|
||||
in_margin.add_theme_constant_override("margin_right", 10)
|
||||
in_margin.add_theme_constant_override("margin_top", 4)
|
||||
in_margin.add_theme_constant_override("margin_bottom", 6)
|
||||
root_vbox.add_child(in_margin)
|
||||
|
||||
var in_row := HBoxContainer.new()
|
||||
in_row.add_theme_constant_override("separation", 6)
|
||||
in_margin.add_child(in_row)
|
||||
|
||||
var prompt_lbl := Label.new()
|
||||
prompt_lbl.text = ">"
|
||||
prompt_lbl.modulate = Color(0.35, 1.0, 0.35)
|
||||
in_row.add_child(prompt_lbl)
|
||||
|
||||
_input_field = LineEdit.new()
|
||||
_input_field.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_input_field.placeholder_text = "param_name | param_name value | save / reset / list / help"
|
||||
_input_field.clear_button_enabled = true
|
||||
_input_field.add_theme_font_size_override("font_size", 14)
|
||||
in_row.add_child(_input_field)
|
||||
|
||||
_input_field.text_changed.connect(_on_text_changed)
|
||||
_input_field.text_submitted.connect(_on_submitted)
|
||||
|
||||
_log_raw("[color=#55bb55]Bullosseum Debug Console[/color] [color=#444444]─ Tab=complete ↑↓=browse ~=close[/color]")
|
||||
_log_raw("[color=#444444]Commands:[/color] [b]save reset list [section] help[/b]")
|
||||
_log_raw("")
|
||||
|
||||
|
||||
func _build_params(content: VBoxContainer) -> void:
|
||||
# Group keys by section preserving registration order
|
||||
var sections: Dictionary = {}
|
||||
# ── Input handling ────────────────────────────────────────────────────────────
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not (event is InputEventKey and (event as InputEventKey).pressed
|
||||
and not (event as InputEventKey).echo):
|
||||
return
|
||||
var ke := event as InputEventKey
|
||||
|
||||
# ~ (Shift+backtick) — toggle regardless of focus
|
||||
if ke.keycode == KEY_QUOTELEFT and ke.shift_pressed:
|
||||
_toggle()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
||||
if not _is_open:
|
||||
return
|
||||
|
||||
match ke.keycode:
|
||||
KEY_TAB:
|
||||
_do_autocomplete()
|
||||
get_viewport().set_input_as_handled()
|
||||
KEY_UP:
|
||||
_navigate(-1)
|
||||
get_viewport().set_input_as_handled()
|
||||
KEY_DOWN:
|
||||
_navigate(1)
|
||||
get_viewport().set_input_as_handled()
|
||||
KEY_ESCAPE:
|
||||
_toggle()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _toggle() -> void:
|
||||
_is_open = not _is_open
|
||||
var h := get_viewport().get_visible_rect().size.y * CONSOLE_HEIGHT_RATIO
|
||||
_panel.offset_bottom = h
|
||||
|
||||
if _tween:
|
||||
_tween.kill()
|
||||
_tween = create_tween().set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_QUART)
|
||||
|
||||
if _is_open:
|
||||
_prev_mouse = Input.mouse_mode
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
_panel.position.y = -h
|
||||
_panel.visible = true
|
||||
_tween.tween_property(_panel, "position:y", 0.0, SLIDE_DURATION)
|
||||
_tween.tween_callback(_input_field.grab_focus)
|
||||
else:
|
||||
_tween.tween_property(_panel, "position:y", -h, SLIDE_DURATION)
|
||||
_tween.tween_callback(func() -> void: _panel.visible = false)
|
||||
Input.mouse_mode = _prev_mouse
|
||||
_clear_suggestions()
|
||||
|
||||
|
||||
# ── Fuzzy autocomplete ────────────────────────────────────────────────────────
|
||||
|
||||
func _on_text_changed(text: String) -> void:
|
||||
_selected_idx = -1
|
||||
_history_idx = -1
|
||||
var query := text.get_slice(" ", 0).strip_edges()
|
||||
if query.is_empty():
|
||||
_clear_suggestions()
|
||||
return
|
||||
_refresh_suggestions(query)
|
||||
|
||||
|
||||
func _refresh_suggestions(query: String) -> void:
|
||||
var ql := query.to_lower()
|
||||
var scored : Array = []
|
||||
for key: String in DP.get_all():
|
||||
var sec: String = DP.get_all()[key]["section"]
|
||||
var s := _fuzzy_score(ql, key.to_lower())
|
||||
if s > 0:
|
||||
scored.append([s, key])
|
||||
scored.sort_custom(func(a: Array, b: Array) -> bool: return a[0] > b[0])
|
||||
|
||||
_suggestions.clear()
|
||||
for i in mini(scored.size(), MAX_SUGGESTIONS):
|
||||
_suggestions.append(scored[i][1])
|
||||
|
||||
for i in MAX_SUGGESTIONS:
|
||||
if i < _suggestions.size():
|
||||
var key : String = _suggestions[i]
|
||||
var meta: Dictionary = DP.get_all()[key]
|
||||
_suggest_labels[i].text = " %s %s [%s]" % [
|
||||
key.rpad(28), _fmt_val(key, meta).rpad(8), meta["section"]
|
||||
]
|
||||
_suggest_labels[i].modulate = _suggestion_color(i)
|
||||
_suggest_labels[i].visible = true
|
||||
else:
|
||||
_suggest_labels[i].visible = false
|
||||
_suggest_panel.visible = not _suggestions.is_empty()
|
||||
|
||||
|
||||
func _suggestion_color(idx: int) -> Color:
|
||||
if idx == _selected_idx:
|
||||
return Color(1.0, 1.0, 0.4) # selected — yellow
|
||||
if idx == 0:
|
||||
return Color(0.85, 1.0, 0.70) # top match — bright green
|
||||
return Color(0.60, 0.78, 0.50) # rest — muted green
|
||||
|
||||
|
||||
func _fuzzy_score(query: String, target: String) -> int:
|
||||
var qi := 0
|
||||
var score := 0
|
||||
var last := -1
|
||||
for ti in target.length():
|
||||
if qi >= query.length():
|
||||
break
|
||||
if target[ti] == query[qi]:
|
||||
score += 10 + (5 if ti == last + 1 else 0)
|
||||
last = ti
|
||||
qi += 1
|
||||
if qi < query.length():
|
||||
return 0 # not all query chars matched
|
||||
if target.begins_with(query):
|
||||
score += 50
|
||||
if target == query:
|
||||
score += 100
|
||||
return score
|
||||
|
||||
|
||||
func _fmt_val(_key: String, meta: Dictionary) -> String:
|
||||
if meta["type"] == TYPE_FLOAT:
|
||||
return "%.4g" % (meta["value"] as float)
|
||||
if meta["type"] == TYPE_BOOL:
|
||||
var bval: bool = meta["value"]
|
||||
return "true" if bval else "false"
|
||||
return str(meta["value"])
|
||||
|
||||
|
||||
func _clear_suggestions() -> void:
|
||||
_suggestions.clear()
|
||||
_selected_idx = -1
|
||||
for lbl in _suggest_labels:
|
||||
lbl.visible = false
|
||||
_suggest_panel.visible = false
|
||||
|
||||
|
||||
func _do_autocomplete() -> void:
|
||||
if _suggestions.is_empty():
|
||||
return
|
||||
var idx := maxi(0, _selected_idx)
|
||||
var key := _suggestions[idx]
|
||||
var meta: Dictionary = DP.get_all()[key]
|
||||
# Fill "key current_value" so the user just edits the number
|
||||
_input_field.text = "%s %s" % [key, _fmt_val(key, meta)]
|
||||
_input_field.caret_column = _input_field.text.length()
|
||||
_clear_suggestions()
|
||||
|
||||
|
||||
func _navigate(dir: int) -> void:
|
||||
if _suggestions.is_empty():
|
||||
# History navigation when no suggestion list is visible
|
||||
if dir == -1 and _history_idx + 1 < _history.size():
|
||||
_history_idx += 1
|
||||
elif dir == 1 and _history_idx > 0:
|
||||
_history_idx -= 1
|
||||
elif dir == 1 and _history_idx == 0:
|
||||
_history_idx = -1
|
||||
_input_field.clear()
|
||||
return
|
||||
else:
|
||||
return
|
||||
if _history_idx >= 0:
|
||||
_input_field.text = _history[_history_idx]
|
||||
_input_field.caret_column = _input_field.text.length()
|
||||
return
|
||||
|
||||
# Cycle through suggestion list
|
||||
if _selected_idx < 0:
|
||||
_selected_idx = 0 if dir == 1 else _suggestions.size() - 1
|
||||
else:
|
||||
_selected_idx = wrapi(_selected_idx + dir, 0, _suggestions.size())
|
||||
|
||||
for i in MAX_SUGGESTIONS:
|
||||
if _suggest_labels[i].visible:
|
||||
_suggest_labels[i].modulate = _suggestion_color(i)
|
||||
|
||||
var key := _suggestions[_selected_idx]
|
||||
var meta: Dictionary = DP.get_all()[key]
|
||||
_input_field.text = "%s %s" % [key, _fmt_val(key, meta)]
|
||||
_input_field.caret_column = _input_field.text.length()
|
||||
|
||||
|
||||
# ── Command execution ─────────────────────────────────────────────────────────
|
||||
|
||||
func _on_submitted(raw: String) -> void:
|
||||
raw = raw.strip_edges()
|
||||
if raw.is_empty():
|
||||
return
|
||||
_history.push_front(raw)
|
||||
_history_idx = -1
|
||||
_input_field.clear()
|
||||
_clear_suggestions()
|
||||
_execute(raw)
|
||||
|
||||
|
||||
func _execute(cmd: String) -> void:
|
||||
_log_raw("[color=#3a5a3a]> %s[/color]" % cmd)
|
||||
var parts := cmd.split(" ", false)
|
||||
if parts.is_empty():
|
||||
return
|
||||
|
||||
match parts[0].to_lower():
|
||||
"save":
|
||||
DP.save()
|
||||
_log_ok("Saved.")
|
||||
return
|
||||
"reset":
|
||||
DP.reset_all()
|
||||
_log_ok("All params reset to defaults.")
|
||||
return
|
||||
"list":
|
||||
_cmd_list(parts)
|
||||
return
|
||||
"help":
|
||||
_log_raw("[color=#888888] save[/color] — persist values to disk")
|
||||
_log_raw("[color=#888888] reset[/color] — restore all defaults")
|
||||
_log_raw("[color=#888888] list [section][/color] — print params (optional section filter)")
|
||||
_log_raw("[color=#888888] <name>[/color] — read current value + range")
|
||||
_log_raw("[color=#888888] <name> <value>[/color] — set param (bool: true/false/1/0)")
|
||||
return
|
||||
|
||||
# Treat as param key, with fuzzy fall-back for partial names
|
||||
var key := parts[0]
|
||||
var all := DP.get_all()
|
||||
if not all.has(key):
|
||||
var matches := _fuzzy_find_all(key)
|
||||
if matches.size() == 1:
|
||||
key = matches[0]
|
||||
_log_raw("[color=#8888ff]→ %s[/color]" % key)
|
||||
else:
|
||||
if matches.size() > 1:
|
||||
_log_warn("Ambiguous '%s': %s" % [parts[0], ", ".join(matches.slice(0, 6))])
|
||||
else:
|
||||
_log_warn("Unknown: %s" % parts[0])
|
||||
return
|
||||
|
||||
var meta: Dictionary = all[key]
|
||||
if parts.size() == 1:
|
||||
_read_param(key, meta)
|
||||
else:
|
||||
_set_param(key, meta, parts[1])
|
||||
|
||||
|
||||
func _read_param(key: String, meta: Dictionary) -> void:
|
||||
if meta["type"] == TYPE_FLOAT:
|
||||
_log_raw(
|
||||
"[b]%s[/b] = [color=#ffe080]%s[/color] [color=#484848](min %s max %s step %s default %s)[/color]" % [
|
||||
key, _fmt_val(key, meta),
|
||||
"%.4g" % (meta["min"] as float),
|
||||
"%.4g" % (meta["max"] as float),
|
||||
"%.4g" % (meta["step"] as float),
|
||||
"%.4g" % (meta["default"] as float),
|
||||
]
|
||||
)
|
||||
else:
|
||||
_log_raw("[b]%s[/b] = [color=#ffe080]%s[/color]" % [key, _fmt_val(key, meta)])
|
||||
|
||||
|
||||
func _set_param(key: String, meta: Dictionary, raw: String) -> void:
|
||||
if meta["type"] == TYPE_FLOAT:
|
||||
if not raw.is_valid_float():
|
||||
_log_warn("Expected number, got: %s" % raw)
|
||||
return
|
||||
var v := snappedf(
|
||||
clampf(raw.to_float(), meta["min"] as float, meta["max"] as float),
|
||||
meta["step"] as float
|
||||
)
|
||||
DP.set_value(key, v)
|
||||
_log_ok("[b]%s[/b] ← [color=#ffe080]%.4g[/color]" % [key, v])
|
||||
elif meta["type"] == TYPE_BOOL:
|
||||
match raw.to_lower():
|
||||
"1", "true", "yes", "on":
|
||||
DP.set_value(key, true)
|
||||
_log_ok("[b]%s[/b] ← [color=#ffe080]true[/color]" % key)
|
||||
"0", "false", "no", "off":
|
||||
DP.set_value(key, false)
|
||||
_log_ok("[b]%s[/b] ← [color=#ffe080]false[/color]" % key)
|
||||
_:
|
||||
_log_warn("Expected true/false, got: %s" % raw)
|
||||
else:
|
||||
_log_warn("Cannot set param of type %d" % meta["type"])
|
||||
|
||||
|
||||
func _cmd_list(parts: PackedStringArray) -> void:
|
||||
var filter := parts[1].to_lower() if parts.size() > 1 else ""
|
||||
var all := DP.get_all()
|
||||
var sections: Dictionary = {}
|
||||
for key: String in all:
|
||||
var sec: String = all[key]["section"]
|
||||
if filter.length() > 0:
|
||||
if not sec.to_lower().begins_with(filter) and not key.to_lower().contains(filter):
|
||||
continue
|
||||
if not sections.has(sec):
|
||||
sections[sec] = []
|
||||
sections[sec].append(key)
|
||||
|
||||
for section: String in sections:
|
||||
content.add_child(HSeparator.new())
|
||||
|
||||
var header := Label.new()
|
||||
header.text = section.to_upper()
|
||||
header.modulate = Color(0.75, 0.85, 1.0)
|
||||
content.add_child(header)
|
||||
|
||||
for key: String in sections[section]:
|
||||
var meta: Dictionary = DP.get_all()[key]
|
||||
if meta["type"] == TYPE_FLOAT:
|
||||
_add_float_row(content, key, meta)
|
||||
for sec: String in sections:
|
||||
_log_raw("[color=#6699ff][b]%s[/b][/color]" % sec)
|
||||
for key: String in sections[sec]:
|
||||
_log_raw(" [color=#777777]%s[/color] = [color=#ffe080]%s[/color]" % [
|
||||
key, _fmt_val(key, all[key])
|
||||
])
|
||||
|
||||
|
||||
func _add_float_row(parent: VBoxContainer, key: String, meta: Dictionary) -> void:
|
||||
var row := HBoxContainer.new()
|
||||
row.size_flags_horizontal = Control.SIZE_FILL
|
||||
parent.add_child(row)
|
||||
|
||||
var lbl := Label.new()
|
||||
lbl.text = key.replace("_", " ")
|
||||
lbl.custom_minimum_size.x = 140
|
||||
lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
row.add_child(lbl)
|
||||
|
||||
var slider := HSlider.new()
|
||||
slider.min_value = meta["min"]
|
||||
slider.max_value = meta["max"]
|
||||
slider.step = meta["step"]
|
||||
slider.value = meta["value"]
|
||||
slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
slider.custom_minimum_size.x = 60
|
||||
row.add_child(slider)
|
||||
|
||||
var spin := SpinBox.new()
|
||||
spin.min_value = meta["min"]
|
||||
spin.max_value = meta["max"]
|
||||
spin.step = meta["step"]
|
||||
spin.value = meta["value"]
|
||||
spin.custom_minimum_size.x = 85
|
||||
row.add_child(spin)
|
||||
|
||||
var rand_btn := Button.new()
|
||||
rand_btn.text = "🎲"
|
||||
rand_btn.tooltip_text = "Randomize"
|
||||
rand_btn.pressed.connect(func() -> void:
|
||||
var v := randf_range(meta["min"], meta["max"])
|
||||
v = snappedf(v, meta["step"])
|
||||
slider.value = v
|
||||
)
|
||||
row.add_child(rand_btn)
|
||||
|
||||
# Slider → SpinBox + DP (use no_signal to avoid echo)
|
||||
slider.value_changed.connect(func(v: float) -> void:
|
||||
spin.set_value_no_signal(v)
|
||||
DP.set_value(key, v)
|
||||
)
|
||||
# SpinBox → Slider + DP
|
||||
spin.value_changed.connect(func(v: float) -> void:
|
||||
slider.set_value_no_signal(v)
|
||||
DP.set_value(key, v)
|
||||
)
|
||||
# External changes (reset, load) → sync both controls
|
||||
DP.any_changed.connect(func(changed_key: String, _val: Variant) -> void:
|
||||
if changed_key != key and changed_key != "__all__":
|
||||
return
|
||||
var v := DP.f(key)
|
||||
slider.set_value_no_signal(v)
|
||||
spin.set_value_no_signal(v)
|
||||
)
|
||||
|
||||
|
||||
|
||||
func _on_save() -> void:
|
||||
DP.save()
|
||||
_status_label.text = "Saved!"
|
||||
get_tree().create_timer(1.5).timeout.connect(func() -> void:
|
||||
_status_label.text = ""
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if _panel.visible and Input.is_key_pressed(KEY_CTRL) and event is InputEventMouseMotion:
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not (event is InputEventKey and event.pressed and not event.echo):
|
||||
return
|
||||
if (event as InputEventKey).keycode != KEY_F1:
|
||||
return
|
||||
_panel.visible = not _panel.visible
|
||||
if _panel.visible:
|
||||
_prev_mouse_mode = Input.mouse_mode
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
else:
|
||||
Input.mouse_mode = _prev_mouse_mode
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
|
||||
func _on_randomize_all() -> void:
|
||||
func _fuzzy_find_all(query: String) -> Array[String]:
|
||||
var result: Array[String] = []
|
||||
var ql := query.to_lower()
|
||||
for key: String in DP.get_all():
|
||||
var meta: Dictionary = DP.get_all()[key]
|
||||
if meta["type"] == TYPE_FLOAT:
|
||||
var v := snappedf(randf_range(meta["min"], meta["max"]), meta["step"])
|
||||
DP.set_value(key, v)
|
||||
if _fuzzy_score(ql, key.to_lower()) > 0:
|
||||
result.append(key)
|
||||
return result
|
||||
|
||||
|
||||
# ── Logging ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func _log_raw(text: String) -> void:
|
||||
if _log.get_line_count() > LOG_MAX_LINES:
|
||||
_log.clear()
|
||||
_log.append_text(text + "\n")
|
||||
|
||||
|
||||
func _log_ok(text: String) -> void:
|
||||
_log_raw("[color=#66ee88]%s[/color]" % text)
|
||||
|
||||
|
||||
func _log_warn(text: String) -> void:
|
||||
_log_raw("[color=#ff7744]%s[/color]" % text)
|
||||
|
||||
Reference in New Issue
Block a user