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)