merge(designer): UI 优化(输入框撑满 + 元素复选框 + 双语标签 + 共鸣元素下拉)

This commit is contained in:
2026-07-22 11:59:40 +08:00
7 changed files with 56 additions and 30 deletions
+2 -2
View File
@@ -4,8 +4,8 @@ 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"]
const TOPO = ["线性 LINEAR", "矩阵 MATRIX", "电路 CIRCUIT"]
const FEAT = [" None", "记忆 PERSISTENT_MEMORY", "双流 DUAL_STREAM", "末位施法 ALWAYS_CAST_LAST", "洗牌 SHUFFLE_DECK", "无限法术 INFINITE_SPELLS"]
var _data: Dictionary = {}
var _cur: String = ""
+8 -1
View File
@@ -1,12 +1,17 @@
## 游戏设计器通用 UI 辅助(静态方法,各标签页复用)
## 游戏设计器通用 UI 辅助(静态方法 + 共享常量,各标签页复用)
@tool
extends RefCounted
## 元素词表(spell_tab 复选框 / resonance_tab 元素下拉共用);label 双语显示,tag 存储,index 对齐
const ELEM_LABELS := ["火 Fire", "冰 Ice", "雷 Lightning", "水 Water", "毒 Poison"]
const ELEM_TAGS := ["tag:fire", "tag:ice", "tag:lightning", "tag:water", "tag:poison"]
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)
s.size_flags_horizontal = Control.SIZE_EXPAND_FILL
return s
static func line(text: String, placeholder: String = "") -> LineEdit:
@@ -14,6 +19,7 @@ static func line(text: String, placeholder: String = "") -> LineEdit:
le.text = text
le.placeholder_text = placeholder
le.custom_minimum_size = Vector2(120, 0)
le.size_flags_horizontal = Control.SIZE_EXPAND_FILL
return le
static func opt(items: Array, selected: int) -> OptionButton:
@@ -21,6 +27,7 @@ static func opt(items: Array, selected: int) -> OptionButton:
for it in items:
o.add_item(str(it))
o.selected = clampi(selected, 0, items.size() - 1)
o.size_flags_horizontal = Control.SIZE_EXPAND_FILL
return o
static func header(text: String) -> Label:
+11 -10
View File
@@ -4,13 +4,14 @@ extends VBoxContainer
const UI = preload("res://addons/game_designer/designer_ui.gd")
const PATH = "res://data/resonance.json"
const MATCH = ["adjacent", "anywhere_in_deck"]
const MATCH = ["adjacent", "anywhere_in_deck"] # 存储 key
const MATCH_LABELS = ["相邻 adjacent", "卡组内任意 anywhere_in_deck"]
var _data: Array = []
var _cur: int = -1
var _id: LineEdit
var _p0: LineEdit
var _p1: LineEdit
var _p0: OptionButton
var _p1: OptionButton
var _match: OptionButton
var _result: LineEdit
var _consume: CheckBox
@@ -29,9 +30,9 @@ func _ready() -> void:
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))
_p0 = _row("元素 A", UI.opt(UI.ELEM_LABELS, 0))
_p1 = _row("元素 B", UI.opt(UI.ELEM_LABELS, 0))
_match = _row("匹配方式", UI.opt(MATCH_LABELS, 0))
_result = _row("产物法术 ID", UI.line("", "action_plasma_storm"))
_consume = CheckBox.new(); _consume.text = "消耗输入法术(合成后移除原法术)"
add_child(_consume)
@@ -61,8 +62,8 @@ func _on_select(idx: int) -> void:
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 ""
_p0.selected = max(0, UI.ELEM_TAGS.find(String(p[0])) if p.size() > 0 else 0)
_p1.selected = max(0, UI.ELEM_TAGS.find(String(p[1])) if p.size() > 1 else 0)
_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))
@@ -70,7 +71,7 @@ func _on_select(idx: int) -> void:
func _on_apply() -> void:
var r := {
"id": _id.text.strip_edges(),
"pattern": [_p0.text.strip_edges(), _p1.text.strip_edges()],
"pattern": [UI.ELEM_TAGS[_p0.selected], UI.ELEM_TAGS[_p1.selected]],
"match": MATCH[_match.selected],
"result_spell_id": _result.text.strip_edges(),
"consume_inputs": _consume.button_pressed,
@@ -83,7 +84,7 @@ func _on_apply() -> void:
UI.set_status(_status, "✓ 已应用(记得保存)")
func _on_new() -> void:
_data.append({"id": "new_recipe", "pattern": ["tag:a", "tag:b"],
_data.append({"id": "new_recipe", "pattern": ["tag:water", "tag:lightning"],
"match": "adjacent", "result_spell_id": "", "consume_inputs": true})
_cur = _data.size() - 1
_refresh()
+28 -11
View File
@@ -7,11 +7,12 @@ extends VBoxContainer
const UI = preload("res://addons/game_designer/designer_ui.gd")
const PATH := "res://data/spells.json"
const TYPE_NAMES := ["ACTION", "MODIFIER", "TRIGGER", "LOGIC"]
const ACTION_KINDS := ["弹道", "zone 毒池", "summon 召唤"] # 0=默认(不写 action_kind) 1=zone 2=summon
const LOGIC_OPS := ["every_n_shots", "if_hp_below", "if_enemy_nearby", "loop"]
const DMG_LABELS := ["物理", "", "", "", "", "奥术"] # index = damage_type 整数
const ST_LABELS := ["", "燃烧", "冻结", "中毒", "潮湿", "沾油", "眩晕", "连击", "易伤"] # index = status 整数
const TYPE_NAMES := ["动作 ACTION", "修饰 MODIFIER", "触发 TRIGGER", "逻辑 LOGIC"]
const ACTION_KINDS := ["弹道 Projectile", "毒池 Zone", "召唤 Summon"] # 0=默认(不写 action_kind) 1=zone 2=summon
const LOGIC_OPS := ["every_n_shots", "if_hp_below", "if_enemy_nearby", "loop"] # 存储 key,不变
const LOGIC_OP_LABELS := ["每 N 次 every_n_shots", "生命低于 if_hp_below", "附近有敌 if_enemy_nearby", "循环 loop"] # 下拉显示
const DMG_LABELS := ["物理 Physical", "火 Fire", "冰 Ice", "雷 Lightning", "毒 Poison", "奥术 Arcane"] # index = damage_type 整数
const ST_LABELS := ["无 None", "燃烧 Burn", "冻结 Freeze", "中毒 Poison", "潮湿 Wet", "沾油 Oily", "眩晕 Stun", "连击 Combo", "易伤 Vuln"] # index = status 整数
var _data: Dictionary = {}
var _cur: String = ""
@@ -21,7 +22,8 @@ var _id: LineEdit
var _type: OptionButton
var _name: LineEdit
var _desc: LineEdit
var _tags: LineEdit
var _elem_checks: Array = []
var _custom_tags: LineEdit
var _mana: SpinBox
var _shop: SpinBox
var _unlock: SpinBox
@@ -52,7 +54,14 @@ func _ready() -> void:
_type.item_selected.connect(_on_type_or_sub_changed)
_name = _row("显示名", UI.line("", "tr 键或文本"))
_desc = _row("描述", UI.line("", "tr 键或文本"))
_tags = _row("元素标签", UI.line("", "逗号分隔,如 tag:fire"))
var erow := HBoxContainer.new()
erow.add_child(UI.cell_label("元素标签"))
_elem_checks.clear()
for i in UI.ELEM_LABELS.size():
var cb := CheckBox.new(); cb.text = UI.ELEM_LABELS[i]
_elem_checks.append(cb); erow.add_child(cb)
add_child(erow)
_custom_tags = _row("自定义标签", UI.line("", "非元素 tag,逗号分隔"))
add_child(HSeparator.new())
_mana = _row("蓝耗 mana_cost", UI.spin(0, 100000, 1, 0))
@@ -62,7 +71,7 @@ func _ready() -> void:
_akind = UI.opt(ACTION_KINDS, 0)
_akind.item_selected.connect(_on_type_or_sub_changed)
_akind_row = _labeled_row("动作种类", _akind)
_lop = UI.opt(LOGIC_OPS, 0)
_lop = UI.opt(LOGIC_OP_LABELS, 0)
_lop.item_selected.connect(_on_type_or_sub_changed)
_lop_row = _labeled_row("逻辑 logic_op", _lop)
@@ -208,7 +217,13 @@ func _on_select(idx: int) -> void:
_type.selected = clampi(int(d.get("type", 0)), 0, 3)
_name.text = String(d.get("display_name", ""))
_desc.text = String(d.get("description", ""))
_tags.text = ",".join(d.get("element_tags", []))
var etags: Array = d.get("element_tags", [])
for i in _elem_checks.size():
_elem_checks[i].button_pressed = UI.ELEM_TAGS[i] in etags
var custom_list := []
for t in etags:
if not (String(t) in UI.ELEM_TAGS): custom_list.append(String(t))
_custom_tags.text = ",".join(custom_list)
var ak := String(meta.get("action_kind", ""))
_akind.selected = (1 if ak == "zone" else (2 if ak == "summon" else 0))
_lop.selected = maxi(0, LOGIC_OPS.find(String(meta.get("logic_op", "every_n_shots"))))
@@ -287,9 +302,11 @@ func _on_apply() -> void:
if not (meta is Dictionary):
UI.set_status(_status, "✗ 其它(JSON) 不是合法对象", true); return
var tags: Array = []
for t in _tags.text.split(",", false):
for i in _elem_checks.size():
if _elem_checks[i].button_pressed: tags.append(UI.ELEM_TAGS[i])
for t in _custom_tags.text.split(",", false):
var s := t.strip_edges()
if s != "": tags.append(s)
if s != "" and not (s in tags): tags.append(s)
if _cur != "" and _cur != sid and _data.has(_cur):
_data.erase(_cur)
_data[sid] = {"type": _type.selected, "display_name": _name.text,
+1 -1
View File
@@ -4,7 +4,7 @@ extends VBoxContainer
const UI = preload("res://addons/game_designer/designer_ui.gd")
const PATH = "res://data/status_effects.json"
const STACK = ["REFRESH 刷新", "INTENSITY 叠层", "INDEPENDENT 独立"]
const STACK = ["刷新 REFRESH", "叠层 INTENSITY", "独立 INDEPENDENT"]
var _data: Dictionary = {}
var _cur: String = ""
+2 -2
View File
@@ -4,8 +4,8 @@ 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 TYPE_NAMES = ["杂兵 Basic", "快速 Fast", "护甲 Armored", "精英 Elite", "首领 MiniBoss", "终焉 Boss"]
const BOSS_NAMES = [" None", "首领 MiniBoss", "终焉 Boss"] # 映射到 boss 值 -1/4/5
const BOSS_VALS = [-1, 4, 5]
var _waves: Array = []
+4 -3
View File
@@ -27,11 +27,12 @@ Godot 编辑器 → 底部面板 → 🎮 游戏设计器 → ✨ 法术 标签
└────────────────────────┘
[➕新增] [⧉复制] [🗑删除]
────────────────────────
ID(唯一)/ 类型[ACTION ▾]/ 显示名 / 描述 / 元素标签(逗号)
ID(唯一)/ 类型[动作 ACTION ▾]/ 显示名 / 描述
元素标签:[✓]火 [ ]冰 [✓]雷 [ ]水 [ ]毒 + 自定义标签框
── 通用 meta ──
蓝耗 mana_cost / 商店价 shop_cost / 解锁价 unlock_cost
── 动态字段(随类型/子类型自动切换)──
ACTION:动作种类[弹道 / zone 毒池 / summon 召唤] + 对应字段
ACTION:动作种类[弹道 / 毒池 / 召唤] + 对应字段
(伤害 / 速度 / 存活 / 半径 / 伤害类型▾ / 施加状态▾ / …)
MODIFIER / TRIGGER / LOGIClogic_op▾ 子选择)各自字段
── 其它(JSON) ── ← schema 之外的键,保留不丢
@@ -50,7 +51,7 @@ MODIFIER / TRIGGER / LOGIClogic_op▾ 子选择)各自字段
2.**ID**(如 `action_lightning`,必须全局唯一)
3.**类型**ACTION / MODIFIER / TRIGGER / LOGIC
4.**显示名 / 描述**(建议填 `tr()` 键,如 `SPELL_LIGHTNING_NAME`,见 [04 本地化](04_localization.md)
5. **元素标签**共鸣用,如 `tag:lightning`;多个用逗号)
5. 勾选 **元素标签**火/冰/雷/水/毒 复选框,可多选;共鸣用);非元素标签填「自定义标签」框
6.**meta 字段**——通用区(蓝耗/商店价/解锁价)常驻;下方动态区随类型(及 ACTION 动作种类 / LOGIC logic_op)自动列出对应字段,直接填数值即可
7.**✓应用到列表** → 校验并写入内存
8.**💾保存文件** → 写入 `res://data/spells.json`