From 4e2004e7405e9c42664dc143551dee8b4013b932 Mon Sep 17 00:00:00 2001 From: m3ultra Date: Wed, 29 Jul 2026 21:31:16 +1000 Subject: [PATCH] Settings page, pause menu, menu navigation; fix packed-app portraits - select screen portraits load via pck-safe loader (the blank-cards bug in the shipped build) - Settings (title menu + persisted user://settings.json): fullscreen, hitbox viewer default, master volume - Esc: pause menu in fights (resume/rematch/select/menu), back-to-title from select/editor/settings - FOITIN_SHOT env hook smoke-tests exported builds end-to-end Co-Authored-By: Claude Opus 5 --- engine/core/match_engine.gd | 50 ++++++++++++++++++++++++- engine/core/settings.gd | 30 +++++++++++++++ engine/core/settings.gd.uid | 1 + engine/scenes/Settings.tscn | 6 +++ engine/scenes/editor.gd | 5 +++ engine/scenes/select.gd | 11 +++--- engine/scenes/settings_menu.gd | 60 ++++++++++++++++++++++++++++++ engine/scenes/settings_menu.gd.uid | 1 + engine/scenes/title.gd | 14 ++++++- 9 files changed, 168 insertions(+), 10 deletions(-) create mode 100644 engine/core/settings.gd create mode 100644 engine/core/settings.gd.uid create mode 100644 engine/scenes/Settings.tscn create mode 100644 engine/scenes/settings_menu.gd create mode 100644 engine/scenes/settings_menu.gd.uid diff --git a/engine/core/match_engine.gd b/engine/core/match_engine.gd index 8aeeade2..128ba338 100644 --- a/engine/core/match_engine.gd +++ b/engine/core/match_engine.gd @@ -17,6 +17,8 @@ var msg_label: Label var darken: ColorRect var tick_count := 0 var match_over := false +var paused := false +var pause_ui: Node2D var finish_window := -1 # >0: loser dizzy, finisher available var finisher_running := false @@ -28,6 +30,8 @@ func _ready() -> void: for arg in OS.get_cmdline_user_args(): if arg.begins_with("--shot="): shot_path = arg.trim_prefix("--shot=") + if shot_path == "" and OS.get_environment("FOITIN_SHOT") != "": + shot_path = ProjectSettings.globalize_path("user://shot.png") _build_stage() _load_fighters() _build_hud() @@ -35,8 +39,50 @@ func _ready() -> void: p2.hp = 60 # demo: one special KOs -> exercises the finisher flow overlay = BoxOverlay.new() overlay.engine = self - overlay.visible = shot_path != "" + overlay.visible = shot_path != "" or Settings.show_boxes add_child(overlay) + _build_pause_ui() + + +func _build_pause_ui() -> void: + pause_ui = Node2D.new() + pause_ui.visible = false + pause_ui.z_index = 100 + add_child(pause_ui) + var dim := ColorRect.new() + dim.color = Color(0, 0, 0, 0.6) + dim.size = Vector2(1280, 720) + pause_ui.add_child(dim) + var lab := Label.new() + lab.text = "PAUSED" + lab.add_theme_font_size_override("font_size", 52) + lab.position = Vector2(555, 200) + pause_ui.add_child(lab) + var entries := [ + ["RESUME (Esc)", func(): _set_paused(false)], + ["REMATCH", func(): get_tree().reload_current_scene()], + ["CHARACTER SELECT", func(): get_tree().change_scene_to_file("res://engine/scenes/Select.tscn")], + ["MAIN MENU", func(): get_tree().change_scene_to_file("res://engine/scenes/Title.tscn")], + ] + for i in entries.size(): + var b := Button.new() + b.text = entries[i][0] + b.position = Vector2(510, 300 + i * 66) + b.size = Vector2(260, 50) + b.add_theme_font_size_override("font_size", 22) + b.pressed.connect(entries[i][1]) + pause_ui.add_child(b) + + +func _set_paused(v: bool) -> void: + paused = v + pause_ui.visible = v + + +func _input(event: InputEvent) -> void: + if event is InputEventKey and event.pressed and not event.echo: + if event.keycode == KEY_ESCAPE and shot_path == "": + _set_paused(not paused) func _build_stage() -> void: @@ -186,7 +232,7 @@ static func _relative(mask: int, facing: int) -> int: ## ---------------------------------------------------------------- tick func _physics_process(_delta: float) -> void: - if p1 == null or p2 == null: + if p1 == null or p2 == null or paused: return if Input.is_key_pressed(KEY_R) and match_over: get_tree().reload_current_scene() diff --git a/engine/core/settings.gd b/engine/core/settings.gd new file mode 100644 index 00000000..23578b96 --- /dev/null +++ b/engine/core/settings.gd @@ -0,0 +1,30 @@ +class_name Settings +## Persistent app settings (user://settings.json). Apply on boot and on save. + +static var fullscreen := false +static var show_boxes := false +static var volume := 80.0 # master, 0..100 + +static func load_and_apply() -> void: + var text := FileAccess.get_file_as_string("user://settings.json") + if not text.is_empty(): + var d = JSON.parse_string(text) + if d is Dictionary: + fullscreen = bool(d.get("fullscreen", fullscreen)) + show_boxes = bool(d.get("show_boxes", show_boxes)) + volume = float(d.get("volume", volume)) + apply() + +static func save() -> void: + var f := FileAccess.open("user://settings.json", FileAccess.WRITE) + f.store_string(JSON.stringify({ + "fullscreen": fullscreen, "show_boxes": show_boxes, "volume": volume, + }, " ")) + f.close() + apply() + +static func apply() -> void: + DisplayServer.window_set_mode( + DisplayServer.WINDOW_MODE_FULLSCREEN if fullscreen + else DisplayServer.WINDOW_MODE_WINDOWED) + AudioServer.set_bus_volume_db(0, linear_to_db(clampf(volume / 100.0, 0.0, 1.0))) diff --git a/engine/core/settings.gd.uid b/engine/core/settings.gd.uid new file mode 100644 index 00000000..db72aff2 --- /dev/null +++ b/engine/core/settings.gd.uid @@ -0,0 +1 @@ +uid://bos4wtpw13ign diff --git a/engine/scenes/Settings.tscn b/engine/scenes/Settings.tscn new file mode 100644 index 00000000..1c49a54d --- /dev/null +++ b/engine/scenes/Settings.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://engine/scenes/settings_menu.gd" id="1"] + +[node name="Settings" type="Node2D"] +script = ExtResource("1") diff --git a/engine/scenes/editor.gd b/engine/scenes/editor.gd index b3b935e7..6f0fa4ba 100644 --- a/engine/scenes/editor.gd +++ b/engine/scenes/editor.gd @@ -182,3 +182,8 @@ func _copy_dir(src: String, dst: String) -> void: da.copy(src + "/" + f, dst + "/" + f) for d in da.get_directories(): _copy_dir(src + "/" + d, dst + "/" + d) + + +func _input(event: InputEvent) -> void: + if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE: + get_tree().change_scene_to_file("res://engine/scenes/Title.tscn") diff --git a/engine/scenes/select.gd b/engine/scenes/select.gd index 42b9cbe5..734d7b13 100644 --- a/engine/scenes/select.gd +++ b/engine/scenes/select.gd @@ -34,12 +34,8 @@ func _ready() -> void: for path in CharacterData.scan_roster(): var d = JSON.parse_string(FileAccess.get_file_as_string(path + "/manifest.json")) var entry := {"path": path, "id": d.get("id", ""), "name": d.get("name", "?"), - "color": Color(d.get("color", "#888888")), "portrait": null} - var pp := ProjectSettings.globalize_path(path + "/portrait.png") - if FileAccess.file_exists(pp): - var img := Image.load_from_file(pp) - if img: - entry.portrait = ImageTexture.create_from_image(img) + "color": Color(d.get("color", "#888888")), + "portrait": MoveData.load_texture_any(path + "/portrait.png")} roster.append(entry) # adaptive grid: shrink cards as the roster grows @@ -118,6 +114,9 @@ func _input(event: InputEvent) -> void: if vs_timer >= 0: return match event.keycode: + KEY_ESCAPE: + get_tree().change_scene_to_file("res://engine/scenes/Title.tscn") + return KEY_A: _move(0, -1) KEY_D: _move(0, 1) KEY_J: _lock(0) diff --git a/engine/scenes/settings_menu.gd b/engine/scenes/settings_menu.gd new file mode 100644 index 00000000..7103e6c0 --- /dev/null +++ b/engine/scenes/settings_menu.gd @@ -0,0 +1,60 @@ +extends Node2D +## Settings page: fullscreen, hitbox viewer default, master volume. + +func _ready() -> void: + var bg := ColorRect.new() + bg.color = Color("#14161c") + bg.size = Vector2(1280, 720) + add_child(bg) + var title := Label.new() + title.text = "SETTINGS" + title.add_theme_font_size_override("font_size", 48) + title.position = Vector2(540, 80) + add_child(title) + + var fs := CheckBox.new() + fs.text = "Fullscreen" + fs.button_pressed = Settings.fullscreen + fs.position = Vector2(480, 220) + fs.add_theme_font_size_override("font_size", 26) + fs.toggled.connect(func(v): Settings.fullscreen = v; Settings.save()) + add_child(fs) + + var boxes := CheckBox.new() + boxes.text = "Show hitboxes (training)" + boxes.button_pressed = Settings.show_boxes + boxes.position = Vector2(480, 280) + boxes.add_theme_font_size_override("font_size", 26) + boxes.toggled.connect(func(v): Settings.show_boxes = v; Settings.save()) + add_child(boxes) + + var vol_label := Label.new() + vol_label.text = "Master volume" + vol_label.position = Vector2(480, 360) + vol_label.add_theme_font_size_override("font_size", 26) + add_child(vol_label) + var vol := HSlider.new() + vol.min_value = 0 + vol.max_value = 100 + vol.value = Settings.volume + vol.position = Vector2(480, 400) + vol.size = Vector2(320, 30) + vol.value_changed.connect(func(v): Settings.volume = v; Settings.save()) + add_child(vol) + + var back := Button.new() + back.text = "BACK (Esc)" + back.position = Vector2(540, 520) + back.size = Vector2(200, 52) + back.add_theme_font_size_override("font_size", 24) + back.pressed.connect(_back) + add_child(back) + + +func _back() -> void: + get_tree().change_scene_to_file("res://engine/scenes/Title.tscn") + + +func _input(event: InputEvent) -> void: + if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE: + _back() diff --git a/engine/scenes/settings_menu.gd.uid b/engine/scenes/settings_menu.gd.uid new file mode 100644 index 00000000..f7431360 --- /dev/null +++ b/engine/scenes/settings_menu.gd.uid @@ -0,0 +1 @@ +uid://crqxe4whatvmu diff --git a/engine/scenes/title.gd b/engine/scenes/title.gd index e39661a3..64a44b1b 100644 --- a/engine/scenes/title.gd +++ b/engine/scenes/title.gd @@ -2,6 +2,13 @@ extends Node2D ## Title screen: PLAY -> character select, EDITOR -> content editor. func _ready() -> void: + # export smoke-test hook: FOITIN_SHOT=1 boots straight into a demo fight + # that saves user://shot.png and quits (match_engine reads the env too) + if OS.get_environment("FOITIN_SHOT") != "": + GameState.p1_pick = "kachujin" + GameState.p2_pick = "decks" + get_tree().change_scene_to_file.call_deferred("res://engine/scenes/Fight.tscn") + return var bg := ColorRect.new() bg.color = Color("#101218") bg.size = Vector2(1280, 720) @@ -19,10 +26,13 @@ func _ready() -> void: sub.position = Vector2(540, 300) add_child(sub) - _button("PLAY", Vector2(540, 420), func(): + Settings.load_and_apply() + _button("PLAY", Vector2(540, 400), func(): get_tree().change_scene_to_file("res://engine/scenes/Select.tscn")) - _button("EDITOR", Vector2(540, 500), func(): + _button("EDITOR", Vector2(540, 470), func(): get_tree().change_scene_to_file("res://engine/scenes/Editor.tscn")) + _button("SETTINGS", Vector2(540, 540), func(): + get_tree().change_scene_to_file("res://engine/scenes/Settings.tscn")) var hint := Label.new() hint.text = "P1: WASD + J/K P2: arrows + O/P"