extends Control ## Intro menu: Siim's arena animation as a looping video background. ## Play / Options / Credits / Exit UI are overlaid; Play follows the cape. const GAME_SCENE := "res://scene.tscn" const FADE_TIME := 0.6 # Arena palette: aged gold on dark leather, warm cream on hover. const MENU_GOLD := Color(0.85, 0.66, 0.20) const MENU_CREAM := Color(0.93, 0.88, 0.72) ## Cape centroid per frame (normalized 0-1), detected from red-pixel centroid. ## 34 frames at 24 fps = 1.4167 s loop. const VIDEO_FPS := 24.0 const CAPE_POS: Array = [ Vector2(0.4530, 0.6513), Vector2(0.4521, 0.6516), Vector2(0.4495, 0.6524), Vector2(0.4456, 0.6537), Vector2(0.4415, 0.6550), Vector2(0.4373, 0.6578), Vector2(0.4338, 0.6616), Vector2(0.4306, 0.6669), Vector2(0.4273, 0.6673), Vector2(0.4256, 0.6586), Vector2(0.4258, 0.6428), Vector2(0.4281, 0.6243), Vector2(0.4324, 0.6086), Vector2(0.4382, 0.5997), Vector2(0.4439, 0.5972), Vector2(0.4483, 0.5970), Vector2(0.4512, 0.5986), Vector2(0.4529, 0.6025), Vector2(0.4535, 0.6078), Vector2(0.4533, 0.6135), Vector2(0.4539, 0.6159), Vector2(0.4545, 0.6120), Vector2(0.4546, 0.6050), Vector2(0.4541, 0.5988), Vector2(0.4528, 0.5961), Vector2(0.4508, 0.6012), Vector2(0.4485, 0.6123), Vector2(0.4466, 0.6256), Vector2(0.4456, 0.6388), Vector2(0.4457, 0.6491), Vector2(0.4473, 0.6552), Vector2(0.4492, 0.6567), Vector2(0.4513, 0.6556), Vector2(0.4524, 0.6526), ] ## Friendly labels for the rebindable actions (order defines list order). const ACTION_LABELS: Dictionary = { &"move_forward": "Move Forward", &"move_back": "Move Back", &"move_left": "Move Left", &"move_right": "Move Right", &"ability_kick": "Kick", &"ability_slam": "Slam", &"ability_dash": "Dash", &"ability_roll": "Roll", } @onready var main_panel: Control = $MainPanel @onready var options_panel: Control = $OptionsPanel @onready var credits_panel: Control = $CreditsPanel @onready var rebind_list: VBoxContainer = $OptionsPanel/VBox/Scroll/RebindList @onready var fade_rect: ColorRect = $FadeRect @onready var play_button: Button = $MainPanel/PlayButton @onready var options_button: Button = $MainPanel/OptionsButton @onready var credits_button: Button = $MainPanel/CreditsButton @onready var exit_button: Button = $MainPanel/ExitButton @onready var options_back: Button = $OptionsPanel/VBox/BackButton @onready var credits_back: Button = $CreditsPanel/VBox/BackButton @onready var video: VideoStreamPlayer = $VideoStreamPlayer var _listening_action: StringName = &"" var _listening_button: Button = null func _ready() -> void: Input.mouse_mode = Input.MOUSE_MODE_VISIBLE options_panel.visible = false credits_panel.visible = false fade_rect.color = Color(0, 0, 0, 0) if video.stream != null: video.play() video.finished.connect(video.play) # loop the background clip play_button.pressed.connect(_on_play_pressed) options_button.pressed.connect(_on_options_pressed) credits_button.pressed.connect(_on_credits_pressed) exit_button.pressed.connect(_on_exit_pressed) options_back.pressed.connect(_close_subpanels) credits_back.pressed.connect(_close_subpanels) _apply_theme() _build_version_label() _build_rebind_rows() play_button.grab_focus() # Version banner in the bottom-right corner so it's clear this is an early build. func _build_version_label() -> void: var lbl := Label.new() lbl.text = GameVersion.full() lbl.add_theme_font_override("font", UiFonts.body()) lbl.add_theme_font_size_override("font_size", 16) lbl.add_theme_color_override("font_color", MENU_CREAM) lbl.add_theme_color_override("font_outline_color", Color.BLACK) lbl.add_theme_constant_override("outline_size", 4) 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 = -16.0 lbl.offset_bottom = -12.0 lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(lbl) # Give the whole menu the Cinzel look and swap the generic translucent boxes for # engraved bronze plaques (dark leather fill, gold border with a heavier bottom # edge for a chiselled-tablet feel). func _apply_theme() -> void: _style_button(play_button, true) for btn: Button in [options_button, credits_button, exit_button, options_back, credits_back]: _style_button(btn, false) for path: String in ["OptionsPanel/VBox/TitleLabel", "CreditsPanel/VBox/TitleLabel"]: var title := get_node_or_null(path) as Label if title != null: title.add_theme_font_override("font", UiFonts.title()) title.add_theme_color_override("font_color", MENU_GOLD) for path: String in ["OptionsPanel/VBox/Hint", "CreditsPanel/VBox/Names"]: var lbl := get_node_or_null(path) as Label if lbl != null: lbl.add_theme_font_override("font", UiFonts.body()) lbl.add_theme_color_override("font_color", MENU_CREAM) func _style_button(btn: Button, big: bool) -> void: btn.add_theme_font_override("font", UiFonts.title() if big else UiFonts.body()) btn.add_theme_color_override("font_color", MENU_GOLD) btn.add_theme_color_override("font_hover_color", MENU_CREAM) btn.add_theme_color_override("font_focus_color", MENU_CREAM) btn.add_theme_color_override("font_pressed_color", MENU_CREAM) btn.add_theme_color_override("font_outline_color", Color(0, 0, 0, 1)) btn.add_theme_constant_override("outline_size", 8 if big else 5) btn.add_theme_stylebox_override("normal", _plaque(false)) btn.add_theme_stylebox_override("hover", _plaque(true)) btn.add_theme_stylebox_override("pressed", _plaque(true)) btn.add_theme_stylebox_override("focus", _plaque(true)) func _plaque(hot: bool) -> StyleBoxFlat: var s := StyleBoxFlat.new() s.bg_color = Color(0.14, 0.09, 0.04, 0.94) if hot else Color(0.07, 0.05, 0.03, 0.82) s.set_border_width_all(2) s.border_width_bottom = 5 s.border_color = MENU_CREAM if hot else MENU_GOLD s.set_corner_radius_all(2) s.content_margin_left = 24.0 s.content_margin_right = 24.0 s.content_margin_top = 8.0 s.content_margin_bottom = 10.0 s.shadow_color = Color(0.0, 0.0, 0.0, 0.5) s.shadow_size = 6 return s func _process(_delta: float) -> void: if not main_panel.visible or video.stream == null: return var frame_count := CAPE_POS.size() var t := video.stream_position var frame_idx := int(t * VIDEO_FPS) % frame_count var cape: Vector2 = CAPE_POS[frame_idx] play_button.anchor_left = cape.x play_button.anchor_right = cape.x play_button.anchor_top = cape.y play_button.anchor_bottom = cape.y func _build_rebind_rows() -> void: for child in rebind_list.get_children(): child.queue_free() for action: StringName in ACTION_LABELS: var row := HBoxContainer.new() row.size_flags_horizontal = Control.SIZE_EXPAND_FILL var name_label := Label.new() name_label.text = ACTION_LABELS[action] name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER name_label.add_theme_font_override("font", UiFonts.body()) name_label.add_theme_color_override("font_color", MENU_CREAM) name_label.add_theme_color_override("font_outline_color", Color.BLACK) name_label.add_theme_constant_override("outline_size", 4) row.add_child(name_label) var key_button := Button.new() key_button.custom_minimum_size = Vector2(160, 0) key_button.text = Controls.get_key_label(action) _style_button(key_button, false) key_button.pressed.connect(_on_rebind_pressed.bind(action, key_button)) row.add_child(key_button) rebind_list.add_child(row) func _on_play_pressed() -> void: _set_buttons_disabled(true) var tween := create_tween() tween.tween_property(fade_rect, "color:a", 1.0, FADE_TIME) tween.tween_callback(func() -> void: get_tree().change_scene_to_file(GAME_SCENE)) func _on_options_pressed() -> void: main_panel.visible = false options_panel.visible = true options_back.grab_focus() func _on_credits_pressed() -> void: main_panel.visible = false credits_panel.visible = true credits_back.grab_focus() func _on_exit_pressed() -> void: get_tree().quit() func _close_subpanels() -> void: _cancel_listening() options_panel.visible = false credits_panel.visible = false main_panel.visible = true play_button.grab_focus() func _set_buttons_disabled(value: bool) -> void: play_button.disabled = value options_button.disabled = value credits_button.disabled = value exit_button.disabled = value func _on_rebind_pressed(action: StringName, button: Button) -> void: _cancel_listening() _listening_action = action _listening_button = button button.text = "Press a key…" func _cancel_listening() -> void: if _listening_button != null: _listening_button.text = Controls.get_key_label(_listening_action) _listening_action = &"" _listening_button = null func _unhandled_input(event: InputEvent) -> void: if _listening_action == &"": return if event is InputEventKey and event.pressed and not event.echo: if event.physical_keycode == KEY_ESCAPE: _cancel_listening() else: var new_event := InputEventKey.new() new_event.physical_keycode = event.physical_keycode Controls.rebind(_listening_action, new_event) _listening_button.text = Controls.get_key_label(_listening_action) _listening_action = &"" _listening_button = null get_viewport().set_input_as_handled()