初次提交

This commit is contained in:
2026-07-20 10:56:52 +08:00
commit 7bcc0026e0
462 changed files with 50191 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
## 平衡编辑器标签 — 难度乘子(3 档)+ Boss 基础血量
@tool
extends VBoxContainer
const UI = preload("res://addons/game_designer/designer_ui.gd")
const PATH = "res://data/balance.json"
const MULT_KEYS = ["enemy_hp", "player_dmg", "wave_count", "boss_hp"]
const MULT_LABELS = ["敌人血量×", "玩家受伤×", "波次怪数×", "Boss血量×"]
const DIFF_NAMES = ["初学者", "标准", "挑战"]
var _data: Dictionary = {}
var _mult_spins: Dictionary = {} # key → [SpinBox ×3]
var _miniboss: SpinBox
var _boss: SpinBox
var _status: Label
func _ready() -> void:
add_child(UI.header("⚖ 平衡编辑器 — 难度乘子 & Boss 血量"))
_load()
# 难度乘子表格:4 行 × 3 难度
var grid := GridContainer.new(); grid.columns = 4; add_child(grid)
grid.add_child(Label.new()) # 角
for dn in DIFF_NAMES:
var l := Label.new(); l.text = dn; l.modulate = Color(0.7, 0.8, 1.0)
grid.add_child(l)
var mults: Dictionary = _data.get("difficulty_mults", {})
for i in MULT_KEYS.size():
var key: String = MULT_KEYS[i]
grid.add_child(UI.cell_label(MULT_LABELS[i]))
var arr: Array = mults.get(key, [1.0, 1.0, 1.0])
var spins: Array = []
for d in 3:
var sp := UI.spin(0.0, 10.0, 0.05, float(arr[d]) if d < arr.size() else 1.0)
grid.add_child(sp); spins.append(sp)
_mult_spins[key] = spins
add_child(HSeparator.new())
# Boss 基础血量
var bhp: Dictionary = _data.get("boss_hp", {})
var h1 := HBoxContainer.new(); add_child(h1)
h1.add_child(UI.cell_label("MiniBoss(W8) 血量"))
_miniboss = UI.spin(1, 999999, 10, float(bhp.get("4", 450)))
h1.add_child(_miniboss)
var h2 := HBoxContainer.new(); add_child(h2)
h2.add_child(UI.cell_label("Final Boss(W20) 血量"))
_boss = UI.spin(1, 999999, 10, float(bhp.get("5", 1800)))
h2.add_child(_boss)
add_child(HSeparator.new())
add_child(UI.btn("💾 保存 balance.json", _save))
_status = UI.status_label(); add_child(_status)
func _load() -> void:
var d = UI.load_json(PATH)
_data = d if d is Dictionary else {}
func _save() -> void:
var mults: Dictionary = {}
for key in MULT_KEYS:
var spins: Array = _mult_spins[key]
mults[key] = [spins[0].value, spins[1].value, spins[2].value]
var out := {
"difficulty_mults": mults,
"boss_hp": {"4": _miniboss.value, "5": _boss.value},
}
if UI.save_json(PATH, out):
UI.set_status(_status, "💾 已保存(重启 F5 生效)")
else:
UI.set_status(_status, "✗ 保存失败", true)
+1
View File
@@ -0,0 +1 @@
uid://13nt7jtotgom
+125
View File
@@ -0,0 +1,125 @@
## Core(法杖)编辑器标签 — 槽位/拓扑/CPU/施法间隔/特性/矩阵·电路结构
@tool
extends VBoxContainer
const UI = preload("res://addons/game_designer/designer_ui.gd")
const PATH = "res://data/cores.json"
const TOPO = ["LINEAR 线性", "MATRIX 矩阵", "CIRCUIT 电路"]
const FEAT = ["", "PERSISTENT_MEMORY 记忆", "DUAL_STREAM 双流", "ALWAYS_CAST_LAST", "SHUFFLE_DECK", "INFINITE_SPELLS"]
var _data: Dictionary = {}
var _cur: String = ""
var _list: ItemList
var _id: LineEdit
var _name: LineEdit
var _slots: SpinBox
var _topo: OptionButton
var _cpu: SpinBox
var _interval: SpinBox
var _feat: OptionButton
var _rows: SpinBox
var _cols: SpinBox
var _edges: TextEdit
var _status: Label
func _ready() -> void:
add_child(UI.header("🪄 Core 编辑器 — 法杖拓扑与特性"))
_load()
_list = ItemList.new()
_list.custom_minimum_size = Vector2(0, 90)
_list.item_selected.connect(_on_select)
add_child(_list)
var brow := HBoxContainer.new(); add_child(brow)
brow.add_child(UI.btn("➕新增", _on_new))
brow.add_child(UI.btn("🗑删除", _on_delete))
add_child(HSeparator.new())
_id = _row("ID", UI.line(""))
_name = _row("显示名", UI.line("", "tr 键或文本"))
_slots = _row("槽位数", UI.spin(1, 20, 1, 5))
_topo = _row("拓扑", UI.opt(TOPO, 0))
_cpu = _row("CPU 上限", UI.spin(1, 40, 1, 5))
_interval = _row("施法间隔(s)", UI.spin(0.05, 5, 0.05, 0.5))
_feat = _row("特性", UI.opt(FEAT, 0))
_rows = _row("矩阵行(MATRIX)", UI.spin(1, 8, 1, 1))
_cols = _row("矩阵列(MATRIX)", UI.spin(1, 16, 1, 5))
add_child(Label.new())
var el := Label.new(); el.text = "电路边(CIRCUITJSON)"; add_child(el)
_edges = TextEdit.new(); _edges.custom_minimum_size = Vector2(0, 50)
_edges.placeholder_text = '[{"from":0,"to":1},{"from":0,"to":2}]'
add_child(_edges)
add_child(HSeparator.new())
var srow := HBoxContainer.new(); add_child(srow)
srow.add_child(UI.btn("✓应用", _on_apply))
srow.add_child(UI.btn("💾保存 cores.json", _save))
_status = UI.status_label(); add_child(_status)
_refresh()
func _row(label: String, ctrl: Control) -> Control:
var h := HBoxContainer.new()
h.add_child(UI.cell_label(label))
h.add_child(ctrl)
add_child(h)
return ctrl
func _load() -> void:
var d = UI.load_json(PATH)
_data = d if d is Dictionary else {}
func _refresh() -> void:
_list.clear()
var ids: Array = _data.keys(); ids.sort()
for cid in ids:
_list.add_item("%s%s" % [cid, _data[cid].get("display_name", "")])
_list.set_item_metadata(_list.item_count - 1, cid)
func _on_select(idx: int) -> void:
_cur = String(_list.get_item_metadata(idx))
var d: Dictionary = _data.get(_cur, {})
_id.text = _cur
_name.text = String(d.get("display_name", ""))
_slots.value = float(d.get("slot_count", 5))
_topo.selected = int(d.get("topology", 0))
_cpu.value = float(d.get("cpu_limit", 5))
_interval.value = float(d.get("cast_interval", 0.5))
_feat.selected = int(d.get("feature_tags", 0))
_rows.value = float(d.get("grid_rows", 1))
_cols.value = float(d.get("grid_cols", 5))
_edges.text = JSON.stringify(d.get("edges", []))
func _on_apply() -> void:
var cid: String = _id.text.strip_edges()
if cid == "":
UI.set_status(_status, "✗ ID 不能为空", true); return
var edges = JSON.parse_string(_edges.text) if _edges.text.strip_edges() != "" else []
if not (edges is Array):
edges = []
if _cur != "" and _cur != cid and _data.has(_cur):
_data.erase(_cur)
_data[cid] = {
"display_name": _name.text, "slot_count": int(_slots.value),
"topology": _topo.selected, "cpu_limit": int(_cpu.value),
"cast_interval": _interval.value, "feature_tags": _feat.selected,
"grid_rows": int(_rows.value), "grid_cols": int(_cols.value), "edges": edges,
}
_cur = cid
_refresh()
UI.set_status(_status, "✓ 已应用『%s』(记得保存)" % cid)
func _on_new() -> void:
var cid := "new_core"
var i := 1
while _data.has(cid): cid = "new_core_%d" % i; i += 1
_data[cid] = {"display_name": cid, "slot_count": 5, "topology": 0,
"cpu_limit": 5, "cast_interval": 0.5, "feature_tags": 0, "edges": []}
_refresh()
func _on_delete() -> void:
if _cur != "" and _data.has(_cur):
_data.erase(_cur); _cur = ""; _refresh()
UI.set_status(_status, "已删除(记得保存)")
func _save() -> void:
if UI.save_json(PATH, _data):
UI.set_status(_status, "💾 已保存 %d 个 Core(重启 F5 生效)" % _data.size())
else:
UI.set_status(_status, "✗ 保存失败", true)
+1
View File
@@ -0,0 +1 @@
uid://dh1wnfcmj186e
+26
View File
@@ -0,0 +1,26 @@
## 游戏设计器主面板 — TabContainer 整合 法术/敌人/波次/Core/平衡 五个编辑器
@tool
extends Control
func _init() -> void:
custom_minimum_size = Vector2(0, 320)
var tabs := TabContainer.new()
tabs.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(tabs)
# 法术(复用现有法术编辑器停靠脚本)
_add_tab(tabs, "✨ 法术", preload("res://addons/spell_editor/spell_dock.gd").new())
_add_tab(tabs, "👾 敌人", preload("res://addons/game_designer/enemy_tab.gd").new())
_add_tab(tabs, "🌊 波次", preload("res://addons/game_designer/wave_tab.gd").new())
_add_tab(tabs, "🪄 Core", preload("res://addons/game_designer/core_tab.gd").new())
_add_tab(tabs, "🔮 共鸣", preload("res://addons/game_designer/resonance_tab.gd").new())
_add_tab(tabs, "🔥 状态", preload("res://addons/game_designer/status_tab.gd").new())
_add_tab(tabs, "⚖ 平衡", preload("res://addons/game_designer/balance_tab.gd").new())
func _add_tab(tabs: TabContainer, title: String, content: Control) -> void:
var scroll := ScrollContainer.new()
scroll.name = title
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
scroll.add_child(content)
tabs.add_child(scroll)
@@ -0,0 +1 @@
uid://bjhq6mgh2g3nq
+68
View File
@@ -0,0 +1,68 @@
## 游戏设计器通用 UI 辅助(静态方法,各标签页复用)
@tool
extends RefCounted
static func spin(min_v: float, max_v: float, step: float, val: float) -> SpinBox:
var s := SpinBox.new()
s.min_value = min_v; s.max_value = max_v; s.step = step
s.value = val
s.custom_minimum_size = Vector2(80, 0)
return s
static func line(text: String, placeholder: String = "") -> LineEdit:
var le := LineEdit.new()
le.text = text
le.placeholder_text = placeholder
le.custom_minimum_size = Vector2(120, 0)
return le
static func opt(items: Array, selected: int) -> OptionButton:
var o := OptionButton.new()
for it in items:
o.add_item(str(it))
o.selected = clampi(selected, 0, items.size() - 1)
return o
static func header(text: String) -> Label:
var l := Label.new()
l.text = text
l.add_theme_font_size_override("font_size", 15)
l.modulate = Color(1.0, 0.85, 0.4)
return l
static func cell_label(text: String) -> Label:
var l := Label.new()
l.text = text
l.custom_minimum_size = Vector2(70, 0)
return l
static func btn(text: String, cb: Callable) -> Button:
var b := Button.new()
b.text = text
b.pressed.connect(cb)
return b
static func status_label() -> Label:
var l := Label.new()
l.add_theme_font_size_override("font_size", 11)
l.modulate = Color(0.6, 0.9, 0.6)
return l
static func set_status(lbl: Label, msg: String, is_err: bool = false) -> void:
if lbl:
lbl.text = msg
lbl.modulate = Color(1.0, 0.5, 0.5) if is_err else Color(0.6, 0.9, 0.6)
static func load_json(path: String):
if not FileAccess.file_exists(path):
return null
return JSON.parse_string(FileAccess.get_file_as_string(path))
static func save_json(path: String, data) -> bool:
DirAccess.make_dir_recursive_absolute("res://data")
var f := FileAccess.open(path, FileAccess.WRITE)
if f:
f.store_string(JSON.stringify(data, " "))
f.close()
return true
return false
+1
View File
@@ -0,0 +1 @@
uid://cwkd7bf4jwxi2
+60
View File
@@ -0,0 +1,60 @@
## 敌人原型编辑器标签(表格:6 原型 × 速度/护甲/尺寸/颜色)
@tool
extends VBoxContainer
const UI = preload("res://addons/game_designer/designer_ui.gd")
const PATH = "res://data/enemies.json"
const NAMES = ["杂兵 Basic", "快速 Fast", "护甲 Armored", "精英 Elite", "首领 MiniBoss", "终焉 Boss"]
var _data: Dictionary = {}
var _rows: Array = [] # [{speed:SpinBox, armor:SpinBox, size:SpinBox, color:ColorPickerButton}]
var _status: Label
func _ready() -> void:
add_child(UI.header("👾 敌人原型 — 调速度 / 护甲 / 尺寸 / 颜色"))
_load()
var grid := GridContainer.new()
grid.columns = 5
add_child(grid)
# 表头
for h in ["原型", "速度", "护甲", "尺寸(px)", "颜色"]:
var hl := Label.new(); hl.text = h; hl.modulate = Color(0.7, 0.8, 1.0)
grid.add_child(hl)
# 行
for i in 6:
var key := str(i)
var d: Dictionary = _data.get(key, {})
grid.add_child(UI.cell_label(NAMES[i]))
var sp := UI.spin(0, 1000, 1, float(d.get("speed", 80)))
var ar := UI.spin(0, 200, 0.5, float(d.get("armor", 0)))
var sz := UI.spin(2, 200, 1, float(d.get("size", 14)))
var cp := ColorPickerButton.new()
var c: Array = d.get("color", [1, 1, 1])
cp.color = Color(float(c[0]), float(c[1]), float(c[2]))
cp.custom_minimum_size = Vector2(60, 0)
grid.add_child(sp); grid.add_child(ar); grid.add_child(sz); grid.add_child(cp)
_rows.append({"speed": sp, "armor": ar, "size": sz, "color": cp})
add_child(HSeparator.new())
add_child(UI.btn("💾 保存 enemies.json", _save))
_status = UI.status_label()
add_child(_status)
func _load() -> void:
var d = UI.load_json(PATH)
_data = d if d is Dictionary else {}
func _save() -> void:
var out: Dictionary = {}
for i in 6:
var r: Dictionary = _rows[i]
out[str(i)] = {
"name": NAMES[i],
"speed": r["speed"].value,
"armor": r["armor"].value,
"size": r["size"].value,
"color": [r["color"].color.r, r["color"].color.g, r["color"].color.b],
}
if UI.save_json(PATH, out):
UI.set_status(_status, "💾 已保存(重启游戏 F5 生效)")
else:
UI.set_status(_status, "✗ 保存失败", true)
+1
View File
@@ -0,0 +1 @@
uid://dskauy03qt13h
+17
View File
@@ -0,0 +1,17 @@
## 游戏设计器插件入口(EditorPlugin
## 在编辑器底部面板添加「🎮 游戏设计器」,整合 法术/敌人/波次/Core/平衡 编辑器。
@tool
extends EditorPlugin
var _panel: Control = null
func _enter_tree() -> void:
_panel = preload("res://addons/game_designer/designer_panel.gd").new()
_panel.name = "🎮 游戏设计器"
add_control_to_bottom_panel(_panel, "🎮 游戏设计器")
func _exit_tree() -> void:
if _panel:
remove_control_from_bottom_panel(_panel)
_panel.free()
_panel = null
@@ -0,0 +1 @@
uid://drkmlrut1l4pf
+6
View File
@@ -0,0 +1,6 @@
[plugin]
name="Game Designer (游戏设计器)"
description="一站式可视化配置面板:策划/开发无需改代码即可调整法术、敌人、波次、Core、平衡数值。所有数据存 res://data/*.json,游戏运行时自动加载。底部面板「🎮 游戏设计器」。"
author="Base Games"
version="1.0.0"
script="game_designer.gd"
+100
View File
@@ -0,0 +1,100 @@
## 共鸣配方编辑器标签 — 元素组合 → 产物法术
@tool
extends VBoxContainer
const UI = preload("res://addons/game_designer/designer_ui.gd")
const PATH = "res://data/resonance.json"
const MATCH = ["adjacent", "anywhere_in_deck"]
var _data: Array = []
var _cur: int = -1
var _id: LineEdit
var _p0: LineEdit
var _p1: LineEdit
var _match: OptionButton
var _result: LineEdit
var _consume: CheckBox
var _list: ItemList
var _status: Label
func _ready() -> void:
add_child(UI.header("🔮 共鸣配方 — 元素组合相邻 → 自动合成产物法术"))
_load()
_list = ItemList.new()
_list.custom_minimum_size = Vector2(0, 80)
_list.item_selected.connect(_on_select)
add_child(_list)
var brow := HBoxContainer.new(); add_child(brow)
brow.add_child(UI.btn("➕新增", _on_new))
brow.add_child(UI.btn("🗑删除", _on_delete))
add_child(HSeparator.new())
_id = _row("配方 ID", UI.line(""))
_p0 = _row("元素 A", UI.line("", "tag:water"))
_p1 = _row("元素 B", UI.line("", "tag:lightning"))
_match = _row("匹配方式", UI.opt(MATCH, 0))
_result = _row("产物法术 ID", UI.line("", "action_plasma_storm"))
_consume = CheckBox.new(); _consume.text = "消耗输入法术(合成后移除原法术)"
add_child(_consume)
add_child(HSeparator.new())
var srow := HBoxContainer.new(); add_child(srow)
srow.add_child(UI.btn("✓应用", _on_apply))
srow.add_child(UI.btn("💾保存 resonance.json", _save))
_status = UI.status_label(); add_child(_status)
_refresh()
func _row(label: String, ctrl: Control) -> Control:
var h := HBoxContainer.new(); h.add_child(UI.cell_label(label)); h.add_child(ctrl); add_child(h)
return ctrl
func _load() -> void:
var d = UI.load_json(PATH)
_data = d if d is Array else []
func _refresh() -> void:
_list.clear()
for r in _data:
var p: Array = r.get("pattern", ["?", "?"])
_list.add_item("%s (%s + %s)" % [r.get("id", "?"), p[0] if p.size() > 0 else "", p[1] if p.size() > 1 else ""])
func _on_select(idx: int) -> void:
_cur = idx
var d: Dictionary = _data[idx]
var p: Array = d.get("pattern", ["", ""])
_id.text = String(d.get("id", ""))
_p0.text = String(p[0]) if p.size() > 0 else ""
_p1.text = String(p[1]) if p.size() > 1 else ""
_match.selected = max(0, MATCH.find(String(d.get("match", "adjacent"))))
_result.text = String(d.get("result_spell_id", ""))
_consume.button_pressed = bool(d.get("consume_inputs", false))
func _on_apply() -> void:
var r := {
"id": _id.text.strip_edges(),
"pattern": [_p0.text.strip_edges(), _p1.text.strip_edges()],
"match": MATCH[_match.selected],
"result_spell_id": _result.text.strip_edges(),
"consume_inputs": _consume.button_pressed,
}
if _cur >= 0 and _cur < _data.size():
_data[_cur] = r
else:
_data.append(r); _cur = _data.size() - 1
_refresh()
UI.set_status(_status, "✓ 已应用(记得保存)")
func _on_new() -> void:
_data.append({"id": "new_recipe", "pattern": ["tag:a", "tag:b"],
"match": "adjacent", "result_spell_id": "", "consume_inputs": true})
_cur = _data.size() - 1
_refresh()
func _on_delete() -> void:
if _cur >= 0 and _cur < _data.size():
_data.remove_at(_cur); _cur = -1; _refresh()
UI.set_status(_status, "已删除(记得保存)")
func _save() -> void:
if UI.save_json(PATH, _data):
UI.set_status(_status, "💾 已保存 %d 个配方(重启 F5 生效)" % _data.size())
else:
UI.set_status(_status, "✗ 保存失败", true)
@@ -0,0 +1 @@
uid://bqukws0ume8dy
+110
View File
@@ -0,0 +1,110 @@
## 状态效果编辑器标签 — DoT/层数/持续时间/VFX
@tool
extends VBoxContainer
const UI = preload("res://addons/game_designer/designer_ui.gd")
const PATH = "res://data/status_effects.json"
const STACK = ["REFRESH 刷新", "INTENSITY 叠层", "INDEPENDENT 独立"]
var _data: Dictionary = {}
var _cur: String = ""
var _id: SpinBox
var _name: LineEdit
var _dur: SpinBox
var _tick: SpinBox
var _stack: OptionButton
var _max: SpinBox
var _dot: SpinBox
var _vfx: LineEdit
var _combo: CheckBox
var _list: ItemList
var _status: Label
func _ready() -> void:
add_child(UI.header("🔥 状态效果 — 燃烧/中毒/连击等 DoT 与叠层"))
_load()
_list = ItemList.new()
_list.custom_minimum_size = Vector2(0, 80)
_list.item_selected.connect(_on_select)
add_child(_list)
var brow := HBoxContainer.new(); add_child(brow)
brow.add_child(UI.btn("➕新增", _on_new))
brow.add_child(UI.btn("🗑删除", _on_delete))
add_child(HSeparator.new())
_id = _row("状态 ID(整数,1=燃烧 3=中毒…)", UI.spin(1, 999, 1, 9))
_name = _row("名称", UI.line(""))
_dur = _row("持续时间(s)", UI.spin(0.1, 999, 0.5, 3.0))
_tick = _row("跳字间隔(s)", UI.spin(0.1, 999, 0.5, 1.0))
_stack = _row("叠层模式", UI.opt(STACK, 0))
_max = _row("最大层数(0=无限)", UI.spin(0, 999, 1, 1))
_dot = _row("每跳伤害", UI.spin(0, 9999, 0.5, 0.0))
_vfx = _row("VFX ID", UI.line("", "status_burn"))
_combo = CheckBox.new(); _combo.text = "连击标记追踪器"
add_child(_combo)
add_child(HSeparator.new())
var srow := HBoxContainer.new(); add_child(srow)
srow.add_child(UI.btn("✓应用", _on_apply))
srow.add_child(UI.btn("💾保存 status_effects.json", _save))
_status = UI.status_label(); add_child(_status)
_refresh()
func _row(label: String, ctrl: Control) -> Control:
var h := HBoxContainer.new(); h.add_child(UI.cell_label(label)); h.add_child(ctrl); add_child(h)
return ctrl
func _load() -> void:
var d = UI.load_json(PATH)
_data = d if d is Dictionary else {}
func _refresh() -> void:
_list.clear()
var ids: Array = _data.keys(); ids.sort()
for k in ids:
_list.add_item("[%s] %s" % [k, _data[k].get("name", "")])
_list.set_item_metadata(_list.item_count - 1, k)
func _on_select(idx: int) -> void:
_cur = String(_list.get_item_metadata(idx))
var d: Dictionary = _data[_cur]
_id.value = float(int(_cur))
_name.text = String(d.get("name", ""))
_dur.value = float(d.get("duration", 3.0))
_tick.value = float(d.get("tick_interval", 1.0))
_stack.selected = int(d.get("stack_mode", 0))
_max.value = float(d.get("max_stacks", 1))
_dot.value = float(d.get("dot_damage", 0.0))
_vfx.text = String(d.get("vfx_id", ""))
_combo.button_pressed = bool(d.get("is_combo_tracker", false))
func _on_apply() -> void:
var sid := str(int(_id.value))
if _cur != "" and _cur != sid and _data.has(_cur):
_data.erase(_cur)
_data[sid] = {
"name": _name.text, "duration": _dur.value, "tick_interval": _tick.value,
"stack_mode": _stack.selected, "max_stacks": int(_max.value),
"dot_damage": _dot.value, "dot_damage_type": 0,
"vfx_id": _vfx.text, "is_combo_tracker": _combo.button_pressed,
}
_cur = sid
_refresh()
UI.set_status(_status, "✓ 已应用 ID %s(记得保存)" % sid)
func _on_new() -> void:
var sid := 9
while _data.has(str(sid)): sid += 1
_data[str(sid)] = {"name": "新状态", "duration": 3.0, "tick_interval": 1.0,
"stack_mode": 0, "max_stacks": 1, "dot_damage": 0.0, "dot_damage_type": 0,
"vfx_id": "", "is_combo_tracker": false}
_refresh()
func _on_delete() -> void:
if _cur != "" and _data.has(_cur):
_data.erase(_cur); _cur = ""; _refresh()
UI.set_status(_status, "已删除(记得保存)")
func _save() -> void:
if UI.save_json(PATH, _data):
UI.set_status(_status, "💾 已保存 %d 个状态(重启 F5 生效)" % _data.size())
else:
UI.set_status(_status, "✗ 保存失败", true)
+1
View File
@@ -0,0 +1 @@
uid://esmos6ikng8c
+86
View File
@@ -0,0 +1,86 @@
## 波次编辑器标签(关卡难度曲线:每波 怪数/血量/类型/精英/金币/XP/Boss
@tool
extends VBoxContainer
const UI = preload("res://addons/game_designer/designer_ui.gd")
const PATH = "res://data/waves.json"
const TYPE_NAMES = ["Basic", "Fast", "Armored", "Elite", "MiniBoss", "Boss"]
const BOSS_NAMES = ["", "MiniBoss(4)", "Boss(5)"] # 映射到 boss 值 -1/4/5
const BOSS_VALS = [-1, 4, 5]
var _waves: Array = []
var _rows: Array = []
var _status: Label
func _ready() -> void:
_build()
func _build() -> void:
add_child(UI.header("🌊 波次编辑器 — 关卡难度曲线(点行号添加/删除波次)"))
_load()
var scroll := ScrollContainer.new()
scroll.custom_minimum_size = Vector2(0, 260)
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
add_child(scroll)
var grid := GridContainer.new()
grid.columns = 8
scroll.add_child(grid)
for h in ["", "怪数", "血量", "类型", "精英", "金币", "XP", "Boss"]:
var hl := Label.new(); hl.text = h; hl.modulate = Color(0.7, 0.8, 1.0)
grid.add_child(hl)
for i in _waves.size():
_add_row(grid, i, _waves[i])
add_child(HSeparator.new())
var row := HBoxContainer.new(); add_child(row)
row.add_child(UI.btn(" 末尾加一波", _on_add))
row.add_child(UI.btn("💾 保存 waves.json", _save))
_status = UI.status_label()
add_child(_status)
func _add_row(grid: GridContainer, idx: int, w: Dictionary) -> void:
grid.add_child(UI.cell_label("W%d" % (idx + 1)))
var count := UI.spin(0, 2000, 1, float(w.get("count", 20)))
var hp := UI.spin(1, 99999, 1, float(w.get("hp", 15)))
var type_o := UI.opt(TYPE_NAMES, int(w.get("type", 0)))
var elite := UI.spin(0, 100, 1, float(w.get("elite", 0)))
var gold := UI.spin(0, 99999, 1, float(w.get("gold", 10)))
var xp := UI.spin(0, 99999, 1, float(w.get("xp", 20)))
var boss_idx := BOSS_VALS.find(int(w.get("boss", -1)))
var boss_o := UI.opt(BOSS_NAMES, max(0, boss_idx))
grid.add_child(count); grid.add_child(hp); grid.add_child(type_o)
grid.add_child(elite); grid.add_child(gold); grid.add_child(xp); grid.add_child(boss_o)
_rows.append({"count": count, "hp": hp, "type": type_o, "elite": elite,
"gold": gold, "xp": xp, "boss": boss_o})
func _load() -> void:
var d = UI.load_json(PATH)
_waves = d if d is Array else []
func _on_add() -> void:
_waves = _read_rows()
_waves.append({"count": 30, "hp": 50, "type": 0, "elite": 0, "gold": 30, "xp": 40, "boss": -1})
_rebuild()
func _rebuild() -> void:
# 简易重建:清空后重新 _init 逻辑(移除所有子节点重建)
for c in get_children():
c.queue_free()
_rows.clear()
call_deferred("_build")
func _read_rows() -> Array:
var out: Array = []
for r in _rows:
out.append({
"count": int(r["count"].value), "hp": r["hp"].value,
"type": r["type"].selected, "elite": int(r["elite"].value),
"gold": int(r["gold"].value), "xp": int(r["xp"].value),
"boss": BOSS_VALS[r["boss"].selected],
})
return out
func _save() -> void:
if UI.save_json(PATH, _read_rows()):
UI.set_status(_status, "💾 已保存 %d 波(重启游戏 F5 生效)" % _rows.size())
else:
UI.set_status(_status, "✗ 保存失败", true)
+1
View File
@@ -0,0 +1 @@
uid://cujsbx8n78jky