Files
spellforge/addons/game_designer/attribute_tab.gd
T
joywayerandClaude Opus 5 36c4f79f10 fix(attr): 属性页提示自动换行、加载失败拒绝写回、收紧 SpinBox 下限
三处评审必修:

1. 提示 Label 未换行,在 dock 里被裁掉。该 Label 最小宽度 786px,autowrap 为
   AUTOWRAP_OFF,而 designer_panel 的 ScrollContainer 禁用横向滚动、同侪分页实际
   宽度 337–887px —— 被裁的正是「inverse 的 hard 是下限」这半句,即这条提示存在的
   全部理由。加 AUTOWRAP_WORD_SMART,最小宽度降到 17px,337px 下 4 行全可见。
   同时补两处内容缺漏:add_int 下 hard=0 同样表示不钳制(cpu_limit 就是 add_int,
   还是第一行);soft 本期无消费方,注明「留给货架 C」。

2. 加载失败后保存会写出打砖游戏的文件。_data 为空时 _collect 产出四条全 0、无
   display_name 的合法 JSON,PlayerStats 照单全收 → move_speed.base=0、hp_max.base=0
   直接进游戏,玩家不能移动/瞬间死亡且零诊断。加 _loaded 标志:加载失败 push_error,
   _save 开头守卫拒绝写回。并按 player_stats.gd 的既有做法加逐条 is Dictionary 守卫
   (畸形条目如 "hp_max": 5 原会硬崩、dock 停在半构建状态)。

3. SpinBox 下限收紧到 0.0,并订正上一版注释里的误诊。噪声不是「按 step 吸附」的
   普遍现象,而是 Range 的 round((v-min)/step)*step+min 在 |min| 远大于 |v| 时的抵消
   误差(min=-99999 与 step=0.01 相差七个数量级)。实测:min=-99999 且不用 _clean 时
   漂移 2 处,min=0 且不用 _clean 时漂移 0 —— 收紧 min 才是真解法,_clean 并非必需。
   四个属性按定义均非负,故 min=0 也更贴语义。_clean 保留作宽区间属性的廉价保险。

另:无法识别的 combine 改为 push_warning 后回落 hybrid(打字错误不该硬拒)。
按评审裁决不给 COMBINE_KEYS[selected] 加 clampi(UI.opt 已 clamp,不可达),改留注释记依赖。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 12:26:09 +08:00

