extends Node2D ## FOITIN content editor — the "secondary app", built in. ## - edit fighter names/stats/colors -> saved to user://overrides/.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//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) 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")