416 lines
14 KiB
GDScript
416 lines
14 KiB
GDScript
extends CanvasLayer
|
|
|
|
const _GOLD := Color(1.00, 0.78, 0.22)
|
|
const _CREAM := Color(0.93, 0.88, 0.72)
|
|
const _MUTED := Color(0.66, 0.60, 0.46)
|
|
|
|
# Per-ability trim colour, tying each tile (and its key cap) to that ability's FX.
|
|
# Order matches _ABILITY_ICONS: kick, slam, dash, roll.
|
|
const _ABILITY_ACCENT: Array[Color] = [
|
|
Color(1.00, 0.62, 0.16), # Kick — ember orange
|
|
Color(0.98, 0.34, 0.20), # Slam — crimson
|
|
Color(0.45, 0.75, 1.00), # Dash — sky blue
|
|
Color(1.00, 0.82, 0.32), # Roll — gold
|
|
]
|
|
|
|
const _ABILITY_ICONS: Array[String] = [
|
|
"res://HUD/KICK.png",
|
|
"res://HUD/SLAM.png",
|
|
"res://HUD/DASH.png",
|
|
"res://HUD/ROLL.png",
|
|
]
|
|
const _ABILITY_ACTIONS: Array[StringName] = [
|
|
&"ability_kick", &"ability_slam", &"ability_dash", &"ability_roll",
|
|
]
|
|
# Fallback label shown in a slot whose icon file fails to load.
|
|
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 _died_connected: bool = false
|
|
var _cd_overlays: Array[ColorRect] = []
|
|
var _cd_labels: Array[Label] = []
|
|
var _ability_bar: HBoxContainer
|
|
var _ability_panels: Array[PanelContainer] = []
|
|
|
|
# Win/lose screen. _game_over latches so the result is shown once; _result_win
|
|
# records which way it went (read by the gameplay test).
|
|
const _WIN_VIDEO := "res://videos/bull_win.ogv" # the bull triumphs
|
|
const _LOSS_VIDEO := "res://videos/matador_win.ogv" # the matador triumphs
|
|
|
|
var _game_over: bool = false
|
|
var _result_win: bool = false
|
|
var _over_root: Control
|
|
var _over_video: VideoStreamPlayer
|
|
|
|
|
|
func _ready() -> void:
|
|
layer = 11
|
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
|
_build_controls_panel()
|
|
_build_tab_hint()
|
|
_build_ability_bar()
|
|
_build_version_label()
|
|
_build_game_over()
|
|
_find_spawner.call_deferred()
|
|
|
|
|
|
# Unobtrusive early-build tag in the bottom-right corner.
|
|
func _build_version_label() -> void:
|
|
var lbl := Label.new()
|
|
lbl.text = GameVersion.full()
|
|
lbl.add_theme_color_override("font_color", _MUTED)
|
|
lbl.add_theme_font_size_override("font_size", 12)
|
|
lbl.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
|
lbl.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
|
lbl.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
|
lbl.offset_right = -14.0
|
|
lbl.offset_bottom = -10.0
|
|
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
add_child(lbl)
|
|
|
|
|
|
func _process(_delta: float) -> void:
|
|
if _player == null:
|
|
var players := get_tree().get_nodes_in_group(&"player")
|
|
if not players.is_empty():
|
|
_player = players[0]
|
|
return
|
|
if not _died_connected:
|
|
_died_connected = true
|
|
_player.died.connect(_on_player_died)
|
|
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.all_defeated.connect(_on_all_defeated)
|
|
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if event is InputEventKey and event.pressed and not event.echo:
|
|
if _game_over and event.physical_keycode in [KEY_ENTER, KEY_KP_ENTER]:
|
|
_restart()
|
|
elif event.physical_keycode == KEY_TAB and not _game_over:
|
|
_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 _restart() -> void:
|
|
get_tree().paused = false
|
|
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
|
get_tree().reload_current_scene()
|
|
|
|
|
|
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"
|
|
|
|
|
|
# ── Win / lose ──────────────────────────────────────────────────────────────
|
|
# The bull takes one clean blade and it's over; clear the last matador and it's a
|
|
# victory. Both routes end here — the arena freezes and the result is proclaimed.
|
|
|
|
func _on_player_died(cause: String = "") -> void:
|
|
_show_game_over(false, cause)
|
|
|
|
|
|
func _on_all_defeated() -> void:
|
|
_show_game_over(true)
|
|
|
|
|
|
func _show_game_over(win: bool, _cause: String = "") -> void:
|
|
if _game_over:
|
|
return
|
|
_game_over = true
|
|
_result_win = win
|
|
|
|
if _controls_panel:
|
|
_controls_panel.visible = false
|
|
if _tab_hint:
|
|
_tab_hint.visible = false
|
|
|
|
# Swap in the matching outcome clip and loop it behind the buttons.
|
|
var path := _WIN_VIDEO if win else _LOSS_VIDEO
|
|
if ResourceLoader.exists(path):
|
|
_over_video.stream = load(path)
|
|
_over_video.play()
|
|
|
|
_over_root.visible = true
|
|
_over_root.modulate.a = 0.0
|
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
|
get_tree().paused = true
|
|
|
|
create_tween().tween_property(_over_root, "modulate:a", 1.0, 0.5)
|
|
|
|
|
|
func _build_game_over() -> void:
|
|
_over_root = Control.new()
|
|
_over_root.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
_over_root.process_mode = Node.PROCESS_MODE_ALWAYS
|
|
_over_root.visible = false
|
|
add_child(_over_root)
|
|
|
|
# Full-screen outcome clip (bull win / matador win), looping.
|
|
_over_video = VideoStreamPlayer.new()
|
|
_over_video.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
_over_video.expand = true
|
|
_over_video.process_mode = Node.PROCESS_MODE_ALWAYS
|
|
_over_video.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
_over_video.finished.connect(_over_video.play) # loop the outcome clip
|
|
_over_root.add_child(_over_video)
|
|
|
|
# Buttons overlaid at the bottom of the clip.
|
|
var row := HBoxContainer.new()
|
|
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
row.add_theme_constant_override("separation", 16)
|
|
row.anchor_left = 0.5
|
|
row.anchor_right = 0.5
|
|
row.anchor_top = 1.0
|
|
row.anchor_bottom = 1.0
|
|
row.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
|
row.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
|
row.offset_bottom = -48.0
|
|
_over_root.add_child(row)
|
|
|
|
var again := _make_button("Go Again (Enter)")
|
|
again.pressed.connect(_restart)
|
|
row.add_child(again)
|
|
|
|
var menu := _make_button("Main Menu")
|
|
menu.pressed.connect(_return_to_menu)
|
|
row.add_child(menu)
|
|
|
|
|
|
const _SLOT_SEPARATION: int = 10
|
|
|
|
|
|
func _build_ability_bar() -> void:
|
|
_ability_bar = HBoxContainer.new()
|
|
_ability_bar.add_theme_constant_override("separation", _SLOT_SEPARATION)
|
|
_ability_bar.anchor_left = 0.5
|
|
_ability_bar.anchor_right = 0.5
|
|
_ability_bar.anchor_top = 1.0
|
|
_ability_bar.anchor_bottom = 1.0
|
|
_ability_bar.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
|
_ability_bar.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
|
_ability_bar.offset_bottom = -20.0
|
|
add_child(_ability_bar)
|
|
for i: int in 4:
|
|
_ability_bar.add_child(_build_ability_slot(i))
|
|
_layout_ability_bar()
|
|
get_viewport().size_changed.connect(_layout_ability_bar)
|
|
|
|
|
|
# Size the ability slots as a fraction of the viewport (clamped) so the bar scales
|
|
# with the window, and keep it horizontally centred by matching the HBox offsets.
|
|
func _layout_ability_bar() -> void:
|
|
var vp := get_viewport().get_visible_rect().size
|
|
var slot := clampf(minf(vp.x * 0.055, vp.y * 0.11), 56.0, 104.0)
|
|
for panel: PanelContainer in _ability_panels:
|
|
panel.custom_minimum_size = Vector2(slot, slot)
|
|
var total := slot * 4.0 + float(_SLOT_SEPARATION) * 3.0
|
|
_ability_bar.offset_left = -total * 0.5
|
|
_ability_bar.offset_right = total * 0.5
|
|
|
|
|
|
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", UiTheme.plaque(UiTheme.LEATHER, _ABILITY_ACCENT[idx], 4, 5.0, 5.0))
|
|
vbox.add_child(panel)
|
|
_ability_panels.append(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])
|
|
# Ignore the texture's native size so a large source PNG doesn't blow the
|
|
# slot's minimum size out; scale-to-fit the box while keeping aspect.
|
|
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
|
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
|
icon.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
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_font_override("font", UiFonts.body())
|
|
key_lbl.add_theme_color_override("font_color", _ABILITY_ACCENT[idx].lerp(_CREAM, 0.35))
|
|
key_lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.85))
|
|
key_lbl.add_theme_constant_override("outline_size", 3)
|
|
key_lbl.add_theme_font_size_override("font_size", 13)
|
|
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", UiTheme.plaque(UiTheme.LEATHER, UiTheme.BORDER, 4, 16.0, 12.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", _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 (Powerball: ram the matador!)", _CREAM)
|
|
_row(vbox, "Scroll", "Zoom", _CREAM)
|
|
_row(vbox, "Tab", "Toggle controls", _MUTED)
|
|
_row(vbox, "Esc", "Back to menu", _MUTED)
|
|
|
|
var sep := HSeparator.new()
|
|
sep.add_theme_stylebox_override("separator", UiTheme.rule())
|
|
vbox.add_child(sep)
|
|
|
|
_toggle_btn = _make_button("Show controls")
|
|
_toggle_btn.pressed.connect(_toggle_controls)
|
|
vbox.add_child(_toggle_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 := UiTheme.plaque(Color(0.10, 0.07, 0.03, 0.90), _GOLD, 3, 18.0, 9.0)
|
|
var hover := UiTheme.plaque(Color(0.16, 0.10, 0.04, 0.96), _CREAM, 3, 18.0, 9.0)
|
|
|
|
btn.add_theme_stylebox_override("normal", normal)
|
|
btn.add_theme_stylebox_override("hover", hover)
|
|
btn.add_theme_stylebox_override("pressed", hover)
|
|
btn.add_theme_stylebox_override("focus", hover)
|
|
btn.add_theme_font_override("font", UiFonts.body())
|
|
btn.add_theme_color_override("font_color", _GOLD)
|
|
btn.add_theme_color_override("font_hover_color", _CREAM)
|
|
btn.add_theme_font_size_override("font_size", 18)
|
|
|
|
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", UiTheme.badge())
|
|
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)
|