- 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>
185 lines
5.7 KiB
GDScript
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)
|