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 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-29 21:31:16 +10:00
parent 58beabe4e8
commit 4e2004e740
9 changed files with 168 additions and 10 deletions

View File

@ -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()

30
engine/core/settings.gd Normal file
View File

@ -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)))

View File

@ -0,0 +1 @@
uid://bos4wtpw13ign

View File

@ -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")

View File

@ -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")

View File

@ -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)

View File

@ -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()

View File

@ -0,0 +1 @@
uid://crqxe4whatvmu

View File

@ -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"