feat(ui): the first-person HUD is one thing, in the game's own hand
The reticle was five white ColorRects, pasted byte-identically into three
level runtime scripts. The vitals were two stock ProgressBars with a flat
colour override, inlined 1200 lines into the movement controller. The ammo
count — the number a shooter's player looks at most — was drawn by the LEVEL,
in a black rounded panel that shared nothing with the menus, and it had a
special case in it (`elif active_weapon is DoubleBarrelShotgun`) because that
weapon never declared a name or a capacity.
Everything that describes A PLAYER now belongs to ui/player_hud.gd, and a
level owns the level. The immediate symptom that fixed: the screen had two
ammo panels on it at once, in two different styles, overlapping in the corner.
What each piece now says, rather than merely shows:
ui/crosshair.gd one drawn reticle instead of five rectangles, so it can
BLOOM — open with speed, airtime and each shot, snap shut
on ADS. That is the accuracy readout of the whole game and
five ColorRects could not express it. Every stroke is
drawn twice, ink underneath, because a 2 px white line
disappears over pale concrete exactly when aim matters.
The hit confirmation is the same cross at 45 degrees, so
it lands where the eye already is.
ui/vital_bar.gd segmented, so remaining health can be COUNTED rather than
estimated, with a drain ghost that holds the old value for
a beat — the gap between fill and ghost is the size of the
hit, which a bar that merely gets shorter never tells you.
ui/ability_chip.gd dash and grapple as a wipe across a chip rather than a
tinted JPEG with a 12 px number under it. A shape changing
size is readable in peripheral vision; 12 px type is not.
chain meter promoted out of the debug panel. Movement is this game's
first stated pillar and chaining is its skill expression,
so the count is a score, not a diagnostic.
The numerals moved OFF the bars and beside them. Text centred on a two-tone
bar cannot be given a colour that beats both the fill and the trough — that is
the 2.4:1 debug/ui_contrast_check.gd measured on the old HUD — so this fixes it
at the source rather than leaning on an outline to rescue it.
debug/hud_layout_check.gd measures where every element actually lands, which is
how three real bugs were found rather than squinted at: `set_anchors_preset`
moves the anchors and LEAVES THE OFFSETS, so the reticle spanned the viewport
with a size of exactly (0,0) and drew itself in the top-left corner; a
PRESET_CENTER applied after the ring's own `_ready` undid its centring; and a
BOX CONTAINER's own `alignment` is what pushes content to an edge, not a
SHRINK_END flag on the box, whose minimum width depends on children that may be
hidden. The ammo card was hanging off the right edge of the screen because of
the last one.
DoubleBarrelShotgun now declares `weapon_name` and `max_shells` like every
other weapon, and the four inline `2`s are gone.
spawn smoke 0 failures, 11/11 movement tests, contrast 108/108, layout PASS.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
414026f001
commit
c4bcbc7fd1
+13
-293
@@ -1,18 +1,7 @@
|
||||
extends Node3D
|
||||
class_name LevelRuntime
|
||||
|
||||
var _speed_label: Label
|
||||
var _state_label: Label
|
||||
var _chain_label: Label
|
||||
var _weapon_label: Label
|
||||
var _grapple_icon: TextureRect
|
||||
var _dash_icon: TextureRect
|
||||
var _grapple_label: Label
|
||||
var _dash_label: Label
|
||||
var _fps_label: Label
|
||||
var _standalone_speed_label: Label
|
||||
var _player: CharacterBody3D
|
||||
var _debug_ui_panel: PanelContainer
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -24,7 +13,6 @@ func _ready() -> void:
|
||||
if not has_node("WorldEnvironment"):
|
||||
LevelEnvironment.add_to(self)
|
||||
|
||||
_build_hud()
|
||||
|
||||
# Multiplayer Spawning
|
||||
var spawner = MultiplayerSpawner.new()
|
||||
@@ -252,284 +240,16 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
||||
return player
|
||||
|
||||
|
||||
# ── HUD ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
func _build_hud() -> void:
|
||||
var canvas := CanvasLayer.new()
|
||||
canvas.name = "UI"
|
||||
add_child(canvas)
|
||||
|
||||
# ── Crosshair ─────────────────────────────────────────────────────────
|
||||
var crosshair := Control.new()
|
||||
crosshair.name = "Crosshair"
|
||||
crosshair.set_anchors_preset(Control.PRESET_CENTER)
|
||||
crosshair.custom_minimum_size = Vector2(20, 20)
|
||||
canvas.add_child(crosshair)
|
||||
|
||||
var ch_dot := ColorRect.new()
|
||||
ch_dot.name = "Dot"
|
||||
ch_dot.color = Color(1, 1, 1, 0.8)
|
||||
ch_dot.size = Vector2(4, 4)
|
||||
ch_dot.position = Vector2(-2, -2)
|
||||
crosshair.add_child(ch_dot)
|
||||
|
||||
# Crosshair lines
|
||||
for data in [
|
||||
{"pos": Vector2(-10, -1), "size": Vector2(6, 2)}, # Left
|
||||
{"pos": Vector2(4, -1), "size": Vector2(6, 2)}, # Right
|
||||
{"pos": Vector2(-1, -10), "size": Vector2(2, 6)}, # Top
|
||||
{"pos": Vector2(-1, 4), "size": Vector2(2, 6)}, # Bottom
|
||||
]:
|
||||
var line := ColorRect.new()
|
||||
line.color = Color(1, 1, 1, 0.6)
|
||||
line.position = data["pos"]
|
||||
line.size = data["size"]
|
||||
crosshair.add_child(line)
|
||||
|
||||
# ── Info panel background ─────────────────────────────────────────────
|
||||
_fps_label = Label.new()
|
||||
_fps_label.name = "FPSLabel"
|
||||
_fps_label.text = "FPS: 0"
|
||||
_fps_label.add_theme_font_size_override("font_size", 24)
|
||||
_fps_label.add_theme_color_override("font_color", Color(0.9, 0.9, 0.2))
|
||||
_fps_label.add_theme_color_override("font_outline_color", Color(0, 0, 0))
|
||||
_fps_label.add_theme_constant_override("outline_size", 4)
|
||||
_fps_label.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||
_fps_label.position = Vector2(12, 12)
|
||||
canvas.add_child(_fps_label)
|
||||
|
||||
_standalone_speed_label = Label.new()
|
||||
_standalone_speed_label.name = "StandaloneSpeedLabel"
|
||||
_standalone_speed_label.text = "Speed: 0.0 m/s"
|
||||
_standalone_speed_label.add_theme_font_size_override("font_size", 24)
|
||||
_standalone_speed_label.add_theme_color_override("font_color", Color(0.3, 1.0, 0.5))
|
||||
_standalone_speed_label.add_theme_color_override("font_outline_color", Color(0, 0, 0))
|
||||
_standalone_speed_label.add_theme_constant_override("outline_size", 4)
|
||||
_standalone_speed_label.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||
_standalone_speed_label.position = Vector2(12, 45)
|
||||
canvas.add_child(_standalone_speed_label)
|
||||
|
||||
_debug_ui_panel = PanelContainer.new()
|
||||
_debug_ui_panel.name = "InfoPanel"
|
||||
_debug_ui_panel.offset_left = 12
|
||||
_debug_ui_panel.offset_top = 50
|
||||
_debug_ui_panel.offset_right = 400
|
||||
_debug_ui_panel.offset_bottom = 160
|
||||
var panel_style := StyleBoxFlat.new()
|
||||
panel_style.bg_color = Color(0, 0, 0, 0.55)
|
||||
panel_style.corner_radius_top_left = 8
|
||||
panel_style.corner_radius_top_right = 8
|
||||
panel_style.corner_radius_bottom_left = 8
|
||||
panel_style.corner_radius_bottom_right = 8
|
||||
panel_style.content_margin_left = 12
|
||||
panel_style.content_margin_top = 8
|
||||
panel_style.content_margin_right = 12
|
||||
panel_style.content_margin_bottom = 8
|
||||
_debug_ui_panel.add_theme_stylebox_override("panel", panel_style)
|
||||
canvas.add_child(_debug_ui_panel)
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.name = "InfoVBox"
|
||||
_debug_ui_panel.add_child(vbox)
|
||||
|
||||
_speed_label = Label.new()
|
||||
_speed_label.name = "SpeedLabel"
|
||||
_speed_label.text = "Speed: 0.0 m/s"
|
||||
_speed_label.add_theme_font_size_override("font_size", 18)
|
||||
_speed_label.add_theme_color_override("font_color", Color(0.3, 1.0, 0.5))
|
||||
vbox.add_child(_speed_label)
|
||||
|
||||
_state_label = Label.new()
|
||||
_state_label.name = "StateLabel"
|
||||
_state_label.text = "State: ground"
|
||||
_state_label.add_theme_font_size_override("font_size", 16)
|
||||
_state_label.add_theme_color_override("font_color", Color(0.7, 0.85, 1.0))
|
||||
vbox.add_child(_state_label)
|
||||
|
||||
_chain_label = Label.new()
|
||||
_chain_label.name = "ChainLabel"
|
||||
_chain_label.text = "Chain: 0 (+0%)"
|
||||
_chain_label.add_theme_font_size_override("font_size", 16)
|
||||
_chain_label.add_theme_color_override("font_color", Color(1.0, 0.8, 0.3))
|
||||
vbox.add_child(_chain_label)
|
||||
|
||||
|
||||
# ── Weapon & Ammo Panel ───────────────────────────────────────────────
|
||||
var wp_panel := PanelContainer.new()
|
||||
wp_panel.name = "WeaponPanel"
|
||||
wp_panel.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||
wp_panel.offset_left = -250
|
||||
wp_panel.offset_top = -100
|
||||
wp_panel.offset_right = -20
|
||||
wp_panel.offset_bottom = -20
|
||||
var wp_style := StyleBoxFlat.new()
|
||||
wp_style.bg_color = Color(0, 0, 0, 0.6)
|
||||
wp_style.corner_radius_top_left = 8
|
||||
wp_style.corner_radius_top_right = 8
|
||||
wp_style.corner_radius_bottom_left = 8
|
||||
wp_style.corner_radius_bottom_right = 8
|
||||
wp_style.content_margin_left = 16
|
||||
wp_style.content_margin_top = 12
|
||||
wp_style.content_margin_right = 16
|
||||
wp_style.content_margin_bottom = 12
|
||||
wp_panel.add_theme_stylebox_override("panel", wp_style)
|
||||
wp_panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
canvas.add_child(wp_panel)
|
||||
|
||||
_weapon_label = Label.new()
|
||||
_weapon_label.name = "WeaponLabel"
|
||||
_weapon_label.text = "Unarmed\n0 / 0"
|
||||
_weapon_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
_weapon_label.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
|
||||
_weapon_label.add_theme_font_size_override("font_size", 24)
|
||||
wp_panel.add_child(_weapon_label)
|
||||
|
||||
# ── Utilities Panel ───────────────────────────────────────────────────────
|
||||
var util_panel := PanelContainer.new()
|
||||
util_panel.name = "UtilPanel"
|
||||
util_panel.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||
util_panel.offset_left = -200
|
||||
util_panel.offset_top = -180
|
||||
util_panel.offset_right = -20
|
||||
util_panel.offset_bottom = -110
|
||||
var util_style := StyleBoxFlat.new()
|
||||
util_style.bg_color = Color(0, 0, 0, 0.6)
|
||||
util_style.corner_radius_top_left = 8
|
||||
util_style.corner_radius_top_right = 8
|
||||
util_style.corner_radius_bottom_left = 8
|
||||
util_style.corner_radius_bottom_right = 8
|
||||
util_style.content_margin_left = 12
|
||||
util_style.content_margin_top = 8
|
||||
util_style.content_margin_right = 12
|
||||
util_style.content_margin_bottom = 8
|
||||
util_panel.add_theme_stylebox_override("panel", util_style)
|
||||
util_panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
canvas.add_child(util_panel)
|
||||
|
||||
var util_hbox := HBoxContainer.new()
|
||||
util_hbox.name = "UtilHBox"
|
||||
util_hbox.add_theme_constant_override("separation", 20)
|
||||
util_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
util_panel.add_child(util_hbox)
|
||||
|
||||
var grapple_vbox := VBoxContainer.new()
|
||||
grapple_vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
util_hbox.add_child(grapple_vbox)
|
||||
|
||||
_grapple_icon = TextureRect.new()
|
||||
if ResourceLoader.exists("res://assets/ui/grapple_icon.jpg"):
|
||||
_grapple_icon.texture = load("res://assets/ui/grapple_icon.jpg")
|
||||
_grapple_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
_grapple_icon.custom_minimum_size = Vector2(32, 32)
|
||||
_grapple_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
grapple_vbox.add_child(_grapple_icon)
|
||||
|
||||
_grapple_label = Label.new()
|
||||
_grapple_label.text = "Grapple"
|
||||
_grapple_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_grapple_label.add_theme_font_size_override("font_size", 12)
|
||||
grapple_vbox.add_child(_grapple_label)
|
||||
|
||||
var dash_vbox := VBoxContainer.new()
|
||||
dash_vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
util_hbox.add_child(dash_vbox)
|
||||
|
||||
_dash_icon = TextureRect.new()
|
||||
if ResourceLoader.exists("res://assets/ui/dash_icon.jpg"):
|
||||
_dash_icon.texture = load("res://assets/ui/dash_icon.jpg")
|
||||
_dash_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
_dash_icon.custom_minimum_size = Vector2(32, 32)
|
||||
_dash_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
dash_vbox.add_child(_dash_icon)
|
||||
|
||||
_dash_label = Label.new()
|
||||
_dash_label.text = "Ready"
|
||||
_dash_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_dash_label.add_theme_font_size_override("font_size", 12)
|
||||
dash_vbox.add_child(_dash_label)
|
||||
|
||||
# ── HUD Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not _player or not is_instance_valid(_player):
|
||||
return
|
||||
|
||||
if _debug_ui_panel:
|
||||
_debug_ui_panel.visible = SettingsManager.show_debug_ui
|
||||
|
||||
if _fps_label:
|
||||
_fps_label.visible = SettingsManager.show_fps
|
||||
if _fps_label.visible:
|
||||
_fps_label.text = "FPS: %d" % Engine.get_frames_per_second()
|
||||
|
||||
var vel: Vector3 = _player.velocity
|
||||
var hspeed := Vector2(vel.x, vel.z).length()
|
||||
var total_speed := vel.length()
|
||||
|
||||
if _speed_label:
|
||||
_speed_label.text = "Speed: %.1f m/s (total: %.1f)" % [hspeed, total_speed]
|
||||
|
||||
if _standalone_speed_label:
|
||||
if SettingsManager.show_movement_speed and not SettingsManager.show_debug_ui:
|
||||
_standalone_speed_label.visible = true
|
||||
_standalone_speed_label.text = "Speed: %.1f m/s" % hspeed
|
||||
if _fps_label and _fps_label.visible:
|
||||
_standalone_speed_label.position = Vector2(12, 45)
|
||||
else:
|
||||
_standalone_speed_label.position = Vector2(12, 12)
|
||||
else:
|
||||
_standalone_speed_label.visible = false
|
||||
|
||||
if _state_label:
|
||||
var sm = _player.get_node_or_null("MovementStateMachine")
|
||||
if sm:
|
||||
_state_label.text = "State: %s" % sm.current_state
|
||||
|
||||
if _chain_label:
|
||||
var sm = _player.get_node_or_null("MovementStateMachine")
|
||||
if sm:
|
||||
_chain_label.text = "Chain: %d (+%d%%)" % [sm.chain_count, int(sm.current_chain_bonus * 100)]
|
||||
|
||||
# Update utility indicators
|
||||
if sm.current_state == "grapple" or sm.is_grapple_shooting:
|
||||
_grapple_icon.modulate = Color(0.2, 1.0, 0.4)
|
||||
_grapple_label.text = "Grappling"
|
||||
else:
|
||||
_grapple_icon.modulate = Color(1.0, 1.0, 1.0)
|
||||
_grapple_label.text = "Ready"
|
||||
|
||||
if sm.has_method("get_dash_cooldown_remaining"):
|
||||
var dash_rem = sm.get_dash_cooldown_remaining()
|
||||
if dash_rem > 0.0:
|
||||
_dash_icon.modulate = Color(1.0, 0.3, 0.3)
|
||||
_dash_label.text = "%.1f" % dash_rem
|
||||
else:
|
||||
_dash_icon.modulate = Color(1.0, 1.0, 1.0)
|
||||
_dash_label.text = "Ready"
|
||||
|
||||
if _weapon_label:
|
||||
var wman = _player.get_node_or_null("HeadPivot/Camera3D/WeaponManager")
|
||||
if wman and wman.weapons.has(wman.active_slot):
|
||||
var active_weapon = wman.weapons[wman.active_slot]
|
||||
var w_name = "Weapon"
|
||||
var cur_ammo = 0
|
||||
var max_ammo = 0
|
||||
|
||||
if "weapon_name" in active_weapon:
|
||||
w_name = active_weapon.weapon_name
|
||||
elif active_weapon is DoubleBarrelShotgun:
|
||||
w_name = "Double Barrel Shotgun"
|
||||
|
||||
if "current_ammo" in active_weapon:
|
||||
cur_ammo = active_weapon.current_ammo
|
||||
max_ammo = active_weapon.max_ammo
|
||||
elif "shells" in active_weapon:
|
||||
cur_ammo = active_weapon.shells
|
||||
max_ammo = 2
|
||||
|
||||
if "reloading" in active_weapon and active_weapon.reloading:
|
||||
_weapon_label.text = "%s\nReloading..." % w_name
|
||||
else:
|
||||
_weapon_label.text = "%s\n%d / %d" % [w_name, cur_ammo, max_ammo]
|
||||
else:
|
||||
_weapon_label.text = "Unarmed\n0 / 0"
|
||||
# ── HUD ───────────────────────────────────────────────────────────────
|
||||
#
|
||||
# There is no HUD here any more, and no _process to drive one.
|
||||
#
|
||||
# Reticle, vitals, ammo, ability cooldowns, the chain meter and the debug
|
||||
# readout all belong to ui/player_hud.gd, spawned by the player itself. Every
|
||||
# one of those describes A PLAYER, so a level that owns them has to reach down
|
||||
# into that player's state machine and weapon manager every frame to fill them
|
||||
# in — which is exactly what this did, from three byte-identical copies across
|
||||
# the three level runtimes. The visible symptom was two ammo panels on screen at
|
||||
# once, in two different styles, overlapping in the bottom-right corner.
|
||||
#
|
||||
# A level owns the level.
|
||||
@@ -1,18 +1,7 @@
|
||||
extends Node3D
|
||||
|
||||
|
||||
var _speed_label: Label
|
||||
var _state_label: Label
|
||||
var _chain_label: Label
|
||||
var _weapon_label: Label
|
||||
var _grapple_icon: TextureRect
|
||||
var _dash_icon: TextureRect
|
||||
var _grapple_label: Label
|
||||
var _dash_label: Label
|
||||
var _fps_label: Label
|
||||
var _standalone_speed_label: Label
|
||||
var _player: CharacterBody3D
|
||||
var _debug_ui_panel: PanelContainer
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -29,7 +18,6 @@ func _ready() -> void:
|
||||
LevelEnvironment.add_to(self)
|
||||
LevelMaterials.apply_toon_recursive(self, 0.0)
|
||||
|
||||
_build_hud()
|
||||
|
||||
# Multiplayer Spawning
|
||||
var spawner = MultiplayerSpawner.new()
|
||||
@@ -221,297 +209,8 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
||||
return player
|
||||
|
||||
|
||||
# ── HUD ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
func _build_hud() -> void:
|
||||
var canvas := CanvasLayer.new()
|
||||
canvas.name = "UI"
|
||||
add_child(canvas)
|
||||
|
||||
# ── Crosshair ─────────────────────────────────────────────────────────
|
||||
var crosshair := Control.new()
|
||||
crosshair.name = "Crosshair"
|
||||
crosshair.set_anchors_preset(Control.PRESET_CENTER)
|
||||
crosshair.custom_minimum_size = Vector2(20, 20)
|
||||
canvas.add_child(crosshair)
|
||||
|
||||
var ch_dot := ColorRect.new()
|
||||
ch_dot.name = "Dot"
|
||||
ch_dot.color = Color(1, 1, 1, 0.8)
|
||||
ch_dot.size = Vector2(4, 4)
|
||||
ch_dot.position = Vector2(-2, -2)
|
||||
crosshair.add_child(ch_dot)
|
||||
|
||||
# Crosshair lines
|
||||
for data in [
|
||||
{"pos": Vector2(-10, -1), "size": Vector2(6, 2)}, # Left
|
||||
{"pos": Vector2(4, -1), "size": Vector2(6, 2)}, # Right
|
||||
{"pos": Vector2(-1, -10), "size": Vector2(2, 6)}, # Top
|
||||
{"pos": Vector2(-1, 4), "size": Vector2(2, 6)}, # Bottom
|
||||
]:
|
||||
var line := ColorRect.new()
|
||||
line.color = Color(1, 1, 1, 0.6)
|
||||
line.position = data["pos"]
|
||||
line.size = data["size"]
|
||||
crosshair.add_child(line)
|
||||
|
||||
# ── Info panel background ─────────────────────────────────────────────
|
||||
_fps_label = Label.new()
|
||||
_fps_label.name = "FPSLabel"
|
||||
_fps_label.text = "FPS: 0"
|
||||
_fps_label.add_theme_font_size_override("font_size", 24)
|
||||
_fps_label.add_theme_color_override("font_color", Color(0.9, 0.9, 0.2))
|
||||
_fps_label.add_theme_color_override("font_outline_color", Color(0, 0, 0))
|
||||
_fps_label.add_theme_constant_override("outline_size", 4)
|
||||
_fps_label.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||
_fps_label.position = Vector2(12, 12)
|
||||
canvas.add_child(_fps_label)
|
||||
|
||||
_standalone_speed_label = Label.new()
|
||||
_standalone_speed_label.name = "StandaloneSpeedLabel"
|
||||
_standalone_speed_label.text = "Speed: 0.0 m/s"
|
||||
_standalone_speed_label.add_theme_font_size_override("font_size", 24)
|
||||
_standalone_speed_label.add_theme_color_override("font_color", Color(0.3, 1.0, 0.5))
|
||||
_standalone_speed_label.add_theme_color_override("font_outline_color", Color(0, 0, 0))
|
||||
_standalone_speed_label.add_theme_constant_override("outline_size", 4)
|
||||
_standalone_speed_label.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||
_standalone_speed_label.position = Vector2(12, 45)
|
||||
canvas.add_child(_standalone_speed_label)
|
||||
|
||||
_debug_ui_panel = PanelContainer.new()
|
||||
_debug_ui_panel.name = "InfoPanel"
|
||||
_debug_ui_panel.offset_left = 12
|
||||
_debug_ui_panel.offset_top = 50
|
||||
_debug_ui_panel.offset_right = 400
|
||||
_debug_ui_panel.offset_bottom = 160
|
||||
var panel_style := StyleBoxFlat.new()
|
||||
panel_style.bg_color = Color(0, 0, 0, 0.55)
|
||||
panel_style.corner_radius_top_left = 8
|
||||
panel_style.corner_radius_top_right = 8
|
||||
panel_style.corner_radius_bottom_left = 8
|
||||
panel_style.corner_radius_bottom_right = 8
|
||||
panel_style.content_margin_left = 12
|
||||
panel_style.content_margin_top = 8
|
||||
panel_style.content_margin_right = 12
|
||||
panel_style.content_margin_bottom = 8
|
||||
_debug_ui_panel.add_theme_stylebox_override("panel", panel_style)
|
||||
canvas.add_child(_debug_ui_panel)
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.name = "InfoVBox"
|
||||
_debug_ui_panel.add_child(vbox)
|
||||
|
||||
_speed_label = Label.new()
|
||||
_speed_label.name = "SpeedLabel"
|
||||
_speed_label.text = "Speed: 0.0 m/s"
|
||||
_speed_label.add_theme_font_size_override("font_size", 18)
|
||||
_speed_label.add_theme_color_override("font_color", Color(0.3, 1.0, 0.5))
|
||||
vbox.add_child(_speed_label)
|
||||
|
||||
_state_label = Label.new()
|
||||
_state_label.name = "StateLabel"
|
||||
_state_label.text = "State: ground"
|
||||
_state_label.add_theme_font_size_override("font_size", 16)
|
||||
_state_label.add_theme_color_override("font_color", Color(0.7, 0.85, 1.0))
|
||||
vbox.add_child(_state_label)
|
||||
|
||||
_chain_label = Label.new()
|
||||
_chain_label.name = "ChainLabel"
|
||||
_chain_label.text = "Chain: 0 (+0%)"
|
||||
_chain_label.add_theme_font_size_override("font_size", 16)
|
||||
_chain_label.add_theme_color_override("font_color", Color(1.0, 0.8, 0.3))
|
||||
vbox.add_child(_chain_label)
|
||||
|
||||
|
||||
# ── Weapon & Ammo Panel ───────────────────────────────────────────────
|
||||
var wp_panel := PanelContainer.new()
|
||||
wp_panel.name = "WeaponPanel"
|
||||
wp_panel.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||
wp_panel.offset_left = -250
|
||||
wp_panel.offset_top = -100
|
||||
wp_panel.offset_right = -20
|
||||
wp_panel.offset_bottom = -20
|
||||
var wp_style := StyleBoxFlat.new()
|
||||
wp_style.bg_color = Color(0, 0, 0, 0.6)
|
||||
wp_style.corner_radius_top_left = 8
|
||||
wp_style.corner_radius_top_right = 8
|
||||
wp_style.corner_radius_bottom_left = 8
|
||||
wp_style.corner_radius_bottom_right = 8
|
||||
wp_style.content_margin_left = 16
|
||||
wp_style.content_margin_top = 12
|
||||
wp_style.content_margin_right = 16
|
||||
wp_style.content_margin_bottom = 12
|
||||
wp_panel.add_theme_stylebox_override("panel", wp_style)
|
||||
wp_panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
canvas.add_child(wp_panel)
|
||||
|
||||
_weapon_label = Label.new()
|
||||
_weapon_label.name = "WeaponLabel"
|
||||
_weapon_label.text = "Unarmed\n0 / 0"
|
||||
_weapon_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
_weapon_label.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
|
||||
_weapon_label.add_theme_font_size_override("font_size", 24)
|
||||
wp_panel.add_child(_weapon_label)
|
||||
|
||||
# ── Utilities Panel ───────────────────────────────────────────────────────
|
||||
var util_panel := PanelContainer.new()
|
||||
util_panel.name = "UtilPanel"
|
||||
util_panel.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||
util_panel.offset_left = -200
|
||||
util_panel.offset_top = -180
|
||||
util_panel.offset_right = -20
|
||||
util_panel.offset_bottom = -110
|
||||
var util_style := StyleBoxFlat.new()
|
||||
util_style.bg_color = Color(0, 0, 0, 0.6)
|
||||
util_style.corner_radius_top_left = 8
|
||||
util_style.corner_radius_top_right = 8
|
||||
util_style.corner_radius_bottom_left = 8
|
||||
util_style.corner_radius_bottom_right = 8
|
||||
util_style.content_margin_left = 12
|
||||
util_style.content_margin_top = 8
|
||||
util_style.content_margin_right = 12
|
||||
util_style.content_margin_bottom = 8
|
||||
util_panel.add_theme_stylebox_override("panel", util_style)
|
||||
util_panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
canvas.add_child(util_panel)
|
||||
|
||||
var util_hbox := HBoxContainer.new()
|
||||
util_hbox.name = "UtilHBox"
|
||||
util_hbox.add_theme_constant_override("separation", 20)
|
||||
util_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
util_panel.add_child(util_hbox)
|
||||
|
||||
var grapple_vbox := VBoxContainer.new()
|
||||
grapple_vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
util_hbox.add_child(grapple_vbox)
|
||||
|
||||
_grapple_icon = TextureRect.new()
|
||||
_grapple_icon.texture = load("res://assets/ui/grapple_icon.jpg")
|
||||
_grapple_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
_grapple_icon.custom_minimum_size = Vector2(32, 32)
|
||||
_grapple_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
grapple_vbox.add_child(_grapple_icon)
|
||||
|
||||
_grapple_label = Label.new()
|
||||
_grapple_label.text = "Grapple"
|
||||
_grapple_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_grapple_label.add_theme_font_size_override("font_size", 12)
|
||||
grapple_vbox.add_child(_grapple_label)
|
||||
|
||||
var dash_vbox := VBoxContainer.new()
|
||||
dash_vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
util_hbox.add_child(dash_vbox)
|
||||
|
||||
_dash_icon = TextureRect.new()
|
||||
_dash_icon.texture = load("res://assets/ui/dash_icon.jpg")
|
||||
_dash_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
_dash_icon.custom_minimum_size = Vector2(32, 32)
|
||||
_dash_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
dash_vbox.add_child(_dash_icon)
|
||||
|
||||
_dash_label = Label.new()
|
||||
_dash_label.text = "Ready"
|
||||
_dash_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_dash_label.add_theme_font_size_override("font_size", 12)
|
||||
dash_vbox.add_child(_dash_label)
|
||||
|
||||
# ── Utility: Key Name ─────────────────────────────────────────────────────────
|
||||
|
||||
func _get_key_name(action: String) -> String:
|
||||
if not InputMap.has_action(action):
|
||||
return "?"
|
||||
var events = InputMap.action_get_events(action)
|
||||
for e in events:
|
||||
if e is InputEventKey:
|
||||
var code = e.physical_keycode if e.physical_keycode != 0 else e.keycode
|
||||
return OS.get_keycode_string(code)
|
||||
elif e is InputEventMouseButton:
|
||||
if e.button_index == MOUSE_BUTTON_LEFT: return "LClick"
|
||||
elif e.button_index == MOUSE_BUTTON_RIGHT: return "RClick"
|
||||
elif e.button_index == MOUSE_BUTTON_MIDDLE: return "MClick"
|
||||
return "?"
|
||||
|
||||
# ── HUD Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not _player or not is_instance_valid(_player):
|
||||
return
|
||||
|
||||
if _debug_ui_panel:
|
||||
_debug_ui_panel.visible = SettingsManager.show_debug_ui
|
||||
|
||||
if _fps_label:
|
||||
_fps_label.visible = SettingsManager.show_fps
|
||||
if _fps_label.visible:
|
||||
_fps_label.text = "FPS: %d" % Engine.get_frames_per_second()
|
||||
|
||||
var vel: Vector3 = _player.velocity
|
||||
var hspeed := Vector2(vel.x, vel.z).length()
|
||||
var total_speed := vel.length()
|
||||
|
||||
if _speed_label:
|
||||
_speed_label.text = "Speed: %.1f m/s (total: %.1f)" % [hspeed, total_speed]
|
||||
|
||||
if _standalone_speed_label:
|
||||
if SettingsManager.show_movement_speed and not SettingsManager.show_debug_ui:
|
||||
_standalone_speed_label.visible = true
|
||||
_standalone_speed_label.text = "Speed: %.1f m/s" % hspeed
|
||||
if _fps_label and _fps_label.visible:
|
||||
_standalone_speed_label.position = Vector2(12, 45)
|
||||
else:
|
||||
_standalone_speed_label.position = Vector2(12, 12)
|
||||
else:
|
||||
_standalone_speed_label.visible = false
|
||||
|
||||
if _state_label:
|
||||
var sm = _player.get_node_or_null("MovementStateMachine")
|
||||
if sm:
|
||||
_state_label.text = "State: %s" % sm.current_state
|
||||
|
||||
if _chain_label:
|
||||
var sm = _player.get_node_or_null("MovementStateMachine")
|
||||
if sm:
|
||||
_chain_label.text = "Chain: %d (+%d%%)" % [sm.chain_count, int(sm.current_chain_bonus * 100)]
|
||||
|
||||
# Update utility indicators
|
||||
if sm.current_state == "grapple" or sm.is_grapple_shooting:
|
||||
_grapple_icon.modulate = Color(0.2, 1.0, 0.4)
|
||||
_grapple_label.text = "Grappling"
|
||||
else:
|
||||
_grapple_icon.modulate = Color(1.0, 1.0, 1.0)
|
||||
_grapple_label.text = "Ready"
|
||||
|
||||
var dash_rem = sm.get_dash_cooldown_remaining()
|
||||
if dash_rem > 0.0:
|
||||
_dash_icon.modulate = Color(1.0, 0.3, 0.3)
|
||||
_dash_label.text = "%.1f" % dash_rem
|
||||
else:
|
||||
_dash_icon.modulate = Color(1.0, 1.0, 1.0)
|
||||
_dash_label.text = "Ready"
|
||||
|
||||
if _weapon_label:
|
||||
var wman = _player.get_node_or_null("HeadPivot/Camera3D/WeaponManager")
|
||||
if wman and wman.weapons.has(wman.active_slot):
|
||||
var active_weapon = wman.weapons[wman.active_slot]
|
||||
var w_name = "Weapon"
|
||||
var cur_ammo = 0
|
||||
var max_ammo = 0
|
||||
|
||||
if "weapon_name" in active_weapon:
|
||||
w_name = active_weapon.weapon_name
|
||||
elif active_weapon is DoubleBarrelShotgun:
|
||||
w_name = "Double Barrel Shotgun"
|
||||
|
||||
if "current_ammo" in active_weapon:
|
||||
cur_ammo = active_weapon.current_ammo
|
||||
max_ammo = active_weapon.max_ammo
|
||||
elif "shells" in active_weapon:
|
||||
cur_ammo = active_weapon.shells
|
||||
max_ammo = 2
|
||||
|
||||
if "reloading" in active_weapon and active_weapon.reloading:
|
||||
_weapon_label.text = "%s\nReloading..." % w_name
|
||||
else:
|
||||
_weapon_label.text = "%s\n%d / %d" % [w_name, cur_ammo, max_ammo]
|
||||
else:
|
||||
_weapon_label.text = "Unarmed\n0 / 0"
|
||||
# ── HUD ───────────────────────────────────────────────────────────────
|
||||
#
|
||||
# There is no HUD here any more, and no _process to drive one. See the note in
|
||||
# scenes/maps/level_runtime.gd: everything that describes A PLAYER belongs to
|
||||
# ui/player_hud.gd, which that player spawns for itself.
|
||||
|
||||
Reference in New Issue
Block a user