foitin/engine/scenes/editor.gd
m3ultra 12528b4de0 Title screen, FOITIN editor, pck-safe loaders, export presets
- Title: PLAY / EDITOR entry
- Editor (in-app): edit fighter name/stats/colour -> user://overrides,
  import character folders -> user://characters, add stage backgrounds
  -> user://stages, open content folder
- loaders work in editor, exported .pck, and user:// drop-ins
  (ResourceLoader remap-stripping + byte-buffer fallbacks)
- stage backgrounds render behind fights when present
- export_presets: mac (universal, ad-hoc sign), win x86_64, linux x86_64

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 15:02:37 +10:00

185 lines
5.7 KiB
GDScript

extends Node2D
## FOITIN content editor — the "secondary app", built in.
## - edit fighter names/stats/colors -> saved to user://overrides/<id>.json
## - import a character folder (pack_character output) -> user://characters/
## - add stage background images -> user://stages/
## Everything it writes lives in user:// so it works from the shipped app;
## the game merges overrides and scans user:// folders at load.
var roster: Array = []
var selected := -1
var fields := {}
var status: Label
var list_box: VBoxContainer
const EDITABLE := [
["name", "Name"], ["health", "Health"], ["walk_speed", "Walk speed"],
["back_speed", "Back speed"], ["jump_velocity", "Jump velocity"],
["color", "Accent colour (#hex)"], ["sprite_scale", "Sprite scale"],
]
func _ready() -> void:
DirAccess.make_dir_recursive_absolute("user://overrides")
DirAccess.make_dir_recursive_absolute("user://characters")
DirAccess.make_dir_recursive_absolute("user://stages")
var bg := ColorRect.new()
bg.color = Color("#14161c")
bg.size = Vector2(1280, 720)
add_child(bg)
var title := Label.new()
title.text = "FOITIN EDITOR"
title.add_theme_font_size_override("font_size", 36)
title.position = Vector2(40, 24)
add_child(title)
# left: roster list
var scroll := ScrollContainer.new()
scroll.position = Vector2(40, 90)
scroll.size = Vector2(240, 540)
add_child(scroll)
list_box = VBoxContainer.new()
scroll.add_child(list_box)
_reload_roster()
# right: field editors
var y := 90
for spec in EDITABLE:
var lab := Label.new()
lab.text = spec[1]
lab.position = Vector2(330, y)
lab.modulate = Color(1, 1, 1, 0.6)
add_child(lab)
var edit := LineEdit.new()
edit.position = Vector2(330, y + 24)
edit.size = Vector2(280, 34)
add_child(edit)
fields[spec[0]] = edit
y += 74
_action("SAVE FIGHTER", Vector2(330, y + 6), Color("#3a7a3a"), _save_selected)
_action("Import character folder...", Vector2(700, 90), Color("#2a4a6a"), _pick_character)
_action("Add stage background...", Vector2(700, 150), Color("#2a4a6a"), _pick_stage)
_action("Open content folder", Vector2(700, 210), Color("#4a4a4a"), func():
OS.shell_open(ProjectSettings.globalize_path("user://")))
_action("BACK TO TITLE", Vector2(700, 630), Color("#6a3a3a"), func():
get_tree().change_scene_to_file("res://engine/scenes/Title.tscn"))
var help := Label.new()
help.text = ("Stages: images in user://stages appear behind fights (random pick).\n" +
"Characters: import a folder made by pipeline/pack_character.py —\n" +
"it must contain manifest.json + moves/<move>/frames/*.png.\n" +
"Edits save as overrides; originals are never touched.")
help.position = Vector2(700, 280)
help.modulate = Color(1, 1, 1, 0.5)
add_child(help)
status = Label.new()
status.position = Vector2(330, 660)
status.modulate = Color("#e8c840")
add_child(status)
func _action(text: String, pos: Vector2, col: Color, cb: Callable) -> void:
var b := Button.new()
b.text = text
b.position = pos
b.size = Vector2(300, 44)
b.modulate = col.lightened(0.6)
b.pressed.connect(cb)
add_child(b)
func _reload_roster() -> void:
for c in list_box.get_children():
c.queue_free()
roster = CharacterData.scan_roster()
for i in roster.size():
var path: String = roster[i]
var b := Button.new()
b.text = path.get_file()
b.alignment = HORIZONTAL_ALIGNMENT_LEFT
b.custom_minimum_size = Vector2(220, 34)
var idx := i
b.pressed.connect(func(): _select(idx))
list_box.add_child(b)
func _select(i: int) -> void:
selected = i
var d = JSON.parse_string(FileAccess.get_file_as_string(roster[i] + "/manifest.json"))
var ov_text := FileAccess.get_file_as_string(
"user://overrides/%s.json" % roster[i].get_file())
if not ov_text.is_empty():
var ov = JSON.parse_string(ov_text)
for k in ov:
d[k] = ov[k]
for key in fields:
fields[key].text = str(d.get(key, ""))
status.text = "editing " + roster[i].get_file()
func _save_selected() -> void:
if selected < 0:
status.text = "pick a fighter first"
return
var ov := {}
for key in fields:
var v: String = fields[key].text.strip_edges()
if v == "":
continue
if key in ["health"]:
ov[key] = int(v)
elif key in ["walk_speed", "back_speed", "jump_velocity", "sprite_scale"]:
ov[key] = float(v)
else:
ov[key] = v
var id: String = roster[selected].get_file()
var f := FileAccess.open("user://overrides/%s.json" % id, FileAccess.WRITE)
f.store_string(JSON.stringify(ov, " "))
f.close()
status.text = "saved %s (applies next match)" % id
func _pick_character() -> void:
_dialog(FileDialog.FILE_MODE_OPEN_DIR, func(path: String):
var id := path.get_file()
if not FileAccess.file_exists(path + "/manifest.json"):
status.text = "no manifest.json in that folder"
return
_copy_dir(path, "user://characters/" + id)
_reload_roster()
status.text = "imported " + id)
func _pick_stage() -> void:
_dialog(FileDialog.FILE_MODE_OPEN_FILE, func(path: String):
var dest := "user://stages/" + path.get_file()
DirAccess.copy_absolute(path, ProjectSettings.globalize_path(dest))
status.text = "stage added: " + path.get_file(),
PackedStringArray(["*.png", "*.jpg", "*.jpeg", "*.webp"]))
func _dialog(mode: int, cb: Callable, filters := PackedStringArray()) -> void:
var dlg := FileDialog.new()
dlg.access = FileDialog.ACCESS_FILESYSTEM
dlg.file_mode = mode
dlg.filters = filters
dlg.size = Vector2(900, 600)
add_child(dlg)
if mode == FileDialog.FILE_MODE_OPEN_DIR:
dlg.dir_selected.connect(cb)
else:
dlg.file_selected.connect(cb)
dlg.popup_centered()
func _copy_dir(src: String, dst: String) -> void:
DirAccess.make_dir_recursive_absolute(dst)
var da := DirAccess.open(src)
for f in da.get_files():
da.copy(src + "/" + f, dst + "/" + f)
for d in da.get_directories():
_copy_dir(src + "/" + d, dst + "/" + d)