From 9361bf8767714c08e8e1d504e2df96d4270c3111 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Fri, 31 Jul 2026 13:37:15 +0800 Subject: [PATCH] =?UTF-8?q?refactor(attr):=20"core"=20=E6=9D=A5=E6=BA=90?= =?UTF-8?q?=E6=8F=90=E4=B8=BA=E5=B8=B8=E9=87=8F=EF=BC=9B=5Fcompute=5Fattr?= =?UTF-8?q?=20=E8=A1=A5=20assert=EF=BC=9B=E6=B3=A8=E6=98=8E=20execute=5Fsu?= =?UTF-8?q?b=20=E9=A2=84=E7=AE=97=E7=8B=AC=E7=AB=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MOD_SOURCE_CORE 常量取代四处裸字面量(combat_manager:275,276、combat_test:20,21)。 combat_manager:273 的注释自己就警告「漏掉 remove 会累积,且 hard:50 会把它封成一个 看起来合理的数字」——而 remove 那一侧的字面量若打错一个字符,产生的正是这个零诊断 故障:旧份额撤不掉、逐次累积、被硬上限封顶成常数。常量把它变成编译期错误。 _compute_attr 缺定义时返回 0.0 是消费侧的不对称:Task 4 已硬化生产侧(attribute_tab 的 _loaded 拒绝写出零载荷),但手删 JSON 里的 cast_delay_mod 仍会让生效间隔变 0 (每物理帧施法一次)、删 move_speed 则玩家不能动。它有 push_error,但运行时 push_error 到不了任何日志通道(本期已实测),故表现为一块莫名其妙的砖。按 _recompute_attrs 空定义 分支的同款模式补 assert:release 编译掉、开发期立刻中断。 spell_evaluator:332 的新注释说「生效 cpu_limit 已含法杖份额」,读者可能推断整个 VM 都按 cpu_limit 走,但 execute_sub 的子荷载预算仍是独立的 MAX_OPS_PER_CPU * 2(=80)。补一句 注明其独立且属既有行为(改前同样脱节),不改 execute_sub 的行为。 Co-Authored-By: Claude Opus 5 --- scenes/main/combat_test.gd | 4 ++-- scripts/autoloads/player_stats.gd | 12 ++++++++++++ scripts/domain/combat/combat_manager.gd | 4 ++-- scripts/domain/spell_system/spell_evaluator.gd | 2 ++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/scenes/main/combat_test.gd b/scenes/main/combat_test.gd index 50e0e65..4c8640b 100644 --- a/scenes/main/combat_test.gd +++ b/scenes/main/combat_test.gd @@ -17,8 +17,8 @@ func _ready() -> void: # 本场景绕过 CombatManager,故须自行注入法杖运算力份额(否则 cpu_limit=0 → MAX_OPS 退化) # 顺序理由见 combat_manager._rebuild_wand:remove 必须先于 add(否则重复进入会累积), # 而两者又必须都先于 equip_wand(equip_wand 里的 cpu_limit<=0 守卫读的就是注入结果) - PlayerStats.remove_modifiers_from("core") - PlayerStats.add_modifier("cpu_limit", "flat", float(loadout["core"].cpu_limit), "core") + PlayerStats.remove_modifiers_from(PlayerStats.MOD_SOURCE_CORE) + PlayerStats.add_modifier("cpu_limit", "flat", float(loadout["core"].cpu_limit), PlayerStats.MOD_SOURCE_CORE) PlayerManager.equip_wand(loadout["core"], loadout["compiled"]) _run_spawn() diff --git a/scripts/autoloads/player_stats.gd b/scripts/autoloads/player_stats.gd index f2c5d3f..695a577 100644 --- a/scripts/autoloads/player_stats.gd +++ b/scripts/autoloads/player_stats.gd @@ -8,6 +8,12 @@ signal leveled_up(new_level: int) # ── 数据源 ──────────────────────────────────────────── const ATTRIBUTES_JSON: String = "res://data/attributes.json" +## 法杖运算力份额的加成来源标识(combat_manager._rebuild_wand / combat_test 注入时使用) +## 必须用常量而非裸字面量:"core" 在 remove/add 两侧必须逐字相同——remove 那侧打错一个字符, +## 旧份额就不会被撤销而是逐次累积,且 cpu_limit 的 hard(50) 会把累积值封成一个看起来合理的 +## 数字(不是显眼的荒谬值),故障零诊断且极难发现。常量把这个风险变成编译期错误。 +const MOD_SOURCE_CORE: String = "core" + # ── HP ─────────────────────────────────────────────── var hp: float = 100.0 var hp_max: float = 100.0 @@ -232,6 +238,12 @@ func _recompute_attrs() -> void: func _compute_attr(attr_id: String) -> float: var d: Dictionary = _attr_def.get(attr_id, {}) if d.is_empty(): + # 返回 0.0 的后果按属性而异,但都是「莫名其妙的一块砖」:缺 cast_delay_mod → 生效间隔 0 + # → 每物理帧施法一次;缺 move_speed → 玩家不能动;缺 hp_max → 进场即死。 + # Task 4 已硬化生产侧(attribute_tab 的 _loaded 拒绝写出零载荷),这里是消费侧对称的那一半。 + # assert 与 _recompute_attrs 空定义分支同款:运行时 push_error 到不了任何日志通道 + #(已实测,见 plan 工具坑 ⑧),只靠它等于没有诊断;assert 在 release 被编译掉、开发期立刻中断。 push_error("PlayerStats: attributes.json 缺少属性「%s」" % attr_id) + assert(false, "PlayerStats: 缺少属性「%s」——生效值将为 0,运行时 push_error 不可见" % attr_id) return 0.0 return AttributeFormula.compute(float(d.get("base", 0.0)), _mods_for(attr_id), d) diff --git a/scripts/domain/combat/combat_manager.gd b/scripts/domain/combat/combat_manager.gd index 3a442d7..88a5775 100644 --- a/scripts/domain/combat/combat_manager.gd +++ b/scripts/domain/combat/combat_manager.gd @@ -272,8 +272,8 @@ func _rebuild_wand() -> void: # 法杖运算力以加成来源接入(非调用点相加),使 hard 上限作用于生效总值 # remove 必须在 add 之前:本函数每次 install_spell 都会跑,漏了 remove 会让 cpu_limit # 5→10→15 地累积,而 hard: 50 会把它封成一个看起来合理的数字,不是显眼的异常值 - PlayerStats.remove_modifiers_from("core") - PlayerStats.add_modifier("cpu_limit", "flat", float(_equipped_core.cpu_limit) if _equipped_core else 0.0, "core") + PlayerStats.remove_modifiers_from(PlayerStats.MOD_SOURCE_CORE) + PlayerStats.add_modifier("cpu_limit", "flat", float(_equipped_core.cpu_limit) if _equipped_core else 0.0, PlayerStats.MOD_SOURCE_CORE) # ── 法杖存档(ProfileManager A/B 槽,ADR-A2)───────────────── func get_wand_save_data() -> Dictionary: diff --git a/scripts/domain/spell_system/spell_evaluator.gd b/scripts/domain/spell_system/spell_evaluator.gd index cc72738..3fbcd38 100644 --- a/scripts/domain/spell_system/spell_evaluator.gd +++ b/scripts/domain/spell_system/spell_evaluator.gd @@ -331,6 +331,8 @@ func execute_compiled(compiled: CompiledDeck, caster_id: int, spawn_pos: Vector2 var ops_count: int = 0 # 生效 cpu_limit 已含法杖份额(combat_manager._rebuild_wand 以 source="core" 的加成接入) # 下限 1:注入缺失时不把游戏打成砖(0 会让 while 一步都不走);该异常由 equip_wand 处报出 + # 注:仅顶层执行循环。execute_sub 的子荷载预算是独立的 MAX_OPS_PER_CPU * 2(= 80), + # 与 cpu_limit 无关——既有行为,改前同样脱节,本次接线未触及 var max_ops: int = MAX_OPS_PER_CPU * maxi(PlayerStats.cpu_limit, 1) var actions_remaining: int = 1 # 初始为 1,由 multicast_count 加成 while deck.has_next() and ops_count < max_ops: