Bull roll ability, console screen, tests

This commit is contained in:
2026-07-28 23:10:38 +03:00
parent a0b248748c
commit ebb6ec6335
16 changed files with 1494 additions and 283 deletions
+202 -99
View File
@@ -7,12 +7,22 @@ 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"]
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
@@ -25,6 +35,16 @@ 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
@@ -33,10 +53,12 @@ func _ready() -> void:
_build_tab_hint()
_build_ability_bar()
_build_health_bar()
_build_scoreboard()
_find_spawner.call_deferred()
func _process(_delta: float) -> void:
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():
@@ -62,8 +84,12 @@ func _process(_delta: float) -> void:
func _find_spawner() -> void:
var spawners := get_tree().get_nodes_in_group(&"matador_spawn")
if not spawners.is_empty():
_spawner = spawners[0]
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:
@@ -92,6 +118,26 @@ func _on_reset_pressed() -> void:
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:
@@ -107,24 +153,8 @@ func _build_health_bar() -> void:
bar.offset_bottom = 16.0
add_child(bar)
var bg_style := StyleBoxFlat.new()
bg_style.bg_color = _BG
bg_style.corner_radius_top_left = 4
bg_style.corner_radius_top_right = 4
bg_style.corner_radius_bottom_left = 4
bg_style.corner_radius_bottom_right = 4
bg_style.border_width_left = 1
bg_style.border_width_right = 1
bg_style.border_width_top = 1
bg_style.border_width_bottom = 1
bg_style.border_color = _BORDER
bg_style.content_margin_left = 6.0
bg_style.content_margin_right = 6.0
bg_style.content_margin_top = 6.0
bg_style.content_margin_bottom = 6.0
var panel := PanelContainer.new()
panel.add_theme_stylebox_override("panel", bg_style)
panel.add_theme_stylebox_override("panel", _panel_style(_BG, 4, 6.0, 6.0))
bar.add_child(panel)
var inner := HBoxContainer.new()
@@ -145,6 +175,139 @@ func _on_health_changed(new_health: int) -> void:
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)
@@ -154,10 +317,10 @@ func _build_ability_bar() -> void:
bar.anchor_bottom = 1.0
bar.grow_horizontal = Control.GROW_DIRECTION_BOTH
bar.grow_vertical = Control.GROW_DIRECTION_BEGIN
bar.offset_left = -120.0
bar.offset_left = -160.0
bar.offset_bottom = -20.0
add_child(bar)
for i: int in 3:
for i: int in 4:
bar.add_child(_build_ability_slot(i))
@@ -165,25 +328,9 @@ func _build_ability_slot(idx: int) -> Control:
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 4)
var bg_style := StyleBoxFlat.new()
bg_style.bg_color = _BG
bg_style.corner_radius_top_left = 6
bg_style.corner_radius_top_right = 6
bg_style.corner_radius_bottom_left = 6
bg_style.corner_radius_bottom_right = 6
bg_style.border_width_left = 1
bg_style.border_width_right = 1
bg_style.border_width_top = 1
bg_style.border_width_bottom = 1
bg_style.border_color = _BORDER
bg_style.content_margin_left = 4.0
bg_style.content_margin_right = 4.0
bg_style.content_margin_top = 4.0
bg_style.content_margin_bottom = 4.0
var panel := PanelContainer.new()
panel.custom_minimum_size = Vector2(72.0, 72.0)
panel.add_theme_stylebox_override("panel", bg_style)
panel.add_theme_stylebox_override("panel", _panel_style(_BG, 6, 4.0, 4.0))
vbox.add_child(panel)
var stack := Control.new()
@@ -196,6 +343,16 @@ func _build_ability_slot(idx: int) -> Control:
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)
@@ -245,23 +402,7 @@ func _build_tab_hint() -> void:
func _build_controls_panel() -> void:
_controls_panel = PanelContainer.new()
var bg := StyleBoxFlat.new()
bg.bg_color = _BG
bg.corner_radius_top_left = 7
bg.corner_radius_top_right = 7
bg.corner_radius_bottom_left = 7
bg.corner_radius_bottom_right = 7
bg.border_width_left = 1
bg.border_width_right = 1
bg.border_width_top = 1
bg.border_width_bottom = 1
bg.border_color = _BORDER
bg.content_margin_left = 14.0
bg.content_margin_right = 14.0
bg.content_margin_top = 10.0
bg.content_margin_bottom = 10.0
_controls_panel.add_theme_stylebox_override("panel", bg)
_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
@@ -289,6 +430,7 @@ func _build_controls_panel() -> void:
_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)
@@ -320,37 +462,8 @@ func _make_button(label_text: String) -> Button:
btn.text = label_text
btn.process_mode = Node.PROCESS_MODE_ALWAYS
var normal := StyleBoxFlat.new()
normal.bg_color = Color(0.18, 0.12, 0.04, 0.90)
normal.corner_radius_top_left = 5
normal.corner_radius_top_right = 5
normal.corner_radius_bottom_left = 5
normal.corner_radius_bottom_right = 5
normal.border_width_left = 1
normal.border_width_right = 1
normal.border_width_top = 1
normal.border_width_bottom = 1
normal.border_color = _BORDER
normal.content_margin_left = 12.0
normal.content_margin_right = 12.0
normal.content_margin_top = 6.0
normal.content_margin_bottom = 6.0
var hover := StyleBoxFlat.new()
hover.bg_color = Color(0.30, 0.20, 0.06, 0.95)
hover.corner_radius_top_left = 5
hover.corner_radius_top_right = 5
hover.corner_radius_bottom_left = 5
hover.corner_radius_bottom_right = 5
hover.border_width_left = 1
hover.border_width_right = 1
hover.border_width_top = 1
hover.border_width_bottom = 1
hover.border_color = _GOLD
hover.content_margin_left = 12.0
hover.content_margin_right = 12.0
hover.content_margin_top = 6.0
hover.content_margin_bottom = 6.0
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)
@@ -368,17 +481,7 @@ func _row(parent: VBoxContainer, key: String, desc: String, desc_color: Color) -
parent.add_child(row)
var badge := PanelContainer.new()
var bs := StyleBoxFlat.new()
bs.bg_color = _BADGE
bs.corner_radius_top_left = 3
bs.corner_radius_top_right = 3
bs.corner_radius_bottom_left = 3
bs.corner_radius_bottom_right = 3
bs.content_margin_left = 6.0
bs.content_margin_right = 6.0
bs.content_margin_top = 2.0
bs.content_margin_bottom = 2.0
badge.add_theme_stylebox_override("panel", bs)
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)