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, dash, slam, roll. const _ABILITY_ACCENT: Array[Color] = [ Color(1.00, 0.62, 0.16), # Kick — ember orange Color(0.45, 0.75, 1.00), # Dash — sky blue Color(0.98, 0.34, 0.20), # Slam — crimson Color(1.00, 0.82, 0.32), # Roll — gold ] const _ABILITY_ICONS: Array[String] = [ "res://HUD/KICK.png", "res://HUD/DASH.png", "res://HUD/SLAM.png", "res://HUD/ROLL.png", ] const _ABILITY_ACTIONS: Array[StringName] = [ &"ability_kick", &"ability_dash", &"ability_slam", &"ability_roll", ] # Fallback label shown in a slot whose icon file fails to load. const _ABILITY_NAMES: Array[String] = ["Kick", "Dash", "Slam", "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 _hp_label: Label var _hp_segments: HBoxContainer var _hp_seg_fills: Array[ColorRect] = [] var _bottom_col: VBoxContainer var _ability_bar: HBoxContainer var _ability_panels: Array[PanelContainer] = [] var _bull_bg: TextureRect # Red damage vignette — a full-screen shader ColorRect flashed on every hit. const _VIGNETTE_SHADER := "res://damage_vignette.gdshader" var _vignette: ColorRect var _vignette_mat: ShaderMaterial # Bull-head backdrop for the gameplay HUD. The image is a wide horned silhouette; its # solid "face" occupies the centred middle band, with horns flaring above. We size the # image so that face band wraps the ability+HP cluster (horns crowning it above). const _BULL_BG := "res://Assets/bull_hud_bg.png" const _BULL_FACE_W: float = 0.529 # face width as a fraction of image width const _BULL_ASPECT: float = 0.2741 # image height / width (267 / 974) const _BULL_FACE_PAD: float = 1.15 # breathing room around the cluster within the face # 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 # Beat between the run ending and the outcome screen. A loss is a short freeze on the # dead bull; a win gives the player a longer stretch to strut a victory lap around the # arena before the screen (and the pause) takes over. const _LOSS_DELAY := 2.0 const _WIN_DELAY := 3.0 var _game_over: bool = false var _result_win: bool = false var _over_root: Control var _over_video: VideoStreamPlayer var _over_label: Label var _over_continue: Button var _over_again: Button func _ready() -> void: layer = 11 process_mode = Node.PROCESS_MODE_ALWAYS _build_controls_panel() _build_tab_hint() _build_bottom_hud() _build_bull_hud_bg() _build_ability_bar() _build_health() _build_version_label() _build_damage_vignette() _build_game_over() _build_touch_controls() _update_control_hints() _find_spawner.call_deferred() # On-screen joystick + ability buttons for touch devices. Self-gating: the overlay # only shows itself when a touchscreen is present (or DP force_touch_controls is on), # so this is a no-op on desktop. func _build_touch_controls() -> void: add_child((load("res://touch_controls.gd") as Script).new()) # Bottom-centre column that groups the gameplay HUD (abilities on top, bull health # directly beneath) so both read as one cluster instead of scattered corners. func _build_bottom_hud() -> void: _bottom_col = VBoxContainer.new() _bottom_col.add_theme_constant_override("separation", 8) _bottom_col.alignment = BoxContainer.ALIGNMENT_END # Collapsed horizontal anchors so the column shrinks to its content and stays # bottom-centred; the bull backdrop is sized against this content rect. _bottom_col.anchor_left = 0.5 _bottom_col.anchor_right = 0.5 _bottom_col.anchor_top = 1.0 _bottom_col.anchor_bottom = 1.0 _bottom_col.grow_horizontal = Control.GROW_DIRECTION_BOTH _bottom_col.grow_vertical = Control.GROW_DIRECTION_BEGIN _bottom_col.offset_bottom = -16.0 _bottom_col.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(_bottom_col) # Bull-head plate that sits behind the ability + health cluster. It's a sibling of the # cluster (not a parent) so it never constrains the cluster's layout; we size and place # it against the cluster's rect in _position_bull_bg. # Skipped on touch/phone: the on-screen thumb cluster already crowds the bottom edge, so # the wide horned plate just adds clutter there. func _build_bull_hud_bg() -> void: if Controls.use_touch_ui(): return _bull_bg = TextureRect.new() if ResourceLoader.exists(_BULL_BG): _bull_bg.texture = load(_BULL_BG) # We size the rect ourselves to the image's aspect, so STRETCH_SCALE fills it # exactly with no letterboxing. _bull_bg.expand_mode = TextureRect.EXPAND_IGNORE_SIZE _bull_bg.stretch_mode = TextureRect.STRETCH_SCALE _bull_bg.modulate.a = 0.95 _bull_bg.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(_bull_bg) # Draw the plate behind the cluster. move_child(_bull_bg, _bottom_col.get_index()) _bottom_col.resized.connect(_position_bull_bg) get_viewport().size_changed.connect(_position_bull_bg) _position_bull_bg.call_deferred() # Wrap the bull's solid "face" band around the current cluster rect: scale the image up # so its face spans the cluster (plus padding), centre it horizontally on the cluster, # and pin its bottom edge to the bottom of the screen so the plate is always seated flush # against the screen edge on any resolution (horns crowning the cluster above). func _position_bull_bg() -> void: if _bull_bg == null or _bottom_col == null: return var r := _bottom_col.get_global_rect() if r.size.x <= 1.0: return var vp_h := get_viewport().get_visible_rect().size.y var bw := (r.size.x * _BULL_FACE_PAD) / _BULL_FACE_W var bh := bw * _BULL_ASPECT _bull_bg.size = Vector2(bw, bh) _bull_bg.position = Vector2( r.position.x + r.size.x * 0.5 - bw * 0.5, vp_h - bh) # 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: _update_control_hints() 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) _player.health_changed.connect(_on_health_changed) _player.hit_taken.connect(_on_hit_taken) _on_health_changed(_player.call(&"get_hp"), _player.call(&"get_max_hp")) 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]: if _result_win: _continue_to_map() else: _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() # A cleared arena advances the run: commit the win and open the map to pick the next node. func _continue_to_map() -> void: get_tree().paused = false Input.mouse_mode = Input.MOUSE_MODE_VISIBLE Run.complete_current_level() get_tree().change_scene_to_file("res://MapScreen.tscn") func _toggle_controls() -> void: _controls_visible = not _controls_visible _toggle_btn.text = "Hide controls" if _controls_visible else "Show controls" _update_control_hints() # The Tab hint and the keyboard controls plaque only make sense with a keyboard, so # they stand down on touch devices — where touch_controls.gd shows the on-screen # joystick and ability buttons instead. Shares Controls.use_touch_ui() with that # overlay so exactly one control scheme shows on any given device. func _update_control_hints() -> void: var touch := Controls.use_touch_ui() # The desktop tiles carry keyboard keycaps that mean nothing on a phone, and their # STOP-filtered panels would sit over the joystick zone eating touches — touch_controls # shows its own ability buttons instead, so drop the desktop bar on touch devices. if _ability_bar != null: _ability_bar.visible = not touch if _game_over or touch: _controls_panel.visible = false _tab_hint.visible = false else: _controls_panel.visible = _controls_visible _tab_hint.visible = not _controls_visible # ── Damage vignette ─────────────────────────────────────────────────────────── # A full-screen ColorRect driven by damage_vignette.gdshader: transparent centre, red # toward the edges, alpha ("flash") tweened up on a hit and eased back to nothing. Built # before the game-over layer so the outcome screen always draws over it. func _build_damage_vignette() -> void: _vignette = ColorRect.new() _vignette.set_anchors_preset(Control.PRESET_FULL_RECT) _vignette.color = Color.WHITE # the shader writes COLOR outright; this is just a base _vignette.mouse_filter = Control.MOUSE_FILTER_IGNORE if ResourceLoader.exists(_VIGNETTE_SHADER): _vignette_mat = ShaderMaterial.new() _vignette_mat.shader = load(_VIGNETTE_SHADER) _vignette_mat.set_shader_parameter("flash", 0.0) _vignette.material = _vignette_mat add_child(_vignette) func _on_hit_taken(_cause: String) -> void: if _vignette_mat == null: return var set_flash := func(v: float) -> void: _vignette_mat.set_shader_parameter("flash", v) create_tween().tween_method(set_flash, 0.9, 0.0, 0.5)\ .set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT) # ── 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 await get_tree().create_timer(_WIN_DELAY if win else _LOSS_DELAY, false, false, true).timeout _over_label.text = "Bull Won!" if _result_win else "Matadors Won!" # A win advances the run (on to the map); a loss ends it (retry from the top). _over_continue.visible = _result_win _over_again.visible = not _result_win var path := _WIN_VIDEO if _result_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. # AspectRatioContainer keeps the 16:9 video's ratio and crops overflow (COVER), # so it fills any screen shape without stretching or leaving dead bars. var arc := AspectRatioContainer.new() arc.set_anchors_preset(Control.PRESET_FULL_RECT) arc.ratio = 16.0 / 9.0 arc.stretch_mode = AspectRatioContainer.STRETCH_COVER arc.alignment_horizontal = AspectRatioContainer.ALIGNMENT_CENTER arc.alignment_vertical = AspectRatioContainer.ALIGNMENT_CENTER arc.process_mode = Node.PROCESS_MODE_ALWAYS arc.mouse_filter = Control.MOUSE_FILTER_IGNORE _over_root.add_child(arc) _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 arc.add_child(_over_video) # Outcome text centred over the video. _over_label = Label.new() _over_label.set_anchors_preset(Control.PRESET_CENTER_TOP) _over_label.grow_horizontal = Control.GROW_DIRECTION_BOTH _over_label.grow_vertical = Control.GROW_DIRECTION_END _over_label.offset_top = 80.0 _over_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _over_label.add_theme_font_override("font", UiFonts.title()) _over_label.add_theme_font_size_override("font_size", 64) _over_label.add_theme_color_override("font_color", _GOLD) _over_label.add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 0.9)) _over_label.add_theme_constant_override("outline_size", 6) _over_label.mouse_filter = Control.MOUSE_FILTER_IGNORE _over_root.add_child(_over_label) # 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) # Win → Continue to the run map; loss → Go Again from the top. Shown per-result in # _show_game_over; both share the Enter shortcut. _over_continue = _make_button("Continue (Enter)") _over_continue.pressed.connect(_continue_to_map) row.add_child(_over_continue) _over_again = _make_button("Go Again (Enter)") _over_again.pressed.connect(_restart) row.add_child(_over_again) var menu := _make_button("Main Menu") menu.pressed.connect(_return_to_menu) row.add_child(menu) # ── Bull health ─────────────────────────────────────────────────────────────── # A row of gold-socketed pips beneath the ability bar — one pip per HP, so raising or # lowering max HP visibly grows or shrinks the row. Lit pips run red and shade toward # crimson as the bull is worn down; spent pips read as empty dark sockets. const _HP_FULL: Color = Color(0.82, 0.16, 0.12) const _HP_EMPTY: Color = Color(0.35, 0.05, 0.05) const _HP_SOCKET: Color = Color(0.05, 0.02, 0.02, 0.92) const _HP_PIP_W: float = 22.0 const _HP_PIP_H: float = 14.0 func _build_health() -> void: var panel := PanelContainer.new() panel.add_theme_stylebox_override( "panel", UiTheme.plaque(UiTheme.LEATHER, UiTheme.BORDER, 4, 12.0, 6.0)) panel.size_flags_horizontal = Control.SIZE_SHRINK_CENTER panel.mouse_filter = Control.MOUSE_FILTER_IGNORE _bottom_col.add_child(panel) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 8) panel.add_child(row) _hp_label = Label.new() _hp_label.text = "BULL" _hp_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _hp_label.add_theme_font_override("font", UiFonts.body()) _hp_label.add_theme_color_override("font_color", _GOLD) _hp_label.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.85)) _hp_label.add_theme_constant_override("outline_size", 3) _hp_label.add_theme_font_size_override("font_size", 14) row.add_child(_hp_label) _hp_segments = HBoxContainer.new() _hp_segments.add_theme_constant_override("separation", 4) _hp_segments.size_flags_vertical = Control.SIZE_SHRINK_CENTER _hp_segments.mouse_filter = Control.MOUSE_FILTER_IGNORE row.add_child(_hp_segments) # The gold-underlined socket behind each pip — shared, non-mutating, so one instance # is safe across every pip. func _hp_socket_style() -> StyleBoxFlat: var s := StyleBoxFlat.new() s.bg_color = _HP_SOCKET s.set_corner_radius_all(3) s.corner_detail = 4 s.border_width_bottom = 2 s.border_color = Color(UiTheme.GOLD.r, UiTheme.GOLD.g, UiTheme.GOLD.b, 0.45) s.content_margin_left = 2.0 s.content_margin_right = 2.0 s.content_margin_top = 2.0 s.content_margin_bottom = 2.0 return s func _rebuild_hp_pips(max_hp: int) -> void: for child in _hp_segments.get_children(): child.queue_free() _hp_seg_fills.clear() var socket_style := _hp_socket_style() for _i: int in maxi(max_hp, 0): var socket := PanelContainer.new() socket.custom_minimum_size = Vector2(_HP_PIP_W, _HP_PIP_H) socket.clip_contents = true socket.add_theme_stylebox_override("panel", socket_style) socket.mouse_filter = Control.MOUSE_FILTER_IGNORE _hp_segments.add_child(socket) var fill := ColorRect.new() fill.color = _HP_FULL fill.mouse_filter = Control.MOUSE_FILTER_IGNORE socket.add_child(fill) _hp_seg_fills.append(fill) func _on_health_changed(current: int, max_hp: int) -> void: if _hp_segments == null: return if _hp_seg_fills.size() != max_hp: _rebuild_hp_pips(max_hp) var cur := maxi(current, 0) var frac := 0.0 if max_hp <= 0 else clampf(float(cur) / float(max_hp), 0.0, 1.0) var lit := _HP_FULL.lerp(_HP_EMPTY, 1.0 - frac) for i: int in _hp_seg_fills.size(): var fill := _hp_seg_fills[i] fill.visible = i < cur fill.color = lit 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.size_flags_horizontal = Control.SIZE_SHRINK_CENTER _ability_bar.mouse_filter = Control.MOUSE_FILTER_IGNORE _bottom_col.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; the bottom column keeps the bar horizontally centred. 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) func _build_ability_slot(idx: int) -> Control: var vbox := VBoxContainer.new() vbox.add_theme_constant_override("separation", 3) 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) # Keycap tucked directly beneath the tile — a solid dark plate wearing the # ability's accent underline, so it reads as attached to its box without ever # covering the artwork. Centred under the tile and tightly spaced. var keycap := PanelContainer.new() keycap.add_theme_stylebox_override("panel", _keycap_style(idx)) keycap.size_flags_horizontal = Control.SIZE_SHRINK_CENTER keycap.mouse_filter = Control.MOUSE_FILTER_IGNORE vbox.add_child(keycap) var key_lbl := Label.new() key_lbl.text = Controls.get_key_label(_ABILITY_ACTIONS[idx]) key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER key_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER key_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE key_lbl.add_theme_font_override("font", UiFonts.body_bold()) key_lbl.add_theme_color_override("font_color", _CREAM) key_lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9)) key_lbl.add_theme_constant_override("outline_size", 3) key_lbl.add_theme_font_size_override("font_size", 14) keycap.add_child(key_lbl) return vbox # Solid keycap plate for an ability bind: near-black fill with the ability's accent # as a chunky top edge, so the accent bridges up to the tile it sits under (reads as # attached) while the key stays on a high-contrast background against busy scenes. func _keycap_style(idx: int) -> StyleBoxFlat: var s := StyleBoxFlat.new() s.bg_color = Color(0.04, 0.03, 0.02, 0.94) s.set_corner_radius_all(4) s.corner_detail = 4 s.set_border_width_all(1) s.border_width_top = 3 s.border_color = _ABILITY_ACCENT[idx] s.content_margin_left = 7.0 s.content_margin_right = 7.0 s.content_margin_top = 2.0 s.content_margin_bottom = 1.0 return s 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_dash"), "Dash", _CREAM) _row(vbox, Controls.get_key_label(&"ability_slam"), "Slam", _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)