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 ## 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", } @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) _build_rebind_rows() play_button.grab_focus() 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.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) 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()