初次提交
This commit is contained in:
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://13nt7jtotgom
|
||||
@@ -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 = "电路边(CIRCUIT,JSON)"; 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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dh1wnfcmj186e
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://cwkd7bf4jwxi2
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dskauy03qt13h
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
[plugin]
|
||||
name="Game Designer (游戏设计器)"
|
||||
description="一站式可视化配置面板:策划/开发无需改代码即可调整法术、敌人、波次、Core、平衡数值。所有数据存 res://data/*.json,游戏运行时自动加载。底部面板「🎮 游戏设计器」。"
|
||||
author="Base Games"
|
||||
version="1.0.0"
|
||||
script="game_designer.gd"
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://esmos6ikng8c
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cujsbx8n78jky
|
||||
Reference in New Issue
Block a user