feat: skin update
This commit is contained in:
@@ -0,0 +1,536 @@
|
||||
@tool
|
||||
extends Control
|
||||
## Skin Catalog Editor — run this scene in the Godot editor to manage all skins.
|
||||
##
|
||||
## USAGE:
|
||||
## Open scenes/tools/skin_catalog_editor.tscn in the editor, then press F6 (Run Current Scene).
|
||||
## Edit skins in the form, then click "💾 Save & Generate" to rewrite:
|
||||
## • scripts/managers/skin_manager.gd (SKIN_CATALOG block)
|
||||
## • server/nakama/tekton_admin.js (SHOP_CATALOG_DEFS block)
|
||||
|
||||
const DATA_PATH := "res://assets/data/skin_catalog_data.json"
|
||||
const SKIN_MANAGER_PATH := "res://scripts/managers/skin_manager.gd"
|
||||
const ADMIN_JS_PATH := "res://server/nakama/tekton_admin.js"
|
||||
|
||||
const CATEGORIES := ["head", "costume", "glove", "accessory"]
|
||||
const RARITIES := ["Common", "Rare", "Epic", "Legendary"]
|
||||
const MODES := ["override", "overlay"]
|
||||
|
||||
# Sentinel markers — must match what's in the target files
|
||||
const BEGIN_SKIN := "## [BEGIN_SKIN_CATALOG]"
|
||||
const END_SKIN := "## [END_SKIN_CATALOG]"
|
||||
const BEGIN_SHOP := "// [BEGIN_SHOP_CATALOG_DEFS]"
|
||||
const END_SHOP := "// [END_SHOP_CATALOG_DEFS]"
|
||||
|
||||
# ─── State ───────────────────────────────────────────────────────────────────
|
||||
var _data: Array = []
|
||||
var _selected_idx: int = -1
|
||||
var _dirty: bool = false
|
||||
|
||||
# ─── UI refs (built in code) ─────────────────────────────────────────────────
|
||||
var _skin_list_vbox: VBoxContainer
|
||||
var _status_label: Label
|
||||
var _form_panel: PanelContainer
|
||||
var _no_sel_label: Label
|
||||
var _form_item_id: LineEdit
|
||||
var _form_name: LineEdit
|
||||
var _form_character: LineEdit
|
||||
var _form_gold: SpinBox
|
||||
var _form_star: SpinBox
|
||||
var _form_category: OptionButton
|
||||
var _form_rarity: OptionButton
|
||||
var _slots_vbox: VBoxContainer
|
||||
var _delete_btn: Button
|
||||
var _save_btn: Button
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
func _ready() -> void:
|
||||
_build_ui()
|
||||
_load_data()
|
||||
_refresh_list()
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# UI Construction
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
func _build_ui() -> void:
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
|
||||
# Root VBox
|
||||
var root := VBoxContainer.new()
|
||||
root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
add_child(root)
|
||||
|
||||
# ── Top bar ──────────────────────────────────────────────────────────────
|
||||
var top := HBoxContainer.new()
|
||||
top.custom_minimum_size.y = 38
|
||||
root.add_child(top)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = " 🎨 Skin Catalog Editor"
|
||||
title.add_theme_font_size_override("font_size", 16)
|
||||
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
top.add_child(title)
|
||||
|
||||
var add_btn := Button.new()
|
||||
add_btn.text = "+ New Skin"
|
||||
add_btn.pressed.connect(_on_add_pressed)
|
||||
top.add_child(add_btn)
|
||||
|
||||
_delete_btn = Button.new()
|
||||
_delete_btn.text = "✕ Delete"
|
||||
_delete_btn.disabled = true
|
||||
_delete_btn.pressed.connect(_on_delete_pressed)
|
||||
top.add_child(_delete_btn)
|
||||
|
||||
_save_btn = Button.new()
|
||||
_save_btn.text = "💾 Save & Generate"
|
||||
_save_btn.pressed.connect(_on_save_pressed)
|
||||
top.add_child(_save_btn)
|
||||
|
||||
# ── Main split: list | form ───────────────────────────────────────────────
|
||||
var hsplit := HSplitContainer.new()
|
||||
hsplit.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
hsplit.split_offset = 230
|
||||
root.add_child(hsplit)
|
||||
|
||||
# LEFT — scrollable skin list
|
||||
var list_scroll := ScrollContainer.new()
|
||||
list_scroll.custom_minimum_size.x = 210
|
||||
list_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
hsplit.add_child(list_scroll)
|
||||
|
||||
_skin_list_vbox = VBoxContainer.new()
|
||||
_skin_list_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
list_scroll.add_child(_skin_list_vbox)
|
||||
|
||||
# RIGHT — form in a scroll container
|
||||
var form_scroll := ScrollContainer.new()
|
||||
form_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
form_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
hsplit.add_child(form_scroll)
|
||||
|
||||
var form_root := VBoxContainer.new()
|
||||
form_root.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
form_scroll.add_child(form_root)
|
||||
|
||||
_no_sel_label = Label.new()
|
||||
_no_sel_label.text = "\n\n← Select a skin from the list to edit it."
|
||||
_no_sel_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
form_root.add_child(_no_sel_label)
|
||||
|
||||
_form_panel = PanelContainer.new()
|
||||
_form_panel.visible = false
|
||||
_form_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
form_root.add_child(_form_panel)
|
||||
|
||||
var form_vbox := VBoxContainer.new()
|
||||
form_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_form_panel.add_child(form_vbox)
|
||||
|
||||
# Form fields
|
||||
form_vbox.add_child(_section_label("── Item Info ───────────────────────────"))
|
||||
_form_item_id = _field(form_vbox, "Item ID", "e.g. oldpop_hat1")
|
||||
_form_name = _field(form_vbox, "Display Name", "e.g. Oldpop Hat I")
|
||||
_form_character = _field(form_vbox, "Character (node)", "e.g. Oldpop, Masbro, Bob")
|
||||
_form_gold = _spinbox(form_vbox, "Gold Price", 0, 99999)
|
||||
_form_star = _spinbox(form_vbox, "Star Price", 0, 99999)
|
||||
_form_category = _option(form_vbox, "Category", CATEGORIES)
|
||||
_form_rarity = _option(form_vbox, "Rarity", RARITIES)
|
||||
|
||||
# ── Slots section ─────────────────────────────────────────────────────────
|
||||
var slots_hdr := HBoxContainer.new()
|
||||
form_vbox.add_child(slots_hdr)
|
||||
|
||||
var slots_title := Label.new()
|
||||
slots_title.text = "── Material Slots ──────────────────────────"
|
||||
slots_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
slots_hdr.add_child(slots_title)
|
||||
|
||||
var add_slot_btn := Button.new()
|
||||
add_slot_btn.text = "+ Add Slot"
|
||||
add_slot_btn.pressed.connect(_on_add_slot_pressed)
|
||||
slots_hdr.add_child(add_slot_btn)
|
||||
|
||||
var slot_header_row := HBoxContainer.new()
|
||||
form_vbox.add_child(slot_header_row)
|
||||
for col_text in ["Mesh Node Name", "Mode", "Material Path (res://...)", ""]:
|
||||
var lbl := Label.new()
|
||||
lbl.text = col_text
|
||||
lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
slot_header_row.add_child(lbl)
|
||||
|
||||
_slots_vbox = VBoxContainer.new()
|
||||
_slots_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
form_vbox.add_child(_slots_vbox)
|
||||
|
||||
# ── Status bar ────────────────────────────────────────────────────────────
|
||||
_status_label = Label.new()
|
||||
_status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
root.add_child(_status_label)
|
||||
|
||||
|
||||
func _section_label(text: String) -> Label:
|
||||
var lbl := Label.new()
|
||||
lbl.text = text
|
||||
return lbl
|
||||
|
||||
|
||||
func _field(parent: VBoxContainer, label_text: String, hint: String = "") -> LineEdit:
|
||||
var row := HBoxContainer.new()
|
||||
parent.add_child(row)
|
||||
var lbl := Label.new()
|
||||
lbl.text = label_text + ":"
|
||||
lbl.custom_minimum_size.x = 150
|
||||
row.add_child(lbl)
|
||||
var edit := LineEdit.new()
|
||||
edit.placeholder_text = hint
|
||||
edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
row.add_child(edit)
|
||||
return edit
|
||||
|
||||
|
||||
func _spinbox(parent: VBoxContainer, label_text: String, mn: int, mx: int) -> SpinBox:
|
||||
var row := HBoxContainer.new()
|
||||
parent.add_child(row)
|
||||
var lbl := Label.new()
|
||||
lbl.text = label_text + ":"
|
||||
lbl.custom_minimum_size.x = 150
|
||||
row.add_child(lbl)
|
||||
var spin := SpinBox.new()
|
||||
spin.min_value = mn
|
||||
spin.max_value = mx
|
||||
spin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
row.add_child(spin)
|
||||
return spin
|
||||
|
||||
|
||||
func _option(parent: VBoxContainer, label_text: String, items: Array) -> OptionButton:
|
||||
var row := HBoxContainer.new()
|
||||
parent.add_child(row)
|
||||
var lbl := Label.new()
|
||||
lbl.text = label_text + ":"
|
||||
lbl.custom_minimum_size.x = 150
|
||||
row.add_child(lbl)
|
||||
var opt := OptionButton.new()
|
||||
for item in items:
|
||||
opt.add_item(item)
|
||||
opt.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
row.add_child(opt)
|
||||
return opt
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Data I/O
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
func _load_data() -> void:
|
||||
if not FileAccess.file_exists(DATA_PATH):
|
||||
_data = []
|
||||
_set_status("No data file found — starting fresh.", Color.YELLOW)
|
||||
return
|
||||
var f := FileAccess.open(DATA_PATH, FileAccess.READ)
|
||||
var parsed = JSON.parse_string(f.get_as_text())
|
||||
f.close()
|
||||
_data = parsed.get("skins", []) if parsed is Dictionary else []
|
||||
_set_status("Loaded %d skin(s) from %s" % [_data.size(), DATA_PATH], Color.WHITE)
|
||||
|
||||
|
||||
func _save_json() -> void:
|
||||
var f := FileAccess.open(DATA_PATH, FileAccess.WRITE)
|
||||
f.store_string(JSON.stringify({"skins": _data}, "\t"))
|
||||
f.close()
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# List
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
func _refresh_list() -> void:
|
||||
for c in _skin_list_vbox.get_children():
|
||||
c.queue_free()
|
||||
for i in _data.size():
|
||||
var entry: Dictionary = _data[i]
|
||||
var btn := Button.new()
|
||||
var cat: String = entry.get("category", "?")
|
||||
var iid: String = entry.get("item_id", "?")
|
||||
btn.text = "[%s]\n%s" % [cat, iid]
|
||||
btn.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
btn.toggle_mode = true
|
||||
btn.button_pressed = (i == _selected_idx)
|
||||
btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
btn.pressed.connect(_on_list_item_pressed.bind(i))
|
||||
_skin_list_vbox.add_child(btn)
|
||||
|
||||
|
||||
func _on_list_item_pressed(idx: int) -> void:
|
||||
if _selected_idx >= 0:
|
||||
_commit_form()
|
||||
_selected_idx = idx
|
||||
_refresh_list()
|
||||
_populate_form()
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Form
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
func _populate_form() -> void:
|
||||
if _selected_idx < 0 or _selected_idx >= _data.size():
|
||||
_form_panel.visible = false
|
||||
_no_sel_label.visible = true
|
||||
_delete_btn.disabled = true
|
||||
return
|
||||
|
||||
_form_panel.visible = true
|
||||
_no_sel_label.visible = false
|
||||
_delete_btn.disabled = false
|
||||
|
||||
var e: Dictionary = _data[_selected_idx]
|
||||
_form_item_id.text = e.get("item_id", "")
|
||||
_form_name.text = e.get("name", "")
|
||||
_form_character.text = e.get("character", "")
|
||||
_form_gold.value = e.get("gold", 0)
|
||||
_form_star.value = e.get("star", 0)
|
||||
|
||||
var cat_idx := CATEGORIES.find(e.get("category", "head"))
|
||||
_form_category.selected = max(0, cat_idx)
|
||||
|
||||
var rar_idx := RARITIES.find(e.get("rarity", "Common"))
|
||||
_form_rarity.selected = max(0, rar_idx)
|
||||
|
||||
_rebuild_slot_rows(e.get("slots", []))
|
||||
|
||||
|
||||
func _rebuild_slot_rows(slots: Array) -> void:
|
||||
for c in _slots_vbox.get_children():
|
||||
c.queue_free()
|
||||
for i in slots.size():
|
||||
_add_slot_row(i, slots[i])
|
||||
|
||||
|
||||
func _add_slot_row(idx: int, slot: Dictionary) -> void:
|
||||
var row := HBoxContainer.new()
|
||||
_slots_vbox.add_child(row)
|
||||
|
||||
var mesh_edit := LineEdit.new()
|
||||
mesh_edit.placeholder_text = "mesh node name"
|
||||
mesh_edit.text = slot.get("mesh", "")
|
||||
mesh_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
mesh_edit.tooltip_text = "MeshInstance3D node name, e.g. oldpop-hat1"
|
||||
row.add_child(mesh_edit)
|
||||
|
||||
var mode_opt := OptionButton.new()
|
||||
for m in MODES:
|
||||
mode_opt.add_item(m)
|
||||
mode_opt.selected = max(0, MODES.find(slot.get("mode", "override")))
|
||||
row.add_child(mode_opt)
|
||||
|
||||
var mat_edit := LineEdit.new()
|
||||
mat_edit.placeholder_text = "res://... (empty = skip)"
|
||||
mat_edit.text = slot.get("material", "")
|
||||
mat_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
mat_edit.tooltip_text = "Full res:// path to .tres material file"
|
||||
row.add_child(mat_edit)
|
||||
|
||||
var del_btn := Button.new()
|
||||
del_btn.text = "✕"
|
||||
del_btn.custom_minimum_size.x = 30
|
||||
del_btn.pressed.connect(func():
|
||||
_commit_form()
|
||||
var slots: Array = _data[_selected_idx].get("slots", [])
|
||||
slots.remove_at(idx)
|
||||
_data[_selected_idx]["slots"] = slots
|
||||
_rebuild_slot_rows(slots)
|
||||
)
|
||||
row.add_child(del_btn)
|
||||
|
||||
|
||||
func _commit_form() -> void:
|
||||
if _selected_idx < 0 or _selected_idx >= _data.size():
|
||||
return
|
||||
var e: Dictionary = _data[_selected_idx]
|
||||
e["item_id"] = _form_item_id.text.strip_edges()
|
||||
e["name"] = _form_name.text.strip_edges()
|
||||
e["character"] = _form_character.text.strip_edges()
|
||||
e["gold"] = int(_form_gold.value)
|
||||
e["star"] = int(_form_star.value)
|
||||
e["category"] = CATEGORIES[_form_category.selected]
|
||||
e["rarity"] = RARITIES[_form_rarity.selected]
|
||||
# Read slots
|
||||
var slots: Array = []
|
||||
for row in _slots_vbox.get_children():
|
||||
if not row is HBoxContainer:
|
||||
continue
|
||||
var ch := row.get_children()
|
||||
if ch.size() < 3:
|
||||
continue
|
||||
slots.append({
|
||||
"mesh": (ch[0] as LineEdit).text.strip_edges(),
|
||||
"mode": MODES[(ch[1] as OptionButton).selected],
|
||||
"material": (ch[2] as LineEdit).text.strip_edges(),
|
||||
})
|
||||
e["slots"] = slots
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Button handlers
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
func _on_add_pressed() -> void:
|
||||
if _selected_idx >= 0:
|
||||
_commit_form()
|
||||
_data.append({
|
||||
"item_id": "new_skin_%d" % _data.size(),
|
||||
"name": "New Skin",
|
||||
"category": "head",
|
||||
"character": "",
|
||||
"gold": 0,
|
||||
"star": 0,
|
||||
"rarity": "Common",
|
||||
"slots": [],
|
||||
})
|
||||
_selected_idx = _data.size() - 1
|
||||
_refresh_list()
|
||||
_populate_form()
|
||||
_set_status("New skin created. Fill in the form and Save & Generate.", Color.YELLOW)
|
||||
|
||||
|
||||
func _on_delete_pressed() -> void:
|
||||
if _selected_idx < 0:
|
||||
return
|
||||
var removed: String = _data[_selected_idx].get("item_id", "?")
|
||||
_data.remove_at(_selected_idx)
|
||||
_selected_idx = min(_selected_idx, _data.size() - 1)
|
||||
_refresh_list()
|
||||
_populate_form()
|
||||
_set_status("Deleted: " + removed, Color.YELLOW)
|
||||
|
||||
|
||||
func _on_add_slot_pressed() -> void:
|
||||
if _selected_idx < 0:
|
||||
return
|
||||
_commit_form()
|
||||
var slots: Array = _data[_selected_idx].get("slots", [])
|
||||
slots.append({"mesh": "", "mode": "override", "material": ""})
|
||||
_data[_selected_idx]["slots"] = slots
|
||||
_rebuild_slot_rows(slots)
|
||||
|
||||
|
||||
func _on_save_pressed() -> void:
|
||||
if _selected_idx >= 0:
|
||||
_commit_form()
|
||||
_save_json()
|
||||
var err_gd := _generate_skin_manager()
|
||||
var err_js := _generate_admin_js()
|
||||
if err_gd == OK and err_js == OK:
|
||||
_set_status("✓ skin_manager.gd and tekton_admin.js updated successfully!", Color(0.4, 1.0, 0.4))
|
||||
else:
|
||||
_set_status("⚠ Some files could not be updated — check the Output log.", Color.YELLOW)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Code Generation — skin_manager.gd
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
func _generate_skin_manager() -> int:
|
||||
if not FileAccess.file_exists(SKIN_MANAGER_PATH):
|
||||
push_error("[SkinCatalogEditor] File not found: " + SKIN_MANAGER_PATH)
|
||||
return ERR_FILE_NOT_FOUND
|
||||
var f := FileAccess.open(SKIN_MANAGER_PATH, FileAccess.READ)
|
||||
var src: String = f.get_as_text()
|
||||
f.close()
|
||||
|
||||
var b := src.find(BEGIN_SKIN)
|
||||
var e := src.find(END_SKIN)
|
||||
if b == -1 or e == -1:
|
||||
push_error("[SkinCatalogEditor] Sentinel markers not found in skin_manager.gd")
|
||||
return ERR_INVALID_DATA
|
||||
|
||||
var lines: PackedStringArray = []
|
||||
lines.append(BEGIN_SKIN)
|
||||
lines.append("const SKIN_CATALOG: Dictionary = {")
|
||||
lines.append("")
|
||||
|
||||
# Group by category
|
||||
var by_cat: Dictionary = {}
|
||||
for entry: Dictionary in _data:
|
||||
var cat: String = entry.get("category", "head")
|
||||
if not by_cat.has(cat):
|
||||
by_cat[cat] = []
|
||||
by_cat[cat].append(entry)
|
||||
|
||||
for cat in CATEGORIES:
|
||||
if not by_cat.has(cat):
|
||||
continue
|
||||
lines.append("\t# \u2500\u2500 [%s] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" % cat.to_upper())
|
||||
for entry: Dictionary in by_cat[cat]:
|
||||
lines.append("\t\"%s\": {" % entry["item_id"])
|
||||
lines.append("\t\t\"category\": \"%s\"," % entry["category"])
|
||||
lines.append("\t\t\"character\": \"%s\"," % entry.get("character", ""))
|
||||
lines.append("\t\t\"slots\": [")
|
||||
for slot: Dictionary in entry.get("slots", []):
|
||||
lines.append("\t\t\t{ \"mesh\": \"%s\", \"mode\": \"%s\", \"material\": \"%s\" }," % [
|
||||
slot.get("mesh", ""), slot.get("mode", "override"), slot.get("material", "")
|
||||
])
|
||||
lines.append("\t\t]")
|
||||
lines.append("\t},")
|
||||
lines.append("")
|
||||
|
||||
lines.append("}")
|
||||
lines.append(END_SKIN)
|
||||
|
||||
var block: String = "\n".join(lines)
|
||||
var new_src: String = src.substr(0, b) + block + src.substr(e + END_SKIN.length())
|
||||
var fw := FileAccess.open(SKIN_MANAGER_PATH, FileAccess.WRITE)
|
||||
fw.store_string(new_src)
|
||||
fw.close()
|
||||
return OK
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Code Generation — tekton_admin.js
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
func _generate_admin_js() -> int:
|
||||
if not FileAccess.file_exists(ADMIN_JS_PATH):
|
||||
push_error("[SkinCatalogEditor] File not found: " + ADMIN_JS_PATH)
|
||||
return ERR_FILE_NOT_FOUND
|
||||
var f := FileAccess.open(ADMIN_JS_PATH, FileAccess.READ)
|
||||
var src: String = f.get_as_text()
|
||||
f.close()
|
||||
|
||||
var b := src.find(BEGIN_SHOP)
|
||||
var e := src.find(END_SHOP)
|
||||
if b == -1 or e == -1:
|
||||
push_error("[SkinCatalogEditor] Sentinel markers not found in tekton_admin.js")
|
||||
return ERR_INVALID_DATA
|
||||
|
||||
var lines: PackedStringArray = []
|
||||
lines.append(BEGIN_SHOP)
|
||||
lines.append("var SHOP_CATALOG_DEFS = [")
|
||||
|
||||
var prev_cat := ""
|
||||
for entry: Dictionary in _data:
|
||||
var cat: String = entry.get("category", "head")
|
||||
if cat != prev_cat:
|
||||
lines.append(" // \u2500\u2500 %s \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" % cat.to_upper())
|
||||
prev_cat = cat
|
||||
var char_val: String = entry.get("character", "")
|
||||
var char_part: String = (", character: \"%s\"" % char_val) if not char_val.is_empty() else ""
|
||||
lines.append(" { id: \"%s\", name: \"%s\", category: \"%s\", gold: %d, star: %d, rarity: \"%s\"%s }," % [
|
||||
entry.get("item_id", ""),
|
||||
entry.get("name", ""),
|
||||
cat,
|
||||
entry.get("gold", 0),
|
||||
entry.get("star", 0),
|
||||
entry.get("rarity", "Common"),
|
||||
char_part,
|
||||
])
|
||||
|
||||
lines.append("];")
|
||||
lines.append(END_SHOP)
|
||||
|
||||
var block: String = "\n".join(lines)
|
||||
var new_src: String = src.substr(0, b) + block + src.substr(e + END_SHOP.length())
|
||||
var fw := FileAccess.open(ADMIN_JS_PATH, FileAccess.WRITE)
|
||||
fw.store_string(new_src)
|
||||
fw.close()
|
||||
return OK
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Helpers
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
func _set_status(msg: String, color: Color = Color.WHITE) -> void:
|
||||
if not _status_label:
|
||||
return
|
||||
_status_label.add_theme_color_override("font_color", color)
|
||||
_status_label.text = msg
|
||||
@@ -0,0 +1 @@
|
||||
uid://d3wlsx2lbdlge
|
||||
Reference in New Issue
Block a user