extends Node
## Central registry for all runtime-tunable parameters.
## Add a new param with one _reg_f() call in _register_all() — the menu picks it up automatically.
## Access: DP.f("key")  •  Persist: DP.save() / DP.load_saved()  •  Reset: DP.reset_all()

signal any_changed(key: String, value: Variant)

const SAVE_PATH := "user://debug_params.cfg"

# _params holds the full metadata (min/max/step/default/section/type) and is the
# authority the debug menu reads. _values is a flat key→value mirror so the hot
# f()/b() reads — called dozens of times per matador per physics frame — cost a
# single hash lookup instead of a nested dict access. Every write updates both.
var _params: Dictionary = {}
var _values: Dictionary = {}


func _ready() -> void:
	_register_all()
	load_saved()


func _register_all() -> void:
	# ── Movement ──────────────────────────────────────────────────────────────
	# WASD/arrows drive the bull: move_accel ramps flat velocity toward max_speed in
	# the input direction; move_friction bleeds it off when no direction is held.
	_reg_f("Movement", "max_speed",           10.0,  2.0, 1000.0)
	_reg_f("Movement", "move_accel",         120.0,  5.0,  600.0)
	_reg_f("Movement", "move_friction",       90.0,  5.0,  600.0)
	_reg_f("Movement", "jump_velocity",        4.5,  1.0,  200.0)
	_reg_f("Movement", "visual_turn_speed",   12.0,  1.0,  200.0)
	_reg_f("Movement", "bounce_restitution",      0.55, 0.0,    2.0, 0.05)
	_reg_f("Movement", "bounce_min_impact",       1.0,  0.0,   20.0)
	_reg_f("Movement", "wall_bounce_restitution", 0.6,  0.0,    1.5, 0.05)
	_reg_f("Movement", "wall_min_bounce_speed",  6.0,  0.0,   30.0)
	# ── Bull health ───────────────────────────────────────────────────────────
	# The bull soaks bull_max_hp clean hits before it dies; each hit grants
	# bull_hit_iframes seconds of invulnerability so a single pass can't drain it.
	_reg_f("Bull",     "bull_max_hp",         10.0,  1.0,   100.0, 1.0)
	_reg_f("Bull",     "bull_hit_iframes",    0.8,  0.0,    3.0, 0.1)
	# ── Dust speed thresholds ─────────────────────────────────────────────────
	_reg_f("Dust",     "dust_walk_spd",       1.5,  0.1,  10.0)
	_reg_f("Dust",     "dust_charge_spd",     7.0,  1.0,  20.0)
	# ── Dust ──────────────────────────────────────────────────────────────────
	_reg_f("Dust",     "dust_lifetime",       0.9,  0.1,   4.0)
	_reg_f("Dust",     "dust_explosiveness",  0.35, 0.0,   1.0)
	_reg_f("Dust",     "dust_spread",        45.0,  5.0,  90.0)
	_reg_f("Dust",     "dust_gravity_y",     -1.5,-15.0,   0.0)
	_reg_f("Dust",     "walk_scale_min",      0.35, 0.05,  3.0)
	_reg_f("Dust",     "walk_scale_max",      0.65, 0.05,  3.0)
	_reg_f("Dust",     "walk_vel_min",        0.8,  0.1,  12.0)
	_reg_f("Dust",     "walk_vel_max",        1.8,  0.1,  12.0)
	_reg_f("Dust",     "charge_scale_min",    1.0,  0.05,  6.0)
	_reg_f("Dust",     "charge_scale_max",    2.2,  0.05,  6.0)
	_reg_f("Dust",     "charge_vel_min",      2.8,  0.1,  20.0)
	_reg_f("Dust",     "charge_vel_max",      5.5,  0.1,  20.0)
	# ── Tail ──────────────────────────────────────────────────────────────────
	_reg_f("Tail",     "tail_gravity",        9.8,   0.0, 30.0)
	_reg_f("Tail",     "tail_damping",        0.08,  0.0,  0.5, 0.01)
	_reg_f("Tail",     "tail_body_radius",    0.35,  0.05, 2.0, 0.05)
	_reg_f("Tail",     "tail_wag_freq",       4.0,   0.5, 15.0)
	_reg_f("Tail",     "tail_wag_idle",       0.3,   0.0,  2.0, 0.05)
	# ── Charge pose (horns-down lean during dash / roll) ─────────────────────
	_reg_f("Charge",   "charge_head_pitch",      0.3,  0.0,    1.2, 0.01)
	_reg_f("Charge",   "charge_head_speed",      5.0,  0.5,   20.0)
	# ── Body ──────────────────────────────────────────────────────────────────
	_reg_f("Body",     "body_tilt_fwd",       0.015, 0.0,   0.1, 0.001)
	_reg_f("Body",     "body_tilt_side",      0.01,  0.0,   0.1, 0.001)
	_reg_f("Body",     "aerial_pitch_scale",  0.6,   0.0,   2.0)
	_reg_f("Body",     "aerial_pitch_speed",  5.0,   0.5,  20.0)
	# ── Legs ──────────────────────────────────────────────────────────────────
	_reg_f("Legs",     "leg_step_threshold",  0.15,  0.01,  1.0, 0.01)
	_reg_f("Legs",     "leg_step_duration",   0.12,  0.02,  0.5, 0.01)
	_reg_f("Legs",     "leg_step_height",     0.15,  0.0,   0.5)
	_reg_f("Legs",     "leg_step_overshoot",  0.3,   0.0,   1.0)
	_reg_f("Legs",     "gait_freq",           1.5,   0.1,   5.0)
	_reg_f("Legs",     "launch_hold_time",    0.15,  0.0,   1.0, 0.01)
	_reg_f("Legs",     "ragdoll_wobble_amp",  0.15,  0.0,   1.0)
	_reg_f("Legs",     "tuck_apex_threshold", 0.5,  -5.0,   5.0)
	_reg_f("Legs",     "jump_tuck_speed",     8.0,   1.0,  30.0)
	_reg_f("Legs",     "jump_tuck_up",        0.2,   0.0,   1.0)
	_reg_f("Legs",     "jump_tuck_fwd",      -0.1,  -0.5,   0.5)
	_reg_f("Legs",     "jump_land_threshold", 1.5,   0.5,   5.0)
	# ── Camera ────────────────────────────────────────────────────────────────
	_reg_f("Camera",   "charge_zoom_factor",  0.65,  0.3,   1.0, 0.05)
	_reg_f("Camera",   "cam_lerp_power",      6.0,   0.5,  30.0)
	_reg_f("Camera",   "cam_sensitivity",     0.005, 0.001, 0.05, 0.001)
	_reg_f("Camera",   "cam_min_zoom",        3.5,   0.5,  20.0)
	_reg_f("Camera",   "cam_max_zoom",       40.0,   2.0,  80.0)
	_reg_f("Camera",   "cam_spring_length",   7.0,   1.0,  30.0)
	_reg_f("Camera",   "cam_min_vert_angle", -60.0, -90.0,   0.0,  1.0)
	_reg_f("Camera",   "cam_max_vert_angle",  45.0,   0.0,  90.0,  1.0)
	# Trailing "drag" camera: the focus point lags the bull and slides opposite to
	# its velocity, so the camera visibly drags behind during a fast charge.
	# cam_drag        = metres of trail per m/s of bull speed (0 = rigid follow)
	# cam_drag_speed  = how fast the trail offset eases toward its target
	# cam_follow_lerp = how tightly the focus point chases the bull (higher = snappier)
	_reg_f("Camera",   "cam_drag",           0.12,  0.0,   1.0, 0.01)
	_reg_f("Camera",   "cam_drag_speed",     3.5,   0.5,  20.0)
	_reg_f("Camera",   "cam_follow_lerp",   10.0,   1.0,  40.0)
	# ── Matador ───────────────────────────────────────────────────────────────
	_reg_f("Matador",  "mat_spawn_count",     1.0,   1.0,  30.0,  1.0)
	# Hydra split: a slain matador is replaced on the spot by mat_split_factor fresh
	# matadors, so the horde doubles each generation (1 → 2 → 4 → 8 …) at the default
	# 2. Capped at mat_split_generations deep so the arena stays winnable (factor <2 or
	# generations 0 = no splitting, the classic one-and-done arena).
	_reg_f("Matador",  "mat_split_factor",      2.0, 1.0,   6.0, 1.0)
	_reg_f("Matador",  "mat_split_generations", 3.0, 0.0,   6.0, 1.0)
	# The bull wins outright once it has slain this many matadors, even if the hydra
	# still has bodies on the sand. 0 = disabled (win only when the horde dies out)
	_reg_f("Matador",  "mat_win_kills",        21.0, 0.0, 200.0, 1.0)
	_reg_f("Matador",  "mat_walk_speed",      5.5,   0.5,  10.0)
	_reg_f("Matador",  "mat_wander_radius",  22.0,   5.0,  50.0)
	# Radius of the inner fighting floor: bull + matadors spawn on this ring (so
	# they stay off the benches). Tune in the console to hug the bench edge.
	_reg_f("Matador",  "arena_spawn_radius", 10.0,   2.0,  22.0, 0.5)
	_reg_f("Matador",  "mat_idle_min",        1.2,   0.0,   5.0, 0.1)
	_reg_f("Matador",  "mat_idle_max",        4.0,   0.5,  10.0, 0.1)
	_reg_f("Matador",  "mat_hit_threshold",   4.0,   1.0,  30.0)
	_reg_f("Matador",  "mat_ragdoll_impulse", 6.0,   0.5,  20.0)
	# Ragdoll joint limits — lower swing/twist = stiffer corpse that keeps its
	# shape; higher angular damp settles the flail instead of flopping flat.
	_reg_f("Matador",  "mat_joint_swing",    35.0,   0.0, 180.0, 1.0)
	_reg_f("Matador",  "mat_joint_twist",    40.0,   0.0, 180.0, 1.0)
	_reg_f("Matador",  "mat_ragdoll_damp",    1.5,   0.0,  10.0, 0.1)
	_reg_f("Matador",  "mat_flee_range",       3.5,  2.0,  40.0)
	_reg_f("Matador",  "mat_flee_speed",       7.0,  0.5,  12.0)
	_reg_f("Matador",  "mat_charge_speed",    10.0,  1.0,  30.0)
	_reg_f("Matador",  "mat_brace_range",      5.0,  2.0,  20.0)
	_reg_f("Matador",  "mat_brace_duration",   0.7,  0.05,  1.5, 0.05)
	_reg_f("Matador",  "mat_commit_dist",      2.5,  0.5,   6.0, 0.1)
	_reg_f("Matador",  "mat_step_speed",       6.5,  2.0,  20.0)
	_reg_f("Matador",  "mat_step_duration",    0.35, 0.1,   1.0, 0.05)
	# How strongly the sidestep biases toward the bull so the blade sweeps through
	# its path during the pass (0 = pure lateral dodge, 1 = straight at the bull).
	_reg_f("Matador",  "mat_pass_lunge",       0.35, 0.0,   1.0, 0.05)
	_reg_f("Matador",  "mat_dodge_cooldown",   2.5,  0.5,  12.0, 0.25)
	_reg_f("Matador",  "mat_dodge_chance",     0.72, 0.0,   1.0, 0.05)
	_reg_f("Matador",  "mat_attack_range",     8.0,  2.0,  20.0)
	_reg_f("Matador",  "mat_attack_speed",     7.5,  1.0,  12.0)
	_reg_f("Matador",  "mat_attack_duration",  1.8,  0.5,   5.0, 0.1)
	_reg_f("Matador",  "mat_attack_min_dist",  3.2,  0.5,   6.0, 0.1)
	# Forward drive during the opening of a melee swing — turns the stationary stab
	# into an estocada lunge so the blade covers ground toward the bull.
	_reg_f("Matador",  "mat_lunge_speed",     13.0,  0.0,  20.0)
	_reg_f("Matador",  "mat_lunge_time",       0.22, 0.0,   0.6, 0.01)
	# Reach of the reach-based stab (the reliable melee hit) and its re-strike gap.
	# The bull dies if it comes inside mat_stab_reach of a matador that's attacking,
	# bracing, or passing with a drawn blade — closing in is genuinely deadly.
	_reg_f("Matador",  "mat_stab_reach",       1.7,  0.5,   4.0, 0.1)
	_reg_f("Matador",  "mat_stab_cooldown",    0.6,  0.1,   3.0, 0.1)
	# Half-angle (deg) the matador must be facing the bull within for a stab to land.
	# Stops "sword's off to the side but I still died" — you're only gored when the
	# blade is actually presented at you.
	_reg_f("Matador",  "mat_stab_arc",        55.0,  5.0, 180.0, 5.0)
	_reg_f("Matador",  "mat_roll_chance",      0.35, 0.0,   1.0, 0.05)
	_reg_f("Matador",  "mat_roll_duration",    0.45, 0.1,   1.5, 0.05)
	# Playback rate of the Draw_weapon clip used for both drawing and sheathing —
	# higher = snappier unholster/holster.
	_reg_f("Matador",  "mat_draw_speed",       1.9,  0.25,  3.0, 0.05)
	# Cross-fade between matador animation clips — smooths Run/Attack/Taunt cuts.
	_reg_f("Matador",  "mat_anim_blend",       0.12, 0.0,   0.5, 0.01)
	# ── Matador heuristics (utility AI) ───────────────────────────────────────
	# Every mat_decide_interval seconds the matador scores its options — dodge the
	# pass, close for a strike, retreat, throw the blade, or strut/taunt — and picks
	# the highest. These weights bias the personality: crank aggression for a reckless
	# bullfighter, caution for a slippery one. mat_w_commit is a stickiness bonus for
	# the current choice so it doesn't dither between two near-tied options.
	_reg_f("Matador",  "mat_decide_interval",  0.2,  0.05,  1.0, 0.05)
	_reg_f("Matador",  "mat_w_aggression",     1.1,  0.0,   3.0, 0.05)
	_reg_f("Matador",  "mat_w_caution",        1.0,  0.0,   3.0, 0.05)
	_reg_f("Matador",  "mat_w_flee",           0.8,  0.0,   3.0, 0.05)
	# Showmanship (strut, circle the bull, taunt) — the supporting matadors lean on
	# this to spread out and give the lead room, so a whole crowd reads as a bullring,
	# not a swarm. Crank it for more posturing, drop it for a relentless mob.
	_reg_f("Matador",  "mat_w_showmanship",    1.6,  0.0,   3.0, 0.05)
	# Weight on hurling the blade. Kept low — a thrown sword is an occasional flourish,
	# not a barrage; raising this brings back the pelting.
	_reg_f("Matador",  "mat_w_throw",          0.35, 0.0,   3.0, 0.05)
	# How many matadors press the bull at once. The nearest this-many engage (close,
	# throw); everyone else keeps their distance and taunts, taking turns as the bull
	# moves. 1 = strict one-on-one, higher = a bolder pack.
	_reg_f("Matador",  "mat_engage_slots",     2.0,  1.0,   8.0, 1.0)
	# Preferred distance (m) a taunting matador circles the bull at while waiting its
	# turn — the showmanship ring the crowd spreads out onto.
	_reg_f("Matador",  "mat_taunt_ring",       7.0,  3.0,  15.0, 0.5)
	# How long (s) a matador safely out of the action holds a taunt before drifting to
	# a new spot — much longer than the between-passes idle, so bystanders mostly pose
	# for the crowd instead of milling about. Actual hold is randomised ±30%.
	_reg_f("Matador",  "mat_taunt_hold",       8.0,  1.0,  20.0, 0.5)
	# Small so it smooths dithering without locking the matador into idling.
	_reg_f("Matador",  "mat_w_commit",         0.15, 0.0,   2.0, 0.05)
	# ── Sword ─────────────────────────────────────────────────────────────────
	# Local seating of the sword in the right hand (drawn / fighting).
	# Defaults are identity — the bone is oriented in Blender to seat the sword
	# directly; these exist to fine-tune in-game without re-exporting.
	_reg_f("Sword",    "sword_pos_x",          0.0, -1.0,   1.0, 0.01)
	_reg_f("Sword",    "sword_pos_y",          0.0, -1.0,   1.0, 0.01)
	_reg_f("Sword",    "sword_pos_z",          0.0, -1.0,   1.0, 0.01)
	_reg_f("Sword",    "sword_rot_x",          0.0, -180.0, 180.0, 1.0)
	_reg_f("Sword",    "sword_rot_y",          0.0, -180.0, 180.0, 1.0)
	_reg_f("Sword",    "sword_rot_z",          0.0, -180.0, 180.0, 1.0)
	# Local seating of the sword in the hip holster (sheathed / wandering).
	# Independent of the hand grip so each pose can be tuned without affecting the
	# other. Defaults angle the blade down along the matador's side.
	_reg_f("Sword",    "sword_rest_pos_x",     0.0, -1.0,   1.0, 0.01)
	_reg_f("Sword",    "sword_rest_pos_y",     0.0, -1.0,   1.0, 0.01)
	_reg_f("Sword",    "sword_rest_pos_z",     0.0, -1.0,   1.0, 0.01)
	_reg_f("Sword",    "sword_rest_rot_x",   -120.0, -180.0, 180.0, 1.0)
	_reg_f("Sword",    "sword_rest_rot_y",     0.0, -180.0, 180.0, 1.0)
	_reg_f("Sword",    "sword_rest_rot_z",     0.0, -180.0, 180.0, 1.0)
	# ── Spear throw (matador ranged attack) ──────────────────────────────────
	# Local seating of the spear in the throwing hand during the wind-up. Tune to
	# seat the shaft in the grip without re-exporting; flight orientation is derived
	# from the aim direction at release, not from this pose.
	_reg_f("Sword",    "spear_pos_x",          0.0, -1.0,   1.0, 0.01)
	_reg_f("Sword",    "spear_pos_y",          0.0, -1.0,   1.0, 0.01)
	_reg_f("Sword",    "spear_pos_z",          0.0, -1.0,   1.0, 0.01)
	_reg_f("Sword",    "spear_rot_x",          0.0, -180.0, 180.0, 1.0)
	_reg_f("Sword",    "spear_rot_y",          0.0, -180.0, 180.0, 1.0)
	_reg_f("Sword",    "spear_rot_z",          0.0, -180.0, 180.0, 1.0)
	# Delay before a fresh sword reappears in the holster if one is ever lost.
	_reg_f("Sword",    "mat_resword_delay",    2.0,  0.0,  10.0, 0.5)
	_reg_f("Sword",    "mat_throw_range",     18.0,  5.0,  40.0)
	_reg_f("Sword",    "mat_throw_min_dist",   5.0,  2.0,  15.0, 0.1)
	_reg_f("Sword",    "mat_throw_windup",     0.5,  0.2,   2.0, 0.05)
	_reg_f("Sword",    "mat_throw_release",    0.55, 0.1,   0.95, 0.01)
	# Horizontal speed of the thrown spear; the vertical launch is solved from this
	# and the gravity below so the arc lands on the bull. Lower = a higher, slower lob.
	_reg_f("Sword",    "mat_throw_speed",     11.0,  5.0,  60.0)
	# Gravity scale on the spear (higher = a more pronounced arc) and the vertical aim
	# offset from the bull origin down onto its low collision body.
	_reg_f("Sword",    "mat_throw_gravity",    1.0,  0.1,   3.0, 0.05)
	_reg_f("Sword",    "mat_throw_aim_y",     -0.45,-1.5,   0.5, 0.05)
	# Aim scatter (± degrees) so thrown blades can be read and dodged on the move.
	_reg_f("Sword",    "mat_throw_spread",    11.0,  0.0,  30.0, 0.5)
	_reg_f("Sword",    "mat_throw_arm_cock",  70.0,-180.0, 180.0, 1.0)
	_reg_f("Sword",    "mat_throw_arm_release", -90.0,-180.0, 180.0, 1.0)
	_reg_f("Sword",    "mat_throw_elbow",     80.0,-180.0, 180.0, 1.0)
	# ── Abilities ─────────────────────────────────────────────────────────────
	_reg_f("Abilities", "kick_cooldown",   1.5,   0.2,  10.0, 0.1)
	_reg_f("Abilities", "kick_range",      6.0,   0.5,   8.0, 0.1)
	_reg_f("Abilities", "kick_strength",  12.0,   1.0,  30.0)
	_reg_f("Abilities", "slam_cooldown",      4.0,   0.5,  15.0, 0.1)
	_reg_f("Abilities", "slam_jump_velocity", 7.0,   2.0,  20.0)
	# Descent gravity multiplier — higher = faster crash, less airtime.
	_reg_f("Abilities", "slam_fall_mult",     3.0,   1.0,   8.0, 0.5)
	# Upward launch velocity (m/s) given to a slammed matador's ragdoll — pops them
	# skyward. ~7 ≈ a 2.5 m hop; 0 disables the launch.
	_reg_f("Abilities", "slam_launch_up",     7.0,   0.0,  15.0, 0.5)
	_reg_f("Abilities", "slam_range",         4.0,   1.0,  10.0, 0.1)
	_reg_f("Abilities", "slam_strength",     14.0,   1.0,  30.0)
	_reg_f("Abilities", "dash_cooldown",   0.5,   0.1,  10.0, 0.1)
	_reg_f("Abilities", "dash_speed",     66.0,   5.0, 200.0)
	# Exponential rate the dash bleeds from dash_speed back to cruise (max_speed).
	# Higher = a snappier bullet that dies off quicker; lower = a longer glide.
	_reg_f("Abilities", "dash_decel",      7.0,   0.5,  30.0, 0.5)
	# ── Roll (homing chain ball) ──────────────────────────────────────────────
	# The bull curls into a ball and ACCELERATES the longer it rolls, from
	# roll_base_speed up to roll_max_speed over roll_rampup_time. It HOMES on the
	# closest matador; on slamming one the ball POPS it (launched skyward via
	# roll_knockup) then CHAINS to the next closest, up to roll_chain_count times.
	# roll_turn is the homing turn rate. When no chain targets remain, the roll ends.
	_reg_f("Roll",     "roll_cooldown",     3.0,   0.5,  15.0, 0.1)
	_reg_f("Roll",     "roll_duration",     2.5,   0.5,   6.0, 0.1)   # max roll time
	_reg_f("Roll",     "roll_chain_count",  2.0,   0.0,   8.0, 1.0)   # extra matadors chained after the first
	_reg_f("Roll",     "roll_base_speed",  28.0,   5.0, 120.0)        # speed at the start of the roll
	_reg_f("Roll",     "roll_max_speed",   95.0,  10.0, 200.0)        # top speed once fully ramped
	_reg_f("Roll",     "roll_rampup_time",  2.0,   0.2,   6.0, 0.1)   # time to reach top speed
	_reg_f("Roll",     "roll_turn",         7.0,   0.2,  20.0)        # homing turn rate (higher = tighter tracking)
	_reg_f("Roll",     "roll_spin_radius",  1.4,   0.3,   4.0, 0.1)   # ball radius for tumble spin (smaller = faster)
	_reg_f("Roll",     "roll_hit_radius",   2.4,   0.5,   6.0, 0.1)   # contact radius that pops the ball
	_reg_f("Roll",     "roll_hit_strength", 22.0,  1.0,  40.0)        # horizontal launch on the pop
	_reg_f("Roll",     "roll_knockup",     11.0,   0.0,  25.0)        # vertical knock-up on the pop
	# ── Debug overlays (see debug_draw.gd) ───────────────────────────────────
	_reg_b("Debug",    "show_collisions",     false)
	# PS1 post-process filter (see ps1_filter.gd on the fullscreen ColorRect):
	# screen-covering CRT/PS1 shader — pixelation, colour-banding + dither, and a
	# rolling scanline. Keys map 1:1 onto the shader uniforms; show_ps1 toggles the
	# whole overlay. Ranges mirror the shader's hint_range so the console can't push
	# a uniform out of bounds.
	_reg_b("PS1",      "show_ps1",             true)
	_reg_f("PS1",      "ps1_pixel_scale",      4.0,  1.0,  8.0,  1.0)
	_reg_f("PS1",      "ps1_color_depth",      5.0,  2.0,  8.0,  1.0)
	_reg_f("PS1",      "ps1_dither_strength",  0.8,  0.0,  2.0,  0.05)
	_reg_f("PS1",      "ps1_scanline_strength", 0.2, 0.0,  1.0,  0.05)
	_reg_f("PS1",      "ps1_roll_speed",       3.0,  0.0, 10.0,  0.1)
	_reg_f("PS1",      "ps1_roll_strength",    1.0,  0.0,  1.0,  0.05)
	_reg_b("Debug",    "show_hitboxes",       false)
	_reg_b("Debug",    "show_bones",          false)
	_reg_b("Debug",    "show_raycasts",       false)
	_reg_b("Debug",    "show_tail",           false)
	_reg_b("Debug",    "show_stats",          false)
	_reg_b("Debug",    "show_states",         false)
	_reg_b("Debug",    "show_animation_name", false)
	_reg_b("Debug",    "show_wireframe",      false)
	_reg_b("Debug",    "show_grid",           false)
	_reg_b("Debug",    "show_axes",           false)
	# Force the on-screen touch controls (joystick + ability buttons) on even on a
	# desktop build — otherwise they appear only when a touchscreen is detected.
	_reg_b("Touch",    "force_touch_controls", false)


func _reg_f(section: String, key: String, default: float,
		min_val: float, max_val: float, step: float = 0.05) -> void:
	_params[key] = {
		"value":   default,
		"default": default,
		"section": section,
		"type":    TYPE_FLOAT,
		"min":     min_val,
		"max":     max_val,
		"step":    step,
	}
	_values[key] = default


func _reg_b(section: String, key: String, default: bool) -> void:
	_params[key] = {
		"value":   default,
		"default": default,
		"section": section,
		"type":    TYPE_BOOL,
	}
	_values[key] = default


## Read a float param. Panics on unknown key — intentional: typos should be loud.
func f(key: String) -> float:
	return _values[key] as float


## Read a bool param.
func b(key: String) -> bool:
	return _values[key] as bool


func set_value(key: String, value: Variant) -> void:
	if not _params.has(key):
		return
	_params[key]["value"] = value
	_values[key] = value
	any_changed.emit(key, value)


## Returns the full metadata dict — used by Console to build UI.
func get_all() -> Dictionary:
	return _params


func save() -> void:
	var cfg := ConfigFile.new()
	for key: String in _params:
		var p: Dictionary = _params[key]
		cfg.set_value(p["section"], key, p["value"])
	cfg.save(SAVE_PATH)


func load_saved() -> void:
	var cfg := ConfigFile.new()
	if cfg.load(SAVE_PATH) != OK:
		return
	for section: String in cfg.get_sections():
		for key: String in cfg.get_section_keys(section):
			if not _params.has(key):
				continue
			var p: Dictionary = _params[key]
			var val: Variant  = cfg.get_value(section, key)
			if p["type"] == TYPE_FLOAT:
				val = clampf(val as float, p["min"] as float, p["max"] as float)
			_params[key]["value"] = val
			_values[key] = val


func reset_all() -> void:
	for key: String in _params:
		var default: Variant = _params[key]["default"]
		_params[key]["value"] = default
		_values[key] = default
	any_changed.emit("__all__", null)


## Reset a single param to its default. No-op on unknown key.
func reset(key: String) -> void:
	if not _params.has(key):
		return
	var default: Variant = _params[key]["default"]
	_params[key]["value"] = default
	_values[key] = default
	any_changed.emit(key, default)


## True when the param currently differs from its registered default.
func is_modified(key: String) -> bool:
	return _params.has(key) and _params[key]["value"] != _params[key]["default"]
