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>
This commit is contained in:
@@ -13,6 +13,7 @@ const ATTR_ORDER = ["cpu_limit", "move_speed", "cast_delay_mod", "hp_max"]
|
|||||||
var _data: Dictionary = {}
|
var _data: Dictionary = {}
|
||||||
var _rows: Dictionary = {} # attr_id → {"base": SpinBox, "soft": SpinBox, "hard": SpinBox, "combine": OptionButton}
|
var _rows: Dictionary = {} # attr_id → {"base": SpinBox, "soft": SpinBox, "hard": SpinBox, "combine": OptionButton}
|
||||||
var _status: Label
|
var _status: Label
|
||||||
|
var _loaded: bool = false # 加载失败时禁止写回,见 _save
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_child(UI.header("📊 属性编辑器 — 玩家属性 base / soft / hard / combine"))
|
add_child(UI.header("📊 属性编辑器 — 玩家属性 base / soft / hard / combine"))
|
||||||
@@ -22,29 +23,54 @@ func _ready() -> void:
|
|||||||
var l := Label.new(); l.text = h; l.modulate = Color(0.7, 0.8, 1.0)
|
var l := Label.new(); l.text = h; l.modulate = Color(0.7, 0.8, 1.0)
|
||||||
grid.add_child(l)
|
grid.add_child(l)
|
||||||
for attr_id in ATTR_ORDER:
|
for attr_id in ATTR_ORDER:
|
||||||
|
# _load 已保证:_loaded 时每个条目都是 Dictionary,否则 _data 为空
|
||||||
var d: Dictionary = _data.get(attr_id, {})
|
var d: Dictionary = _data.get(attr_id, {})
|
||||||
grid.add_child(UI.cell_label(String(d.get("display_name", attr_id))))
|
grid.add_child(UI.cell_label(String(d.get("display_name", attr_id))))
|
||||||
var sp_base := UI.spin(-99999.0, 99999.0, 0.01, float(d.get("base", 0.0)))
|
# 四个属性按定义均非负,故 min = 0;且 min 取 0 可避开 Range 的抵消误差(见 _clean)
|
||||||
var sp_soft := UI.spin(-99999.0, 99999.0, 0.01, float(d.get("soft", 0.0)))
|
var sp_base := UI.spin(0.0, 99999.0, 0.01, float(d.get("base", 0.0)))
|
||||||
var sp_hard := UI.spin(-99999.0, 99999.0, 0.01, float(d.get("hard", 0.0)))
|
var sp_soft := UI.spin(0.0, 99999.0, 0.01, float(d.get("soft", 0.0)))
|
||||||
var idx: int = COMBINE_KEYS.find(String(d.get("combine", "hybrid")))
|
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)
|
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)
|
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}
|
_rows[attr_id] = {"base": sp_base, "soft": sp_soft, "hard": sp_hard, "combine": op}
|
||||||
add_child(HSeparator.new())
|
add_child(HSeparator.new())
|
||||||
var tip := Label.new()
|
var tip := Label.new()
|
||||||
tip.text = "hard=0 在 hybrid 下表示不钳制;inverse 的 hard 是「下限」,填 0 即钳到 0(与 hybrid 相反);add_int 拒绝 pct 加成。"
|
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)
|
tip.modulate = Color(0.7, 0.7, 0.7)
|
||||||
add_child(tip)
|
add_child(tip)
|
||||||
add_child(UI.btn("💾 保存 attributes.json", _save))
|
add_child(UI.btn("💾 保存 attributes.json", _save))
|
||||||
_status = UI.status_label(); add_child(_status)
|
_status = UI.status_label(); add_child(_status)
|
||||||
|
|
||||||
|
## 加载失败必须留痕并禁止写回:本页的零值载荷(move_speed.base = 0 等)是合法 JSON,
|
||||||
|
## PlayerStats 会照单全收,游戏表现为玩家不能移动 / 瞬间死亡且毫无诊断。
|
||||||
func _load() -> void:
|
func _load() -> void:
|
||||||
var d = UI.load_json(PATH)
|
var d = UI.load_json(PATH)
|
||||||
_data = d if d is Dictionary else {}
|
_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
|
||||||
|
|
||||||
## SpinBox 按 step 吸附会引入双精度噪声(0.01 → 0.00999999999476),
|
## Range 用 round((v - min) / step) * step + min 吸附取值。min = -99999 与 0.01 的 step
|
||||||
## 直接写回会把噪声固化进 JSON;抹到 1e-6 精度,远细于本页任何有意义的调参粒度。
|
## 相差七个数量级,此式在此发生抵消误差(0.01 → 0.00999999999476),直接写回会把噪声
|
||||||
|
## 固化进 JSON。本页已把 min 收紧到 0.0,当前 12 个值全部逐位精确、并不依赖本函数;
|
||||||
|
## 保留它是给将来真需要宽区间(含负值)的属性的廉价保险。
|
||||||
static func _clean(v: float) -> float:
|
static func _clean(v: float) -> float:
|
||||||
return snappedf(v, 0.000001)
|
return snappedf(v, 0.000001)
|
||||||
|
|
||||||
@@ -59,11 +85,15 @@ func _collect() -> Dictionary:
|
|||||||
entry["base"] = _clean(r["base"].value)
|
entry["base"] = _clean(r["base"].value)
|
||||||
entry["soft"] = _clean(r["soft"].value)
|
entry["soft"] = _clean(r["soft"].value)
|
||||||
entry["hard"] = _clean(r["hard"].value)
|
entry["hard"] = _clean(r["hard"].value)
|
||||||
entry["combine"] = COMBINE_KEYS[r["combine"].selected] # 存 key,不存双语显示串
|
# 存 key,不存双语显示串;selected 恒为 0..2(UI.opt 构造时已 clampi,本页此后不再赋值)
|
||||||
|
entry["combine"] = COMBINE_KEYS[r["combine"].selected]
|
||||||
out[attr_id] = entry
|
out[attr_id] = entry
|
||||||
return out
|
return out
|
||||||
|
|
||||||
func _save() -> void:
|
func _save() -> void:
|
||||||
|
if not _loaded:
|
||||||
|
UI.set_status(_status, "✗ 定义未加载,拒绝写回", true)
|
||||||
|
return
|
||||||
if UI.save_json(PATH, _collect()):
|
if UI.save_json(PATH, _collect()):
|
||||||
UI.set_status(_status, "💾 已保存(重启 F5 生效)")
|
UI.set_status(_status, "💾 已保存(重启 F5 生效)")
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user