Files
Bullosseum/hud.gd
T

502 lines
16 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends CanvasLayer
const _GOLD := Color(1.00, 0.78, 0.22)
const _CREAM := Color(0.88, 0.83, 0.72)
const _MUTED := Color(0.65, 0.60, 0.48)
const _BG := Color(0.06, 0.04, 0.02, 0.78)
const _BADGE := Color(0.16, 0.10, 0.03, 0.90)
const _BORDER := Color(0.65, 0.48, 0.15, 0.55)
# Scoring: each kill scores _KILL_BASE × the live combo count; the combo grows with
# every kill inside _COMBO_WINDOW seconds of the last and resets when that lapses.
const _KILL_BASE: int = 100
const _COMBO_WINDOW: float = 3.0
const _ABILITY_ICONS: Array[String] = [
"res://HUD/HUD_0000_ability_JALG.png",
"res://HUD/HUD_0001_ability_SLAM.png",
"res://HUD/HUD_0002_ability_DASH.png",
"res://HUD/HUD_0003_ability_ROLL.png",
]
const _ABILITY_ACTIONS: Array[StringName] = [
&"ability_kick", &"ability_slam", &"ability_dash", &"ability_roll",
]
# Fallback label shown in a slot whose icon file doesn't exist yet (e.g. ROLL).
const _ABILITY_NAMES: Array[String] = ["Kick", "Slam", "Dash", "Roll"]
var _controls_visible: bool = false
var _controls_panel: PanelContainer
var _toggle_btn: Button
var _tab_hint: Label
var _spawner: Node
var _player: Node = null
var _cd_overlays: Array[ColorRect] = []
var _cd_labels: Array[Label] = []
var _health_segments: Array[ColorRect] = []
var _health_connected: bool = false
var _score: int = 0
var _combo: int = 0
var _combo_timer: float = 0.0
var _score_label: Label
var _wave_label: Label
var _combo_label: Label
var _combo_track: ColorRect
var _combo_fill: ColorRect
var _banner_label: Label
func _ready() -> void:
layer = 11
process_mode = Node.PROCESS_MODE_ALWAYS
_build_controls_panel()
_build_tab_hint()
_build_ability_bar()
_build_health_bar()
_build_scoreboard()
_find_spawner.call_deferred()
func _process(delta: float) -> void:
_update_combo(delta)
if _player == null:
var players := get_tree().get_nodes_in_group(&"player")
if not players.is_empty():
_player = players[0]
return
if not _health_connected:
_health_connected = true
_player.health_changed.connect(_on_health_changed)
_on_health_changed(_player.health)
for i: int in _cd_overlays.size():
var fraction: float = _player.call(&"ability_cooldown_fraction", i)
var ov := _cd_overlays[i]
ov.anchor_top = 1.0 - fraction
ov.anchor_bottom = 1.0
var remaining: float = _player.call(&"ability_cooldown_remaining", i)
var lbl := _cd_labels[i]
if remaining > 0.0:
lbl.text = "%.1fs" % remaining
lbl.visible = true
else:
lbl.visible = false
func _find_spawner() -> void:
var spawners := get_tree().get_nodes_in_group(&"matador_spawn")
if spawners.is_empty():
return
_spawner = spawners[0]
_spawner.matador_killed.connect(_on_matador_killed)
_spawner.wave_changed.connect(_on_wave_changed)
_on_wave_changed(_spawner.current_wave())
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.echo:
if event.physical_keycode == KEY_TAB:
_toggle_controls()
elif event.physical_keycode == KEY_ESCAPE:
_return_to_menu()
func _return_to_menu() -> void:
get_tree().paused = false
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
get_tree().change_scene_to_file("res://MainMenu.tscn")
func _toggle_controls() -> void:
_controls_visible = not _controls_visible
_controls_panel.visible = _controls_visible
_tab_hint.visible = not _controls_visible
_toggle_btn.text = "Hide controls" if _controls_visible else "Show controls"
func _on_reset_pressed() -> void:
if get_tree().paused:
get_tree().paused = false
if _spawner:
_spawner.reset_spawn()
_score = 0
_score_label.text = "SCORE 0"
_reset_combo()
# A bordered, rounded, gold-trimmed panel background — the HUD's one repeated look.
# Margins are symmetric (L=R, T=B); pass a transparent border to draw none.
func _panel_style(bg: Color, corner: int, h_margin: float, v_margin: float,
border: Color = _BORDER) -> StyleBoxFlat:
var s := StyleBoxFlat.new()
s.bg_color = bg
s.set_corner_radius_all(corner)
if border.a > 0.0:
s.set_border_width_all(1)
s.border_color = border
s.content_margin_left = h_margin
s.content_margin_right = h_margin
s.content_margin_top = v_margin
s.content_margin_bottom = v_margin
return s
func _build_health_bar() -> void:
var bar := HBoxContainer.new()
bar.add_theme_constant_override("separation", 4)
bar.anchor_left = 0.5
bar.anchor_right = 0.5
bar.anchor_top = 0.0
bar.anchor_bottom = 0.0
bar.grow_horizontal = Control.GROW_DIRECTION_BOTH
bar.offset_left = -90.0
bar.offset_top = 16.0
bar.offset_bottom = 16.0
add_child(bar)
var panel := PanelContainer.new()
panel.add_theme_stylebox_override("panel", _panel_style(_BG, 4, 6.0, 6.0))
bar.add_child(panel)
var inner := HBoxContainer.new()
inner.add_theme_constant_override("separation", 3)
panel.add_child(inner)
for i: int in 10:
var seg := ColorRect.new()
seg.custom_minimum_size = Vector2(14.0, 14.0)
seg.color = Color(0.72, 0.08, 0.08)
inner.add_child(seg)
_health_segments.append(seg)
func _on_health_changed(new_health: int) -> void:
for i: int in _health_segments.size():
_health_segments[i].color = Color(0.72, 0.08, 0.08) if i < new_health \
else Color(0.20, 0.10, 0.10)
# ── Scoreboard: score · wave · kill-combo ──────────────────────────────────────
func _build_scoreboard() -> void:
var panel := PanelContainer.new()
panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
panel.anchor_left = 1.0
panel.anchor_right = 1.0
panel.anchor_top = 0.0
panel.anchor_bottom = 0.0
panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
panel.offset_left = -172.0
panel.offset_right = -14.0
panel.offset_top = 14.0
panel.add_theme_stylebox_override("panel", _panel_style(_BG, 6, 12.0, 8.0))
add_child(panel)
var vbox := VBoxContainer.new()
vbox.mouse_filter = Control.MOUSE_FILTER_IGNORE
vbox.add_theme_constant_override("separation", 2)
panel.add_child(vbox)
_score_label = _score_line(vbox, "SCORE 0", _GOLD, 20)
_wave_label = _score_line(vbox, "WAVE 1", _CREAM, 14)
_combo_label = _score_line(vbox, "", _CREAM, 16)
_combo_track = ColorRect.new()
_combo_track.color = Color(0.0, 0.0, 0.0, 0.4)
_combo_track.custom_minimum_size = Vector2(0.0, 4.0)
_combo_track.mouse_filter = Control.MOUSE_FILTER_IGNORE
_combo_track.visible = false
vbox.add_child(_combo_track)
_combo_fill = ColorRect.new()
_combo_fill.color = _GOLD
_combo_fill.set_anchors_preset(Control.PRESET_FULL_RECT)
_combo_fill.mouse_filter = Control.MOUSE_FILTER_IGNORE
_combo_track.add_child(_combo_fill)
# Centre banner that flashes on each new wave.
_banner_label = Label.new()
_banner_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
_banner_label.anchor_left = 0.5
_banner_label.anchor_right = 0.5
_banner_label.anchor_top = 0.26
_banner_label.anchor_bottom = 0.26
_banner_label.grow_horizontal = Control.GROW_DIRECTION_BOTH
_banner_label.grow_vertical = Control.GROW_DIRECTION_BOTH
_banner_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_banner_label.add_theme_color_override("font_color", _GOLD)
_banner_label.add_theme_font_size_override("font_size", 44)
_banner_label.modulate.a = 0.0
add_child(_banner_label)
func _score_line(parent: VBoxContainer, text: String, color: Color, size: int) -> Label:
var lbl := Label.new()
lbl.text = text
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
lbl.add_theme_color_override("font_color", color)
lbl.add_theme_font_size_override("font_size", size)
parent.add_child(lbl)
return lbl
# Every kill lengthens the combo; scoring is base × combo so a fast chain is worth
# far more than the same kills spread out — rewarding aggressive, uninterrupted play.
func _on_matador_killed() -> void:
_combo += 1
_combo_timer = _COMBO_WINDOW
_score += _KILL_BASE * _combo
_score_label.text = "SCORE %d" % _score
_pulse(_score_label, 1.15)
if _combo >= 2:
_combo_label.text = "%d× COMBO" % _combo
_combo_label.add_theme_color_override("font_color", _combo_color())
_combo_fill.color = _combo_color()
_combo_track.visible = true
_pulse(_combo_label, 1.25)
else:
_combo_label.text = ""
_combo_track.visible = false
func _on_wave_changed(wave: int) -> void:
_wave_label.text = "WAVE %d" % wave
if wave > 1:
_flash_banner("WAVE %d" % wave)
func _update_combo(delta: float) -> void:
if _combo <= 0:
return
_combo_timer -= delta
if _combo_timer <= 0.0:
_reset_combo()
elif _combo_track.visible:
_combo_fill.anchor_right = clampf(_combo_timer / _COMBO_WINDOW, 0.0, 1.0)
func _reset_combo() -> void:
_combo = 0
_combo_timer = 0.0
_combo_label.text = ""
_combo_track.visible = false
func _combo_color() -> Color:
if _combo >= 8:
return Color(1.0, 0.25, 0.10)
if _combo >= 5:
return Color(1.0, 0.50, 0.10)
if _combo >= 3:
return Color(1.0, 0.85, 0.20)
return _CREAM
func _pulse(ctrl: Control, amount: float) -> void:
ctrl.pivot_offset = ctrl.size * 0.5
ctrl.scale = Vector2.ONE * amount
create_tween().tween_property(ctrl, "scale", Vector2.ONE, 0.25) \
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
func _flash_banner(text: String) -> void:
_banner_label.text = text
_banner_label.modulate.a = 0.0
var tw := create_tween()
tw.tween_property(_banner_label, "modulate:a", 1.0, 0.2)
tw.tween_interval(0.7)
tw.tween_property(_banner_label, "modulate:a", 0.0, 0.6)
func _build_ability_bar() -> void:
var bar := HBoxContainer.new()
bar.add_theme_constant_override("separation", 10)
bar.anchor_left = 0.5
bar.anchor_right = 0.5
bar.anchor_top = 1.0
bar.anchor_bottom = 1.0
bar.grow_horizontal = Control.GROW_DIRECTION_BOTH
bar.grow_vertical = Control.GROW_DIRECTION_BEGIN
bar.offset_left = -160.0
bar.offset_bottom = -20.0
add_child(bar)
for i: int in 4:
bar.add_child(_build_ability_slot(i))
func _build_ability_slot(idx: int) -> Control:
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 4)
var panel := PanelContainer.new()
panel.custom_minimum_size = Vector2(72.0, 72.0)
panel.add_theme_stylebox_override("panel", _panel_style(_BG, 6, 4.0, 4.0))
vbox.add_child(panel)
var stack := Control.new()
stack.set_anchors_preset(Control.PRESET_FULL_RECT)
panel.add_child(stack)
if ResourceLoader.exists(_ABILITY_ICONS[idx]):
var icon := TextureRect.new()
icon.texture = load(_ABILITY_ICONS[idx])
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.set_anchors_preset(Control.PRESET_FULL_RECT)
stack.add_child(icon)
else:
var name_lbl := Label.new()
name_lbl.text = _ABILITY_NAMES[idx]
name_lbl.set_anchors_preset(Control.PRESET_FULL_RECT)
name_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
name_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
name_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
name_lbl.add_theme_color_override("font_color", _GOLD)
name_lbl.add_theme_font_size_override("font_size", 16)
stack.add_child(name_lbl)
var cd_overlay := ColorRect.new()
cd_overlay.color = Color(0.0, 0.0, 0.0, 0.70)
cd_overlay.anchor_left = 0.0
cd_overlay.anchor_right = 1.0
cd_overlay.anchor_top = 1.0
cd_overlay.anchor_bottom = 1.0
cd_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
stack.add_child(cd_overlay)
_cd_overlays.append(cd_overlay)
var cd_lbl := Label.new()
cd_lbl.set_anchors_preset(Control.PRESET_FULL_RECT)
cd_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
cd_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
cd_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
cd_lbl.visible = false
cd_lbl.add_theme_color_override("font_color", Color.WHITE)
cd_lbl.add_theme_font_size_override("font_size", 18)
stack.add_child(cd_lbl)
_cd_labels.append(cd_lbl)
var key_lbl := Label.new()
key_lbl.text = Controls.get_key_label(_ABILITY_ACTIONS[idx])
key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
key_lbl.add_theme_color_override("font_color", _GOLD)
key_lbl.add_theme_font_size_override("font_size", 12)
vbox.add_child(key_lbl)
return vbox
func _build_tab_hint() -> void:
_tab_hint = Label.new()
_tab_hint.text = "Press 'Tab' to see controls"
_tab_hint.anchor_left = 0.0
_tab_hint.anchor_top = 1.0
_tab_hint.anchor_right = 0.0
_tab_hint.anchor_bottom = 1.0
_tab_hint.grow_vertical = Control.GROW_DIRECTION_BEGIN
_tab_hint.offset_left = 20.0
_tab_hint.offset_bottom = -20.0
_tab_hint.add_theme_color_override("font_color", _MUTED)
_tab_hint.add_theme_font_size_override("font_size", 12)
add_child(_tab_hint)
func _build_controls_panel() -> void:
_controls_panel = PanelContainer.new()
_controls_panel.add_theme_stylebox_override("panel", _panel_style(_BG, 7, 14.0, 10.0))
_controls_panel.anchor_left = 0.0
_controls_panel.anchor_top = 1.0
_controls_panel.anchor_right = 0.0
_controls_panel.anchor_bottom = 1.0
_controls_panel.grow_vertical = Control.GROW_DIRECTION_BEGIN
_controls_panel.offset_left = 20.0
_controls_panel.offset_bottom = -20.0
_controls_panel.visible = false
add_child(_controls_panel)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 5)
_controls_panel.add_child(vbox)
var move_keys := "%s %s %s %s" % [
Controls.get_key_label(&"move_forward"),
Controls.get_key_label(&"move_left"),
Controls.get_key_label(&"move_back"),
Controls.get_key_label(&"move_right"),
]
_row(vbox, move_keys, "Move (not intended)", _MUTED)
_row(vbox, "LMB (hold)", "Right horn", _CREAM)
_row(vbox, "RMB (hold)", "Left horn", _CREAM)
_row(vbox, Controls.get_key_label(&"ability_kick"), "Kick", _CREAM)
_row(vbox, Controls.get_key_label(&"ability_slam"), "Slam", _CREAM)
_row(vbox, Controls.get_key_label(&"ability_dash"), "Dash", _CREAM)
_row(vbox, Controls.get_key_label(&"ability_roll"), "Roll (bank off walls!)", _CREAM)
_row(vbox, "Scroll", "Zoom", _CREAM)
_row(vbox, "R", "Respawn matadors (cooldown)", _MUTED)
_row(vbox, "Tab", "Toggle controls", _MUTED)
_row(vbox, "Esc", "Back to menu", _MUTED)
var sep := HSeparator.new()
var sep_style := StyleBoxFlat.new()
sep_style.bg_color = Color(0.65, 0.48, 0.15, 0.30)
sep_style.content_margin_top = 4.0
sep_style.content_margin_bottom = 4.0
sep.add_theme_stylebox_override("separator", sep_style)
vbox.add_child(sep)
var hbox := HBoxContainer.new()
hbox.add_theme_constant_override("separation", 8)
vbox.add_child(hbox)
_toggle_btn = _make_button("Show controls")
_toggle_btn.pressed.connect(_toggle_controls)
hbox.add_child(_toggle_btn)
var reset_btn := _make_button("Reset")
reset_btn.pressed.connect(_on_reset_pressed)
hbox.add_child(reset_btn)
func _make_button(label_text: String) -> Button:
var btn := Button.new()
btn.text = label_text
btn.process_mode = Node.PROCESS_MODE_ALWAYS
var normal := _panel_style(Color(0.18, 0.12, 0.04, 0.90), 5, 12.0, 6.0)
var hover := _panel_style(Color(0.30, 0.20, 0.06, 0.95), 5, 12.0, 6.0, _GOLD)
btn.add_theme_stylebox_override("normal", normal)
btn.add_theme_stylebox_override("hover", hover)
btn.add_theme_stylebox_override("pressed", hover)
btn.add_theme_color_override("font_color", _GOLD)
btn.add_theme_color_override("font_hover_color", _GOLD)
btn.add_theme_font_size_override("font_size", 12)
return btn
func _row(parent: VBoxContainer, key: String, desc: String, desc_color: Color) -> void:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 10)
parent.add_child(row)
var badge := PanelContainer.new()
badge.add_theme_stylebox_override("panel", _panel_style(_BADGE, 3, 6.0, 2.0, Color(0, 0, 0, 0)))
badge.custom_minimum_size = Vector2(100.0, 0.0)
row.add_child(badge)
var key_lbl := Label.new()
key_lbl.text = key
key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
key_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
key_lbl.add_theme_color_override("font_color", _GOLD)
key_lbl.add_theme_font_size_override("font_size", 12)
badge.add_child(key_lbl)
var desc_lbl := Label.new()
desc_lbl.text = desc
desc_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
desc_lbl.add_theme_color_override("font_color", desc_color)
desc_lbl.add_theme_font_size_override("font_size", 12)
row.add_child(desc_lbl)