foitin/engine/scenes/select.gd
m3ultra 4e2004e740 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>
2026-07-29 21:31:16 +10:00

153 lines
4.3 KiB
GDScript

extends Node2D
## Character select: P1 picks with A/D + J, P2 with arrows + O.
## Roster = whatever folders are under characters/ — same scan as the match.
var roster: Array = [] # [{path, id, name, portrait, color}]
var cursor := [0, 0]
var locked := [false, false]
var cards: Array = []
var cursors_ui: Array = []
var vs_timer := -1
var CARD_W := 220
var CARD_H := 300
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 = "F O I T I N"
title.add_theme_font_size_override("font_size", 64)
title.position = Vector2(490, 40)
add_child(title)
var sub := Label.new()
sub.text = "CHOOSE YOUR FIGHTER"
sub.add_theme_font_size_override("font_size", 24)
sub.modulate = Color(1, 1, 1, 0.6)
sub.position = Vector2(520, 120)
add_child(sub)
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": MoveData.load_texture_any(path + "/portrait.png")}
roster.append(entry)
# adaptive grid: shrink cards as the roster grows
var per_row := 4
if roster.size() > 16:
per_row = 9
CARD_W = 124
CARD_H = 180
elif roster.size() > 8:
per_row = 8
CARD_W = 140
CARD_H = 196
per_row = mini(roster.size(), per_row)
var gap: int = 40 if roster.size() <= 8 else 14
var total_w: int = per_row * (CARD_W + gap) - gap
var x0: float = 640.0 - total_w / 2.0
for i in roster.size():
var card := Node2D.new()
card.position = Vector2(x0 + (i % per_row) * (CARD_W + gap),
170 + int(i / float(per_row)) * (CARD_H + 56) as float)
add_child(card)
var frame := ColorRect.new()
frame.color = Color("#22252e")
frame.size = Vector2(CARD_W, CARD_H)
card.add_child(frame)
var accent := ColorRect.new()
accent.color = roster[i].color
accent.size = Vector2(CARD_W, 8)
accent.position = Vector2(0, CARD_H - 8)
card.add_child(accent)
if roster[i].portrait:
var spr := Sprite2D.new()
spr.texture = roster[i].portrait
spr.centered = false
var pscale: float = (CARD_W - 24) / 160.0
spr.scale = Vector2(pscale, pscale)
spr.position = Vector2(12, 12)
card.add_child(spr)
var nm := Label.new()
nm.text = roster[i].name
nm.add_theme_font_size_override("font_size", 26 if roster.size() <= 8 else 17)
nm.position = Vector2(12, CARD_H - 40)
card.add_child(nm)
cards.append(card)
for p in 2:
var cur := ColorRect.new()
cur.color = Color("#e8c840") if p == 0 else Color("#40a8e8")
cur.size = Vector2(CARD_W + 12, 6)
add_child(cur)
var tag := Label.new()
tag.text = "P%d" % (p + 1)
tag.add_theme_font_size_override("font_size", 20)
cur.add_child(tag)
tag.position = Vector2(-2, 8 if p == 0 else -34)
cursors_ui.append(cur)
var help := Label.new()
help.text = "P1: A/D move, J lock in P2: arrows move, O lock in"
help.modulate = Color(1, 1, 1, 0.4)
help.position = Vector2(440, 660)
add_child(help)
_place_cursors()
func _place_cursors() -> void:
for p in 2:
var card: Node2D = cards[cursor[p]]
cursors_ui[p].position = card.position + Vector2(-6, CARD_H + 14 if p == 0 else -22)
cursors_ui[p].modulate.a = 0.5 if locked[p] else 1.0
func _input(event: InputEvent) -> void:
if not event is InputEventKey or not event.pressed or event.echo:
return
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)
KEY_LEFT: _move(1, -1)
KEY_RIGHT: _move(1, 1)
KEY_O: _lock(1)
_place_cursors()
func _move(p: int, dir: int) -> void:
if locked[p]:
return
cursor[p] = wrapi(cursor[p] + dir, 0, roster.size())
func _lock(p: int) -> void:
locked[p] = true
if locked[0] and locked[1]:
GameState.p1_pick = roster[cursor[0]].id
GameState.p2_pick = roster[cursor[1]].id
var vs := Label.new()
vs.text = "%s VS %s" % [roster[cursor[0]].name, roster[cursor[1]].name]
vs.add_theme_font_size_override("font_size", 48)
vs.position = Vector2(400, 580)
add_child(vs)
vs_timer = 60
func _physics_process(_d: float) -> void:
if vs_timer > 0:
vs_timer -= 1
elif vs_timer == 0:
get_tree().change_scene_to_file("res://engine/scenes/Fight.tscn")