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_dash": "Dash", &"ability_slam": "Slam", &"ability_roll": "Roll", } @onready var main_panel: Control = $MainPanel @onready var options_panel: Control = $OptionsPanel @onready var credits_panel: Control = $CreditsPanel @onready var options_vbox: VBoxContainer = $OptionsPanel/VBox @onready var options_scroll: ScrollContainer = $OptionsPanel/VBox/Scroll @onready var rebind_list: VBoxContainer = $OptionsPanel/VBox/Scroll/RebindList @onready var fade_rect: ColorRect = $FadeRect @onready var play_button: Button = $VideoAspect/CapeLayer/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 = $VideoAspect/VideoStreamPlayer @onready var blur_video: VideoStreamPlayer = $VideoBlur/BlurVideo 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 if blur_video.stream != null: blur_video.play() blur_video.finished.connect(blur_video.play) # loop the blurred backdrop 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_options_toggles() _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) var hint := get_node_or_null("OptionsPanel/VBox/Hint") as Label if hint != null: hint.add_theme_font_override("font", UiFonts.body()) hint.add_theme_color_override("font_color", MENU_CREAM) _style_credits(get_node_or_null("CreditsPanel/VBox/Names") as RichTextLabel) # The credits use a RichTextLabel so the Instagram handles are clickable links # ([url] tags). meta_clicked opens the profile — in the web export OS.shell_open # pops a new browser tab. func _style_credits(names: RichTextLabel) -> void: if names == null: return names.add_theme_font_override("normal_font", UiFonts.body()) names.add_theme_font_override("bold_font", UiFonts.title()) names.add_theme_color_override("default_color", MENU_CREAM) names.meta_clicked.connect(func(meta: Variant) -> void: OS.shell_open(str(meta))) 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 bg := Color(0.14, 0.09, 0.04, 0.94) if hot else Color(0.07, 0.05, 0.03, 0.82) var trim := MENU_CREAM if hot else MENU_GOLD return UiTheme.plaque(bg, trim, 2, 24.0, 9.0) 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 # Player-facing option toggles and audio sliders, seated above the rebind list. func _build_options_toggles() -> void: var idx := options_scroll.get_index() _add_section_label("── Audio ──", idx); idx += 1 _add_slider_row("Master Volume", Settings.volume_master, Settings.set_volume_master, idx); idx += 1 _add_slider_row("Ambient Volume", Settings.volume_ambient, Settings.set_volume_ambient, idx); idx += 1 _add_slider_row("Effects Volume", Settings.volume_effects, Settings.set_volume_effects, idx); idx += 1 _add_toggle_row("Mute All", Settings.mute_all, Settings.set_mute_all, idx); idx += 1 _add_section_label("── Gameplay ──", idx); idx += 1 _add_toggle_row("Screen Shake", Settings.screen_shake, Settings.set_screen_shake, idx); idx += 1 _add_toggle_row("Blood & Gore", Settings.gore, Settings.set_gore, idx); idx += 1 _add_section_label("── Controls ──", idx) func _add_section_label(text: String, at_index: int) -> void: var lbl := Label.new() lbl.text = text lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL lbl.add_theme_font_override("font", UiFonts.title()) lbl.add_theme_font_size_override("font_size", 18) lbl.add_theme_color_override("font_color", MENU_GOLD) lbl.add_theme_color_override("font_outline_color", Color.BLACK) lbl.add_theme_constant_override("outline_size", 4) options_vbox.add_child(lbl) options_vbox.move_child(lbl, at_index) func _add_slider_row(label_text: String, value: float, setter: Callable, at_index: int) -> void: var row := HBoxContainer.new() row.size_flags_horizontal = Control.SIZE_EXPAND_FILL var name_label := Label.new() name_label.text = label_text name_label.custom_minimum_size = Vector2(160, 0) name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER name_label.add_theme_font_override("font", UiFonts.body()) name_label.add_theme_font_size_override("font_size", 20) 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", 5) row.add_child(name_label) var slider := HSlider.new() slider.min_value = 0.0 slider.max_value = 1.0 slider.step = 0.01 slider.value = value slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL slider.custom_minimum_size = Vector2(0, 28) row.add_child(slider) var pct_label := Label.new() pct_label.text = "%d%%" % roundi(value * 100.0) pct_label.custom_minimum_size = Vector2(46, 0) pct_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT pct_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER pct_label.add_theme_font_override("font", UiFonts.body()) pct_label.add_theme_font_size_override("font_size", 18) pct_label.add_theme_color_override("font_color", MENU_GOLD) row.add_child(pct_label) slider.value_changed.connect(func(v: float) -> void: setter.call(v) pct_label.text = "%d%%" % roundi(v * 100.0)) options_vbox.add_child(row) options_vbox.move_child(row, at_index) func _add_toggle_row(label_text: String, value: bool, setter: Callable, at_index: int) -> void: var row := HBoxContainer.new() row.size_flags_horizontal = Control.SIZE_EXPAND_FILL var name_label := Label.new() name_label.text = label_text 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_font_size_override("font_size", 20) 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", 5) row.add_child(name_label) var toggle := CheckButton.new() toggle.button_pressed = value toggle.add_theme_color_override("font_color", MENU_GOLD) toggle.toggled.connect(func(on: bool) -> void: setter.call(on)) row.add_child(toggle) options_vbox.add_child(row) options_vbox.move_child(row, at_index) 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_font_size_override("font_size", 20) 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", 5) 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) Run.start_new_run() # fresh map for this run; the arena is the intro fight 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 play_button.visible = false options_panel.visible = true options_back.grab_focus() func _on_credits_pressed() -> void: main_panel.visible = false play_button.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.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()