From 7884e43147b76bb8a79d5ce2468faad74c4e375c Mon Sep 17 00:00:00 2001 From: Joywayer Date: Fri, 31 Jul 2026 12:35:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(attr):=20=E8=A1=A5=E9=BD=90=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E9=A1=B5=E6=9C=80=E5=90=8E=E4=B8=A4=E6=9D=A1=E9=9D=99?= =?UTF-8?q?=E9=BB=98=E6=94=B9=E5=86=99=E6=95=B0=E6=8D=AE=E7=9A=84=E8=AF=8A?= =?UTF-8?q?=E6=96=AD=EF=BC=8C=E5=B9=B6=E8=AE=A2=E6=AD=A3=E4=B8=A4=E5=A4=84?= =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 条目缺失时无任何诊断(上一轮 `and not d.is_empty()` 引入的回归)。 attributes.json 格式良好但少了 hp_max 时,_loaded 仍为 true、保存照常, _collect 写出 {"base":0,"soft":0,"hard":0,"combine":"hybrid"}。更糟的是这个键 从此「存在」,player_stats.gd 的「缺少属性」报错也不再触发,hp_max=0 完全静默 进游戏;而 hard=0 在 hybrid 下意味着不钳制,连兜底都没有,玩家瞬死。 改为 if d.is_empty() 单独 push_warning,与 combine 那条对称。 2. min=0.0 引入的负值静默钳制同样无诊断("move_speed":{"base":-50} → 写回 0)。 运行时本就把生效值钳到 ≥0,但静默改写权威数据正是 combine 警告要防的事, 并入同一警告块保持处理对称。 3. _clean 的注释末句由描述改为禁令。原文「当前 12 个值并不依赖本函数」是事实, 但读起来像绿灯,而删掉它一直无害——直到有人放宽 min,也就是它存在的唯一场景。 4. _collect 的注释夸大了:未知键只有是对象时才存活,顶层标量键会让 _load 整体 拒绝加载。改为「保留未知属性条目」。 实测:缺 hp_max 与 base=-1234.5 两条警告均如实发出;未改动的 attributes.json 连建三次零警告零报错(误报比不报更糟,已专门验证)。 Co-Authored-By: Claude Opus 5 --- addons/game_designer/attribute_tab.gd | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/addons/game_designer/attribute_tab.gd b/addons/game_designer/attribute_tab.gd index 9d3e98d..1890120 100644 --- a/addons/game_designer/attribute_tab.gd +++ b/addons/game_designer/attribute_tab.gd @@ -32,9 +32,19 @@ func _ready() -> void: 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]) + # 本页凡是「静默改写权威数据」的路径都必须留下诊断,一条都不能漏—— + # 保存会把改写结果写回 attributes.json,而下游 PlayerStats 只会照单全收 + if d.is_empty(): + # 条目缺失时占位的 0 一旦被保存,键就「存在」了, + # player_stats.gd 的「缺少属性」报错从此不再触发,hp_max=0 静默进游戏 + push_warning("attribute_tab: %s 缺少属性「%s」,本页以 0 值占位,保存会把 0 写进文件" % [PATH, attr_id]) + else: + if idx < 0: + # 不硬拒(打字错误不该让整页打不开),但必须留下诊断:保存会把它改写成 hybrid + push_warning("attribute_tab: 「%s」的 combine「%s」无法识别,下拉已回落到 hybrid,保存将覆盖原值" % [attr_id, combine_key]) + for f in ["base", "soft", "hard"]: + if d.has(f) and float(d[f]) < 0.0: + push_warning("attribute_tab: 「%s」的 %s = %s 为负,输入框下限 0 已将其钳制,保存会把 0 写进文件" % [attr_id, f, str(d[f])]) 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} @@ -69,12 +79,14 @@ func _load() -> void: ## Range 用 round((v - min) / step) * step + min 吸附取值。min = -99999 与 0.01 的 step ## 相差七个数量级,此式在此发生抵消误差(0.01 → 0.00999999999476),直接写回会把噪声 -## 固化进 JSON。本页已把 min 收紧到 0.0,当前 12 个值全部逐位精确、并不依赖本函数; -## 保留它是给将来真需要宽区间(含负值)的属性的廉价保险。 +## 固化进 JSON。本页 min = 0.0 时 12 个值原本就逐位精确,本函数当前不改变落盘结果 +##(唯一副作用:0.1 在内存里低 1 ULP,stringify 仍输出 0.1)。 +## 勿因此删除 —— 一旦某属性需要负值 / 宽区间而放宽 min,抵消误差立即回来。 static func _clean(v: float) -> float: return snappedf(v, 0.000001) -## 表单 → 字典(合并式:以 _data 为基底保留未知键) +## 表单 → 字典(合并式:以 _data 为基底保留未知属性条目,以及条目内未列入表单的键; +## 顶层标量键不在此列——_load 已因它们整体拒绝加载) func _collect() -> Dictionary: var out: Dictionary = _data.duplicate(true) for attr_id in ATTR_ORDER: