extends CanvasLayer ## T-1088 numeric locomotion instrumentation HUD (design §10.1): tick, tick_rate, ## stance, player tile, render speed, clip + speed_scale, snapshot age, yaw ## target/actual, store size. Sandbox-only. Cadence sync is verifiable numerically ## on screen, not just by eye — tuning NATIVE_MPS is a read-the-HUD job. ## ## All component reads go through has_method guards so the HUD works from the S1 ## skeleton and lights up ("—" -> numbers) as later slices land. Duck-typed contract: ## PlayerRig (S4): get_render_speed() -> float (m/s), ## get_yaw_target_rad() -> float ## PlayerRig or root (S6): get_current_clip() -> StringName, ## get_anim_speed_scale() -> float ## Greybox (S3): get_store_size() -> int const FONT_SIZE := 13 @onready var _rig: Node = get_node_or_null("../WorldRoot/PlayerRig") @onready var _model_root: Node3D = get_node_or_null("../WorldRoot/PlayerRig/ModelRoot") @onready var _greybox: Node = get_node_or_null("../WorldRoot/Greybox") var _label: Label = null var _last_tick: int = -1 var _last_snapshot_msec: int = -1 func _ready() -> void: layer = 10 _label = Label.new() _label.name = "Readout" _label.position = Vector2(8, 8) _label.add_theme_font_size_override("font_size", FONT_SIZE) _label.add_theme_color_override("font_color", Color(0.85, 0.92, 1.0)) _label.add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 0.9)) _label.add_theme_constant_override("outline_size", 4) add_child(_label) func _process(_delta: float) -> void: # Snapshot age: a consumed-tick change is the arrival signal (world_renderer.gd:33-36 # tick-gate pattern). SimBridge delivery is latest-wins, so consumption time is the # honest age reference for interpolation debugging. if GameState.current_tick != _last_tick: _last_tick = GameState.current_tick _last_snapshot_msec = Time.get_ticks_msec() _label.text = _build_text() func _build_text() -> String: var lines := PackedStringArray() var rate := str(GameState.game_time.get("tick_rate", "—")) lines.append("tick %d rate %s snap age %s" % [GameState.current_tick, rate, _age_text()]) var tile := SandboxSpace.wire_to_tile(GameState.player_position.x, GameState.player_position.y) lines.append("stance %s tile (%d, %d)" % [GameState.player_stance, tile.x, tile.y]) var anim_sources: Array = [_rig, get_parent()] lines.append( "speed %s m/s clip %s scale %s" % [ _fmt(_call_first([_rig], "get_render_speed"), "%.3f"), _fmt(_call_first(anim_sources, "get_current_clip"), "%s"), _fmt(_call_first(anim_sources, "get_anim_speed_scale"), "%.2f"), ] ) var yaw_target: Variant = _call_first([_rig], "get_yaw_target_rad") if yaw_target != null: yaw_target = rad_to_deg(float(yaw_target)) var yaw_actual: Variant = null if _model_root != null: yaw_actual = rad_to_deg(_model_root.rotation.y) lines.append( "yaw target %s actual %s" % [_fmt(yaw_target, "%.1f deg"), _fmt(yaw_actual, "%.1f deg")] ) lines.append("store %s tiles" % _fmt(_call_first([_greybox], "get_store_size"), "%d")) return "\n".join(lines) func _age_text() -> String: if _last_snapshot_msec < 0: return "—" return "%.3f s" % (float(Time.get_ticks_msec() - _last_snapshot_msec) / 1000.0) # First answer from the candidate nodes implementing `method`, else null. static func _call_first(nodes: Array, method: String) -> Variant: for node in nodes: if node != null and (node as Object).has_method(method): return (node as Object).call(method) return null # Format a duck-typed value; "—" until the providing component lands. static func _fmt(value: Variant, format: String) -> String: if value == null: return "—" return format % value