101 lines
5.2 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 属性编辑器标签 — 玩家属性的 base / soft / hard / combine
## 数据:res://data/attributes.json(权威属性定义见 docs/design/numerical_design.md §1.1
@tool
extends VBoxContainer
const UI = preload("res://addons/game_designer/designer_ui.gd")
const PATH = "res://data/attributes.json"
# 显示与存储解耦:LABELS 仅供下拉显示,KEYS 才写进 JSON,二者 index 对齐
const COMBINE_KEYS = ["hybrid", "inverse", "add_int"]
const COMBINE_LABELS = ["混合 hybrid", "反向 inverse", "整数加 add_int"]
const ATTR_ORDER = ["cpu_limit", "move_speed", "cast_delay_mod", "hp_max"]
var _data: Dictionary = {}
var _rows: Dictionary = {} # attr_id → {"base": SpinBox, "soft": SpinBox, "hard": SpinBox, "combine": OptionButton}
var _status: Label
var _loaded: bool = false # 加载失败时禁止写回,见 _save
func _ready() -> void:
add_child(UI.header("📊 属性编辑器 — 玩家属性 base / soft / hard / combine"))
_load()
var grid := GridContainer.new(); grid.columns = 5; add_child(grid)
for h in ["属性", "基准 base", "软上限 soft", "硬上限 hard", "合成 combine"]:
var l := Label.new(); l.text = h; l.modulate = Color(0.7, 0.8, 1.0)
grid.add_child(l)
for attr_id in ATTR_ORDER:
# _load 已保证:_loaded 时每个条目都是 Dictionary,否则 _data 为空
var d: Dictionary = _data.get(attr_id, {})
grid.add_child(UI.cell_label(String(d.get("display_name", attr_id))))
# 四个属性按定义均非负,故 min = 0;且 min 取 0 可避开 Range 的抵消误差(见 _clean
var sp_base := UI.spin(0.0, 99999.0, 0.01, float(d.get("base", 0.0)))
var sp_soft := UI.spin(0.0, 99999.0, 0.01, float(d.get("soft", 0.0)))
var sp_hard := UI.spin(0.0, 99999.0, 0.01, float(d.get("hard", 0.0)))
var combine_key := String(d.get("combine", "hybrid"))
var idx: int = COMBINE_KEYS.find(combine_key)
if idx < 0 and not d.is_empty():
# 不硬拒(打字错误不该让整页打不开),但必须留下诊断:保存会把它改写成 hybrid
push_warning("attribute_tab: 「%s」的 combine「%s」无法识别,下拉已回落到 hybrid,保存将覆盖原值" % [attr_id, combine_key])
var op := UI.opt(COMBINE_LABELS, idx if idx >= 0 else 0)
grid.add_child(sp_base); grid.add_child(sp_soft); grid.add_child(sp_hard); grid.add_child(op)
_rows[attr_id] = {"base": sp_base, "soft": sp_soft, "hard": sp_hard, "combine": op}
add_child(HSeparator.new())
var tip := Label.new()
tip.text = "hard=0 在 hybrid / add_int 下表示不钳制;inverse 的 hard 是「下限」,填 0 即钳到 0(与 hybrid 相反);add_int 拒绝 pct 加成;soft 本期不参与计算(留给货架 C)。"
# dock 的 ScrollContainer 禁用横向滚动,不换行的话这句会被右侧裁掉(正是最需要看到的半句)
tip.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
tip.modulate = Color(0.7, 0.7, 0.7)
add_child(tip)
add_child(UI.btn("💾 保存 attributes.json", _save))
_status = UI.status_label(); add_child(_status)
## 加载失败必须留痕并禁止写回:本页的零值载荷(move_speed.base = 0 等)是合法 JSON
## PlayerStats 会照单全收,游戏表现为玩家不能移动 / 瞬间死亡且毫无诊断。
func _load() -> void:
var d = UI.load_json(PATH)
_loaded = d is Dictionary
if not _loaded:
push_error("attribute_tab: 无法加载 %s(缺失或格式错误)" % PATH)
_data = {}
return
# 逐条守卫,同 player_stats.gd 的既有做法:畸形条目(如 "hp_max": 5)会让下面的
# Dictionary 赋值硬崩、dock 停在半构建状态;整个文件拒绝加载并禁止写回
for k in d:
if not (d[k] is Dictionary):
push_error("attribute_tab: %s 的「%s」应为对象,整个文件已拒绝加载" % [PATH, k])
_loaded = false
_data = {}
return
_data = d
## Range 用 round((v - min) / step) * step + min 吸附取值。min = -99999 与 0.01 的 step
## 相差七个数量级,此式在此发生抵消误差(0.01 → 0.00999999999476),直接写回会把噪声
## 固化进 JSON。本页已把 min 收紧到 0.0,当前 12 个值全部逐位精确、并不依赖本函数;
## 保留它是给将来真需要宽区间(含负值)的属性的廉价保险。
static func _clean(v: float) -> float:
return snappedf(v, 0.000001)
## 表单 → 字典(合并式:以 _data 为基底保留未知键)
func _collect() -> Dictionary:
var out: Dictionary = _data.duplicate(true)
for attr_id in ATTR_ORDER:
var r: Dictionary = _rows.get(attr_id, {})
if r.is_empty():
continue
var entry: Dictionary = out.get(attr_id, {}).duplicate(true)
entry["base"] = _clean(r["base"].value)
entry["soft"] = _clean(r["soft"].value)
entry["hard"] = _clean(r["hard"].value)
# 存 key,不存双语显示串;selected 恒为 0..2UI.opt 构造时已 clampi,本页此后不再赋值)
entry["combine"] = COMBINE_KEYS[r["combine"].selected]
out[attr_id] = entry
return out
func _save() -> void:
if not _loaded:
UI.set_status(_status, "✗ 定义未加载,拒绝写回", true)
return
if UI.save_json(PATH, _collect()):
UI.set_status(_status, "💾 已保存(重启 F5 生效)")
else:
UI.set_status(_status, "✗ 保存失败", true)