Compare commits

...
10 Commits
Author SHA1 Message Date
joywayerandClaude Opus 5 9a5f2c1271 chore: 补提交 addons/godot_mcp 的 35 个 .gd.uid,与全项目约定对齐
此前 godot_mcp 是全项目唯一不跟踪 .uid 的目录:
  scripts/                51 .gd / 51 .uid
  addons/game_designer/   13 .gd / 13 .uid
  addons/godot_mcp/       35 .gd /  0 .uid  ← 唯一例外
且 .gitignore 无任何 .uid 规则,CLAUDE.md 亦要求新增 .gd 连同 .gd.uid 一并提交。

.uid 是稳定资源标识符,不跟踪则其它机器扫描时会重新生成不同 UID,
可能打断 uid:// 引用;Godot 官方亦建议提交。一并消除长期挂在
git status 里的 35 行噪声。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 15:00:25 +08:00
joywayerandClaude Opus 5 9361bf8767 refactor(attr): "core" 来源提为常量;_compute_attr 补 assert;注明 execute_sub 预算独立
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 <noreply@anthropic.com>
2026-07-31 13:37:15 +08:00
joywayerandClaude Opus 5 84c10725e2 docs: 订正 wand_fast 饱和点 0.05→0.0667;roadmap 关闭已决定项并澄清移速表述
【订正】wand_fast(0.25s) 的饱和点按公式 (1/60)/0.25 = 0.0667,不是 0.05。
numerical_design:21 的标注与它自己上一句的公式直接矛盾,spec/plan 另有三处同源
错误标注——均派生自一句口头转述,实测表里记的 0.0667 一直是对的。

结论也随之改对,不只是改数字:hard=0.05 使 wand_basic(饱和 0.0333) 与
circuit_fork(0.0278) 无无效区间,但 wand_fast 仍剩 [0.05, 0.0667) 这段买不到东西
(较原 hard:0.01 的 6.7 倍缩至 1.33 倍)。仍取 0.05 的理由:饱和点 = (1/60)/法杖
基准间隔,随杖而异,不存在对所有杖都最优的单一 hard——取 0.0667 让 wand_fast 干净
会使另两把杖够不到自己的饱和点,反而制造新的不可达区间。hard 数值不变。

顺带说明实测表里 wand_fast@0.0667 读到 2 帧的原因:采样点比真实边界 1/15 高 3.3e-5
(T 上高 8.3e-6 s),再叠加整帧倍数多花一帧的浮点余量效应,故实际 1 帧上界略低于
0.0667。数据没错,是标签错了。

【roadmap】E3-① 下的 cast_delay_mod 条目仍记为「待人定」并指向两个选项,而该决定
已在 ba7c885 关闭——roadmap 是查「E3 还剩什么」的索引,留着会把已关闭的决定重新打开。
改为已处理,并注明彻底消除需让 hard 逐杖化(挪进 cores.json),属独立立项。

【roadmap】「实测 200→300 px/s」读起来像发版移速由 200 改成 300,与同批改动里
numerical_design 的订正(发版是 200,300 是从未被读过的纸面值)直接冲突。改写为
「发版基准仍是 200;实测手段是临时挂 +50% 加成使生效值变 300」。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 13:36:53 +08:00
joywayerandClaude Opus 5 ba7c885291 fix(attr): cast_delay_mod 硬上限 0.01 → 0.05,消掉两把主力杖的射速无效区间
_handle_auto_cast 用 if 而非 while,每物理帧至多施法一次,故本属性存在饱和点
= (1/60) / 法杖基准间隔:wand_basic(0.5s) ≈ 0.033、wand_fast(0.25s) ≈ 0.05。
原 hard: 0.01 在两把主力杖上都深深落在饱和区内——玩家把 cast_delay_mod 从
0.033 一路买到 0.01,射速一点没变,属"承诺了循环交付不了的东西"。

取 0.05 使 wand_fast 恰在硬上限处饱和、wand_basic 全程无无效区间。
边界实测(同一套 _physics_process 帧距扫描):wand_fast@0.05 = 1 帧/次(60/s)、
wand_basic@0.05 = 2 帧/次(30/s)、请求 0.01 被 maxf(hard,…) 钳回 0.05 后帧距不变、
soft(0.1) 对照仍 3 帧/次(20/s)、circuit_fork@0.05 = 2 帧/次。

未加任何代码钳制:下限已由 attributes.json + AttributeFormula 的 maxf(hard,…)
强制,代码里再加一个会形成重复权威,设计师调 hard 时立刻脱同步。

spec 新增 §2.2b 收录完整实测数据集(饱和点公式与三把杖的值、soft→hard 是台阶
而非曲线、周期恒为 ceil(T×60) 且整帧倍数多花一帧故实际射速恒 ≤ 名义值)——
结论易得而数据难得,故一并留档。circuit_fork 的饱和点 0.0278 标注为公式推导。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 13:18:46 +08:00
joywayerandClaude Opus 5 d259205db2 docs(plan): 补跑守卫的另外三条进入路径,六条全部由运行时而非眼读覆盖
resume_game / _apply_padded(买卡) / apply_wand_save_data 三条原本只经静态核对,
现已在运行时跑到:均 0 条 cpu_limit=0 报错,且 deck/属性状态正确。
连同已跑的 start_game / switch_core / combat_test,六条齐全。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 13:10:24 +08:00
joywayerandClaude Opus 5 f3d07c04f8 docs(plan): 订正 Step 7b 报错归因措辞——MCP 注入脚本的解析告警不是探针
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 13:06:49 +08:00
joywayerandClaude Opus 5 04ca402f3c docs(plan): 回填属性系统计划为实际执行版,勾除除合并决策外全部步骤
按 Task 5 Step 8:每个因评审而变的代码块与断言脚本都替换为真正跑通的版本,
逐处附「原写法为何不可用」——这才是回填的持久价值,否则下次复跑喂出的是
过时代码和可能已失效的断言(归航那期踩过)。

主要回填:AttributeFormula 的 pct 越界守卫与静态类型;PlayerStats 的 add_modifier
校验 / hp 钳制 / reset_for_run 清空 / 空定义分支用 assert;守卫从施法处移到 equip_wand;
cast_delay_mod 改开火时生效(原计划的装备时折叠会让局中加成到下次换杖才生效,
正好废掉货架 C);补 combat_test 注入这个原计划遗漏的消费方;属性页 SpinBox 下限、
_loaded 拒写回、autowrap、两条 push_warning。

订正计划自己的一句假话:原写「四个属性的值全部落在 0.01 格点上,故往返无量化损失」。
实测 min=-99999 时 0.1→0.100000000005821、0.01→0.00999999999476131,二者恰恰都在
0.01 格点上——成因是 Range 吸附式在 min 与 step 相差七个数量级时的抵消误差,与格点
无关;而 0.0001 容差比该漂移大七个数量级,原断言在有 bug 的版本上也是绿的。改为对
落盘文本逐字节比较。

Task 5 另记两条工具坑:运行中游戏的 push_error/push_warning 不进 MCP 的两个日志通道
(只有 print 进),故「跑一局看日志无报错」本身是空断言,必须读编辑器 Debugger 错误页
并先用故意错误做正对照;execute_game_script 不支持 await、三引号会被外层包装破坏。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 13:05:40 +08:00
joywayerandClaude Opus 5 9f71108719 docs(attr): 守卫注释改引函数名而非会腐烂的行号;调试场景补注入顺序理由
Task 3 复审转来的两条 Minor。player_manager 守卫注释里的
「player_stats.gd:173/191」是行号引用,改动一次就失效,改为
_load_attr_definitions / add_modifier 两个函数名。
combat_test 的注入原先只有不变式没有理由,补上 remove→add→equip_wand
三者的先后依据(累积防护 + 守卫读的是注入结果)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 12:55:08 +08:00
joywayerandClaude Opus 5 1aeae19187 docs: 订正 move_speed 300→200 与 cpu_limit「从不读」告警;路线图 E3-① 勾除并订正现状
move_speed 300 从未被任何代码读取,是纸面孤值;200 自 S0 沿用并已围绕它调校
20 波内容与 Boss 弹幕密度,以既成事实为准。cpu_limit 行的「代码硬编码 40×5、
从不读 cpu_limit」告警随本次接线失效,改记生效值构成(玩家基准 0 + 法杖 3–8)
与 hard(50) 作用于加总后的总值;基准值一并由 5 改为 0,否则重蹈 move_speed 的
「纸面值与实现长期脱节」。

路线图 E3 现状原写「player_stats.gd 有 resistance 等占位属性(4/5 stats 未接线)」
与事实不符——权威属性表是 11 个,armor/resistance 根本不在表内,是实现先于设计的
孤儿字段。E3-① 勾除并标注实际范围(只做框架 + 三条死数据),attunement_×4 /
luck / recharge_speed_mod 三项延后各附理由;顺带记 cast_delay_mod 的饱和区待人定。
E1 现状里「玩家侧抗性仍占位」一句同因失效,一并订正。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 12:54:22 +08:00
joywayerandClaude Opus 5 7884e43147 fix(attr): 补齐属性页最后两条静默改写数据的诊断,并订正两处注释
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 <noreply@anthropic.com>
2026-07-31 12:35:12 +08:00
46 changed files with 756 additions and 253 deletions
+18 -6
View File
@@ -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 ULPstringify 仍输出 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:
+1
View File
@@ -0,0 +1 @@
uid://cin2nsghxc2ls
@@ -0,0 +1 @@
uid://c8exes22tfkqj
@@ -0,0 +1 @@
uid://bdigkr0emai80
@@ -0,0 +1 @@
uid://bonthf2yoeyhw
@@ -0,0 +1 @@
uid://bcqa6ux42fs1w
@@ -0,0 +1 @@
uid://bijs5xgewwx3a
@@ -0,0 +1 @@
uid://burglba33dtlu
@@ -0,0 +1 @@
uid://bwlmjlivjq4wt
@@ -0,0 +1 @@
uid://drnul0blh6wv0
@@ -0,0 +1 @@
uid://da135mgraliib
@@ -0,0 +1 @@
uid://bwjnrsson0em
@@ -0,0 +1 @@
uid://dxrh88r7hn546
@@ -0,0 +1 @@
uid://7ffhpvf8g87g
@@ -0,0 +1 @@
uid://dwu2yrh3e5sxu
@@ -0,0 +1 @@
uid://b58ilnv1n62ak
@@ -0,0 +1 @@
uid://d2ng2v8ayfehs
@@ -0,0 +1 @@
uid://b3ogc0lfxeve6
@@ -0,0 +1 @@
uid://c4loc8wwb4qe8
@@ -0,0 +1 @@
uid://bmyqrhfpp382w
@@ -0,0 +1 @@
uid://diyd0jmvjhevd
@@ -0,0 +1 @@
uid://or4r1hf3581w
@@ -0,0 +1 @@
uid://3dbek7farv50
@@ -0,0 +1 @@
uid://f0b3g6xkqbtd
@@ -0,0 +1 @@
uid://c1rd4ny2dftmj
@@ -0,0 +1 @@
uid://dxedxhq1sfhsq
@@ -0,0 +1 @@
uid://bwig0wr0jlne2
@@ -0,0 +1 @@
uid://cmsrsh4xgw30p
@@ -0,0 +1 @@
uid://b0l5v3g6gnkju
@@ -0,0 +1 @@
uid://yc5bqv2o84nn
@@ -0,0 +1 @@
uid://d123ueb43a5sc
+1
View File
@@ -0,0 +1 @@
uid://chtol501pvkq3
+1
View File
@@ -0,0 +1 @@
uid://by5xvwsbrofb
+1
View File
@@ -0,0 +1 @@
uid://b0vs2krneteax
@@ -0,0 +1 @@
uid://bxtcaicg3ko7l
+1
View File
@@ -0,0 +1 @@
uid://bv7imd44hcnwy
+1 -1
View File
@@ -1,6 +1,6 @@
{
"cpu_limit": { "display_name": "运算力 cpu_limit", "base": 0.0, "soft": 20.0, "hard": 50.0, "combine": "add_int" },
"move_speed": { "display_name": "移动速度 move_speed", "base": 200.0, "soft": 600.0, "hard": 800.0, "combine": "hybrid" },
"cast_delay_mod": { "display_name": "施法延迟 cast_delay_mod", "base": 1.0, "soft": 0.1, "hard": 0.01, "combine": "inverse" },
"cast_delay_mod": { "display_name": "施法延迟 cast_delay_mod", "base": 1.0, "soft": 0.1, "hard": 0.05, "combine": "inverse" },
"hp_max": { "display_name": "最大生命 hp_max", "base": 100.0, "soft": 2000.0, "hard": 0.0, "combine": "hybrid" }
}
+3 -3
View File
@@ -17,11 +17,11 @@
| :--- | :--- | :--- | :--- | :--- | :--- |
| `hp_max` | 最大生命 | 100 | 2000 | - | |
| `mana_max` | 最大魔力 | 100 | 1000 | - | 全局蓝条上限,法杖也有自己的上限,取 Min 值 |
| `move_speed` | 移动速度 | 300 | 600 | 800 | 像素/秒 |
| `cast_delay_mod` | 施法延迟修正 | 1.0 (100%) | 0.1 | 0.01 | 越低越快,乘算系数 |
| `move_speed` | 移动速度 | **200** | 600 | 800 | 像素/秒。⚠️ **2026-07-31 订正**:原写 300 从未被任何代码读取过,是纸面孤值;实际实现自 S0 起即为 200(`player_manager.gd``const MOVE_SPEED`,现已改为读 `data/attributes.json`),20 波内容、Boss 弹幕密度、无敌帧 0.5 s 均按此值调校。此处以既成事实为准 —— 改回 300 等于用未经验证的数字推翻一整轮已调校的内容。理由详见 `docs_dev/specs/2026-07-31-player-attributes-design.md` §2.2。 |
| `cast_delay_mod` | 施法延迟修正 | 1.0 (100%) | 0.1 | **0.05** | 越低越快,乘算系数。⚠️ **2026-07-31 由 0.01 上调至 0.05**`player_manager._handle_auto_cast``if` 而非 `while`,**每物理帧至多施法一次**,故本属性存在**饱和点** = `(1/60) / 法杖基准间隔` —— `wand_basic`(0.5 s) ≈ **0.0333**`wand_fast`(0.25 s) ≈ **0.0667**`circuit_fork`(0.6 s) ≈ 0.0278。**饱和点随杖而异,故不存在对所有杖都最优的单一 `hard`**:要让 `wand_fast` 无无效区间需 `hard ≥ 0.0667`,但那会让 `wand_basic` 够不到自己的 0.0333,反而制造新的不可达区间。取 `0.05` 是折中 —— `wand_basic``circuit_fork` 无无效区间,`wand_fast` 仍剩 `[0.05, 0.0667)` 这一小段买不到东西;相对原 `0.01`(两把主力杖都深陷饱和区、从 0.0333 一路买到 0.01 射速纹丝不动)是大幅改进。实测数据(可达档位是台阶而非曲线、周期恒为 `ceil(T×60)`)见 `docs_dev/specs/2026-07-31-player-attributes-design.md` §2.2b。 |
| `recharge_speed_mod` | 充能速度修正 | 1.0 (100%) | 5.0 | - | 越低越快 |
| `luck` | 幸运 | 0 | 100 | - | 影响高阶法术掉率、暴击率 |
| `cpu_limit` | 运算力上限 | 5 | 20 | 50 | `SpellEvaluator` 实际 MAX_OPS = `cpu_limit × 40`(默认 5 → 200 步;软上限 20 → 800 步;硬上限 50 → 2000 步)。升级此属性可执行更复杂的法术链,与 `implementation_plan.md §2.2``MAX_OPS_PER_CPU = 40` 常量对应。 ⚠️**实现现状(2026-07-20)**代码硬编码 `MAX_OPS = 40×5 = 200`**从不读 cpu_limit**——升级此属性对施法预算无效(待修,本设计更优)。 |
| `cpu_limit` | 运算力上限 | **0**(玩家侧) | 20 | 50 | `SpellEvaluator` 实际 MAX_OPS = `生效 cpu_limit × 40`,与 `implementation_plan.md §2.2``MAX_OPS_PER_CPU = 40` 常量对应。**生效值 = 玩家侧基准 0 + 法杖份额(`cores.json` 逐杖 38,以 `source="core"` 的加成来源接入)+ 未来的玩家加成**;`hard`(50) 作用于**加总后的生效值**,故法杖份额也受硬上限约束。玩家侧基准取 0 而非 5 是刻意的:生效值已含法杖的 3–8,取 0 使小木法杖仍为 5 → 200 步(现有平衡零改动),取 5 会让所有法杖的执行预算翻倍;语义上玩家属性是"全局加成",基准 0 正是纯加成语义。 ✅ **2026-07-31 已修复**(原 ⚠️「代码硬编码 MAX_OPS = 40×5 = 200从不读 cpu_limit」现已失效):`spell_evaluator.gd` 改读 `MAX_OPS_PER_CPU * maxi(PlayerStats.cpu_limit, 1)``cores.json` 逐杖配置的运算力自此生效(高速法杖 3 → 120 步、小木 5 → 200、矩阵板 8 → 320,运行时实测)。设计见 `docs_dev/specs/2026-07-31-player-attributes-design.md` §2.2。 |
| `attunement_fire` | 火焰精通 | 0 | 50 | 100 | 每点:火焰伤害 +3%(乘算),火焰状态(点燃/爆燃)触发概率 +2%。影响 `damage_type=FIRE` 的所有投射物。 |
| `attunement_ice` | 冰霜精通 | 0 | 50 | 100 | 每点:冰霜伤害 +3%,冰冻/减速触发概率 +2%。 |
| `attunement_lightning` | 雷电精通 | 0 | 50 | 100 | 每点:雷电伤害 +3%,麻痹/连锁导电触发概率 +2%。 |
@@ -40,7 +40,7 @@ E6-a 玩家无敌帧(i-frames) ← 先行小项:Boss 弹幕公平性前置,
**目标**:让子弹的 pierce 以外的深度维度真正生效,使元素、抗性、轨迹类词条与核心特性产生行为差异。
**现状(代码复核 2026-07-23**
- ~~元素抗性~~ ✅ **完成**2026-07-24`feat/element-resistance`):`apply_damage_from_context``ctx.damage_type``_RESIST` 表传入 `calc_damage(0, res)``enemies.json` 每型可选 `resistances{元素名:-0.9~0.9}`(支持负=弱点加伤);设计器敌人页加 6×5 抗性网格。MCP 实测:抗火0.5→半伤、弱冰-0.5→1.5×、抗性+护甲叠加45。⚠️ **已知限制**DOT 伤害(`status_manager.gd:116` 直调 `apply_damage`)暂不享元素抗性(架构上 DOT 不走 context,见 E2)。(`player_stats.gd:23 resistance` 玩家侧抗性仍占位。)
- ~~元素抗性~~ ✅ **完成**2026-07-24`feat/element-resistance`):`apply_damage_from_context``ctx.damage_type``_RESIST` 表传入 `calc_damage(0, res)``enemies.json` 每型可选 `resistances{元素名:-0.9~0.9}`(支持负=弱点加伤);设计器敌人页加 6×5 抗性网格。MCP 实测:抗火0.5→半伤、弱冰-0.5→1.5×、抗性+护甲叠加45。⚠️ **已知限制**DOT 伤害(`status_manager.gd:116` 直调 `apply_damage`)暂不享元素抗性(架构上 DOT 不走 context,见 E2)。(~~`player_stats.gd:23 resistance` 玩家侧抗性仍占位~~ —— **2026-07-31 订正**:该字段零消费方且不在权威属性表内,属实现先于设计的残留,已随 E3-① 删除;玩家侧减伤真要做时按货架 C 属性词条立项,见 `docs_dev/specs/2026-07-31-player-attributes-design.md` §2.6。)
- ~~连锁/弹跳~~ ✅ **完成**2026-07-24`feat/bullet-bounce`):镜像 pierce 管线——`CastStats.bounce_add``_apply_modifier` 折叠 → `_push_projectile` 写冷数据 → `_check_collision` 命中转向最近未访问敌(`_find_nearest_unvisited`)、`damage_mult×decay` 递减、`visited_targets` 防回跳、命中循环跳过已访问防密集群重复命中。bounce 优先于 pierce(耗尽后落 pierce)。`modifier_bounce` 词条上架。MCP 实测:bounce=2→3 敌 100/90/81、速度重定向证明优先级、折叠 bounce_add=2。
- ~~归航~~ ✅ **完成**2026-07-30`feat/bullet-homing`):镜像 bounce 管线——`CastStats.homing_add`(float) → `_apply_modifier` 折叠 → `_push_projectile` 写冷数据 `homing_strength/homing_range/homing_target_id``_gd_integrate` 在位置积分**之前**调 `_apply_homing`,用 `wrapf` 取最短转向、`clampf``strength*delta` 限角速度、`rotated()` 保速率。目标为**发射后锁定**:仅首帧与目标死亡时经 `_find_nearest_unvisited` 重选;bounce 命中重定向时改写 `homing_target_id`bounce 选目标、homing 追上去)。`modifier_homing` 词条上架。MCP 实测见下方「⚠️ 有意偏离」与 spec §5。
- ⚠️ **有意偏离本文档原方案**:本行原写「`_gd_integrate` 消费 `_enemy_pos_snapshot`」,实现**没有**消费它,而是**删除**了 `_enemy_pos_snapshot` 及其唯一生产者 `EnemyManager.fill_pos_snapshot`。理由:最终设计采用**按 entity_id 锁定目标**(轨迹可读、目标死亡有明确判据 `get_pos_by_id` 哨兵),而该快照**按槽位存位置、不含 entity_id**,结构上支撑不了锁定语义;接上它反而要每帧 O(敌人数) 重扫最近点。净效果是**减少**一份每帧 O(敌人数) 开销。
@@ -81,14 +81,21 @@ E6-a 玩家无敌帧(i-frames) ← 先行小项:Boss 弹幕公平性前置,
**目标**:把商店从"只卖法术"扩成完整 roguelite 经济:核心抽取、属性升级、出售退款,配合玩家属性成长系统。
**现状(代码复核 2026-07-23**`shop_manager.gd` 无 shelf/sell/core/attr 任何字段(grep 零匹配)→ **仅货架 A(法术抽取)**`player_stats.gd``resistance` 等占位属性(4/5 stats 未接线)。无出售退款(G5)。
**现状(代码复核 2026-07-23;属性部分 2026-07-31 订正**`shop_manager.gd` 无 shelf/sell/core/attr 任何字段(grep 零匹配)→ **仅货架 A(法术抽取)**。无出售退款(G5)。
- ⚠️ **2026-07-31 订正**:本行原写「`player_stats.gd``resistance` 等占位属性(4/5 stats 未接线)」,**与事实不符**。权威属性表(`docs/design/numerical_design.md` §1.1)是 **11 个属性**`hp_max`/`mana_max`/`move_speed`/`cast_delay_mod`/`recharge_speed_mod`/`luck`/`cpu_limit`/`attunement_×4`),`armor`/`resistance` **根本不在表内** —— 它们是实现先于设计的孤儿字段(零消费方),已随子计划 ① 删除。真实缺口是:三条**死数据**(`cpu_limit`/`move_speed`/`cast_delay_mod`,消费方已存在只是没接上,子计划 ① 已修)+ 三类**未立项新机制**(`attunement_×4`/`luck`/`recharge_speed_mod`)。详见 `docs_dev/specs/2026-07-31-player-attributes-design.md` §0/§1。
**设计来源**`docs/design/game_design.md §5`(三货架)、`numerical_design.md`(属性曲线、退款比例)。
**依赖**:货架 C 依赖「属性词条系统」先落地;核心抽取复用已有 `cores.json`/`make_core_by_id`
**切分(建议 4 个子计划,属性词条优先)**
1. **属性词条系统**:定义 4/5 缺失 player stats(如暴击/急速/范围/吸血/减伤)+ 数据驱动加成来源;PlayerStats 接线到伤害/射速/…;`attributes.json` + 设计器页。M
1. ~~**属性词条系统**~~**完成**2026-07-31`feat/player-attributes`spec/plan `docs_dev/{specs,plans}/2026-07-31-player-attributes*`):`AttributeFormula` 纯静态公式模块(`hybrid` 连乘 / `inverse` 反向下限 / `add_int``pct` + `pct` 越界钳 0);`PlayerStats``_attr_def` + `_modifiers` 并把公式结果写回静态类型裸字段(读取端零开销);`add_modifier` / `remove_modifiers_from(source)` 按来源增撤;`data/attributes.json` + 设计器「属性」页。**接线三条死数据**:`spell_evaluator` MAX_OPS 改读生效 `cpu_limit`(法杖份额以 `source="core"` 加成接入,运行时实测 3→120 / 5→200 / 8→320 步)、`player_manager``const MOVE_SPEED` 改读 `PlayerStats.move_speed`(**发版基准仍是 200**;实测手段是临时挂一条 `+50%` 加成使生效值变 300,测得玩家实际位移速度同步由 200 变 300 px/s,证明读取端是活的)、`_handle_auto_cast` 开火时乘 `cast_delay_mod`。删孤儿字段 `armor`/`resistance``hp_max`/`cpu_limit` 移出存档(改为派生值)
- ⚠️ **实际范围小于原描述**:原写「定义 4/5 缺失 player stats(如暴击/急速/范围/吸血/减伤)」—— 那些属性名是拟稿推测,不存在于任何权威文档(见上方「现状」订正)。本期只做**框架 + 三条死数据**,明确延后三项,各有理由:
- `attunement_fire/ice/lightning/poison`(4 个元素精通)—— 属**新增玩法维度**而非断线,需接入元素伤害管线,独立立项。
- `luck` —— 权威表定义它「影响暴击率」,而**暴击链目前是死的**(`bullet_manager.gd:81``is_crit` 硬编码为 `false``CastStats.crit_chance` 从未被掷判);打通暴击链是其前置。
- `recharge_speed_mod` —— 代码中**不存在充能系统**(全项目 grep `recharge` 零命中),属新增机制而非断线。
- 另:`mana_max` 的「与法杖取 Min」语义(权威 §1.1)未迁入框架,现状是核心值直接覆盖玩家值;那是独立设计判断,记为已知遗留。
-**`cast_delay_mod` 的饱和区问题已处理(`hard: 0.01 → 0.05`,提交 `ba7c885`,决定已关闭)**`_handle_auto_cast``if` 而非 `while`,每物理帧至多一次施法 → 60 次/秒硬顶,实际速率按**整帧量化**,故饱和点 = `(1/60) / 法杖基准间隔``circuit_fork` 0.0278 / `wand_basic` 0.0333 / `wand_fast` 0.0667)。原 `hard: 0.01` 远在所有杖的饱和点之下,从饱和点买到 0.01 射速纹丝不动。因饱和点随杖而异、不存在对所有杖都最优的单一 `hard`,取 0.05 为折中:`wand_basic` / `circuit_fork` 无无效区间,`wand_fast` 仍剩 `[0.05, 0.0667)`(较原来的 6.7 倍缩至 1.33 倍)。完整实测数据集见 `docs_dev/specs/2026-07-31-player-attributes-design.md` §2.2b。若日后要把 `wand_fast` 那段也消掉,正确做法是让 `hard` **逐杖化**(挪进 `cores.json`)而非继续挪这个全局数 —— 属独立立项,**不是本期遗留待办**。
2. **货架 C(属性购买)**:商店可购属性词条(消耗金币/以太),叠加到 PlayerStats。S~M。
3. **货架 B(核心抽取)**:商店提供 Core 抽取/更换(复用 `_CORE_ROSTER`),与现有换杖打通。S~M。
4. **出售退款 G5**:背包内法术/核心可出售,按 `numerical` 退款比例返还货币。S。
File diff suppressed because it is too large Load Diff
@@ -70,7 +70,7 @@
| :-- | --: | --: | --: | :-- | :-- |
| `cpu_limit` | **0** | 20 | 50 | `add_int` | 修死数据;生效 MAX_OPS 用 `玩家 + 法杖` |
| `move_speed` | **200** | 600 | 800 | `hybrid` | 由 `const` 转为属性 |
| `cast_delay_mod` | 1.0 | 0.1 | 0.01 | `inverse` | 新增,乘到 `core.cast_interval` |
| `cast_delay_mod` | 1.0 | 0.1 | **0.05** | `inverse` | 新增,在**开火时**乘到 `core.cast_interval``hard` 原定 0.012026-07-31 据实测上调至 0.05,见 §2.2b |
| `hp_max` | 100 | 2000 | — | `hybrid` | 已可用,迁入框架 |
**`cpu_limit` 玩家基准取 0**(权威表写 5):因生效值 = 玩家 + 法杖,而法杖已提供 3–8。取 0 使小木法杖仍为 5 → MAX_OPS 200,**与当前行为完全一致,零平衡改动**;取 5 会让所有法杖的执行预算翻倍。语义上玩家属性是「全局加成」(权威 §1.1 开头:「这些属性是全局的,会修正所有法杖的输出」),基准为 0 正是纯加成语义。
@@ -91,6 +91,125 @@ PlayerStats.add_modifier("cpu_limit", "flat", float(core.cpu_limit), "core")
**`mana_max` 本期不迁入**:权威 §1.1 写明「法杖也有自己的上限,取 Min 值」,而现状是核心值直接覆盖玩家值。那个 Min 语义需要独立的设计判断(取 Min 后玩家升级蓝上限在低上限法杖下完全无效,这是否是设计意图?),不应混进属性框架这一期顺手改。本期保持原样,记录为已知遗留。
### 2.2b `cast_delay_mod` 的饱和点 —— 实测数据与 `hard` 上调(2026-07-31 追记)
> 本节记录的是**运行时实测**,不是推导。数据来自验收期(Task 5 Step 7b②)向 `get_tree().root`
> 挂载的临时 `_physics_process` 扫描节点:16 个相位 × 180 物理帧,逐相位切换法杖与
> `cast_delay_mod`,记录**相邻两次施法的帧距分布**。
#### 现象
`player_manager._handle_auto_cast``if` 而非 `while`
```gdscript
_cast_timer -= delta
if _cast_timer <= 0.0:
_cast_timer = _cast_interval * PlayerStats.cast_delay_mod # 赋值,非 -= 或 +=
SpellEvaluator.execute_compiled(...)
```
因此**每物理帧至多施法一次**,60 Hz 物理步长下硬顶 60 次/秒。且 `_cast_timer` 是**赋值**而非累积扣减,
亚帧余量每周期被丢弃 —— 实际射速按**整帧量化**。
#### 实测表
每个相位的帧距分布都是**单值**(如「1 帧×179」「2 帧×89」),这本身就是量化的直接证据。
| 法杖 | `cast_delay_mod` | 生效间隔 T | 实测帧距 | 实际速率 |
| :-- | --: | --: | --: | --: |
| `wand_basic`(基准 0.5 s | 1.0 | 0.5 s | 31 帧 | 1.94 /s |
| | 0.5 | 0.25 s | 16 帧 | 3.75 /s |
| | **0.1soft** | 0.05 s | 3 帧 | **20 /s** |
| | 0.07 | 0.035 s | 3 帧 | 20 /s |
| | 0.06 | 0.03 s | 2 帧 | 30 /s |
| | **0.05(现 hard** | 0.025 s | 2 帧 | **30 /s** |
| | 0.04 | 0.02 s | 2 帧 | 30 /s |
| | 0.034 | 0.017 s | 2 帧 | 30 /s |
| | **0.033** | 0.0165 s | 1 帧 | **60 /s ← 饱和点** |
| | 0.02 | 0.01 s | 1 帧 | 60 /s |
| | 0.01(原 hard | 0.005 s | 1 帧 | 60 /s |
| `wand_fast`(基准 0.25 s | 1.0 | 0.25 s | 16 帧 | 3.75 /s |
| | **0.1soft** | 0.025 s | 2 帧 | **30 /s** |
| | 0.07 | 0.0175 s | 2 帧 | 30 /s |
| | 0.0667 | 0.016675 s | 2 帧 | 30 /s |
| | **0.05(现 hard** | 0.0125 s | 1 帧 | **60 /s ← 已在饱和区内(饱和点是 0.0667,见下)** |
| | 0.01(原 hard | 0.0025 s | 1 帧 | 60 /s |
#### 四条结论
1. **饱和点 = `(1/60) / 法杖基准间隔`**
| 法杖 | 基准间隔 | 饱和点 | 来源 |
| :-- | --: | --: | :-- |
| `wand_basic` | 0.5 s | ≈ **0.0333** | 实测(0.034 仍 2 帧、0.033 已 1 帧)|
| `wand_fast` | 0.25 s | ≈ **0.0667**= 1/15| 实测夹在 (0.05, 0.0667] 内:0.0667 仍 2 帧、0.05 已 1 帧 |
| `circuit_fork` | 0.6 s | ≈ 0.0278 | **公式推导,未实测** |
> **关于 `wand_fast @ 0.0667` 读到 2 帧**:数据没错,是采样点的问题。真实边界是 `1/15 = 0.06666…`
> 我取的 `0.0667` 比它高 `3.3e-5`T 上高 `8.3e-6 s`),故落在 2 帧那侧。再叠加结论 3 的浮点余量效应
> —— T 恰为整帧倍数时反而多花一帧 —— **实际的 1 帧上界略低于 0.0667**。故 `wand_fast` 的饱和点
> 应读作「0.0667 稍下方」,本文一律以公式值 0.0667 记,误差方向是保守的(真实无效区间只会**更大**一点)。
2. **soft→hard 之间的可达档位是台阶而非曲线**`wand_basic` 只有 20 / 30 / 60 次每秒三档,
`wand_fast` 只有 30 / 60 两档。中间值买不到。
3. **周期恒为 `ceil(T × 60)` 帧,且 T 恰为整帧倍数时会多花一帧**`T = 0.5`**31** 帧而非 30
`T = 0.25`**16** 帧而非 15):`_cast_timer` 是赋值,`0.5 - 30 × delta` 的浮点余量为正,
要到第 31 帧才跨过 0。故**实际射速恒 ≤ 名义值,永不超发** —— 这是好事,但意味着名义
「2 次/秒」实际是 1.94 次/秒。
4. 综上,原 `hard: 0.01` 在两把主力杖上都**深深落在饱和区内**:`wand_basic` 白费 3.3 倍区间、
`wand_fast` 白费 6.7 倍。玩家在货架 C 花钱把 `cast_delay_mod` 从 0.033 买到 0.01**射速一点没变**。
#### 决定(用户 2026-07-31
`hard: 0.01 → 0.05`
**关键前提:饱和点 = `(1/60) / 法杖基准间隔`,随杖而异,因此不存在对所有杖都最优的单一 `hard`。**
`hard` 是**全局**的一个数,而每把杖的饱和点各不相同(0.0278 / 0.0333 / 0.0667),任何取值都只能覆盖一部分:
| `hard` 取值 | `circuit_fork`(0.0278) | `wand_basic`(0.0333) | `wand_fast`(0.0667) |
| :-- | :-- | :-- | :-- |
| 0.01(原值) | 无效区间 0.0278→0.01 | 无效区间 0.0333→0.01 | 无效区间 0.0667→0.016.7 倍)|
| **0.05(已采用)** | ✅ 无无效区间 | ✅ 无无效区间 | ⚠️ 仍剩 `[0.05, 0.0667)` |
| 0.0667 | ✅ | ✅ | ✅ 无无效区间 |
—— 但 `0.0667` 会让 `wand_basic`(饱和 0.0333)与 `circuit_fork`(0.0278)**够不到自己的饱和点**,
即它们的最高射速被硬上限提前截断,**制造出新的"不可达区间"**(想更快但买不到)。
**故 0.05 是折中**:让**多数杖**`wand_basic` / `circuit_fork`,以及所有基准间隔 ≥ 0.333 s 的杖)
无无效区间,只在最快的 `wand_fast` 上留 `[0.05, 0.0667)` 这一小段。相对原 `0.01`
(两把主力杖都深陷饱和区)是大幅改进,但**不是"两把主力杖都不再有无效区间"** ——
`wand_fast` 那段仍在,只是从 6.7 倍缩到 1.33 倍。
> **📌 订正记录(2026-07-31**:本节初稿曾写「`wand_fast` 恰好在 0.05 饱和 / 两把主力杖都无无效区间」,
> **算错了** —— 按同一节自己给出的公式 `(1/60)/0.25 = 0.0667`,不是 0.05。实测数据表(§2.2b 上方)
> 记的 0.0667 一直是对的,错的只是从那句口头转述派生出来的几处标注。已连同
> `numerical_design.md` §1.1、roadmap E3-①、plan Step 7b② 一并订正。
> **教训与本期其余三条同源**:结论若与自己上一句的公式矛盾,是最容易被略过的一类错 ——
> 因为读者会假定作者算过。
**明确不做**:不在代码里加下限钳制。下限已由 `attributes.json` + `AttributeFormula`
`maxf(hard, ...)` 强制;在 `player_manager` 再加一个会形成**重复权威**,设计师调 `hard` 时立刻脱同步,
违反 CLAUDE.md「纯数据驱动,代码不含硬编码副本或回退」。
#### 改动后的边界复测(`hard = 0.05` 落地后,同一套扫描)
| 法杖 | 请求 `mod` | 生效 `mod` | 生效间隔 T | 实测帧距 | 实际速率 | 判读 |
| :-- | --: | --: | --: | --: | --: | :-- |
| `wand_fast` | 0.05 | 0.05 | 0.0125 s | **1 帧×179** | 60 /s | ⚠️ 已达 60 /s,但饱和点是 0.0667 —— `[0.05, 0.0667)` 这段买了没用 |
| `wand_fast` | 0.01 | **0.05** | 0.0125 s | 1 帧×179 | 60 /s | ✅ `maxf(hard, …)` 生效,越界请求被钳回 |
| `wand_basic` | 0.05 | 0.05 | 0.025 s | **2 帧×89** | 30 /s | ✅ |
| `wand_basic` | 0.01 | **0.05** | 0.025 s | 2 帧×89 | 30 /s | ✅ 钳制生效 |
| `wand_basic` | 0.1soft | 0.1 | 0.05 s | 3 帧×59 | 20 /s | ✅ 对照,soft 段未受影响 |
| `circuit_fork` | 0.05 | 0.05 | 0.03 s | **2 帧×89** | 30 /s | ✅ 见下方遗留 |
> **遗留(实测)**`circuit_fork`0.6 s)在新 `hard` 0.05 处实测为 **2 帧/次(30 /s** ——
> 它的饱和点 0.0278(公式推导)低于 0.05,故**没有**无效区间,但也吃不满 60 /s`wand_basic`
> 同理(0.0333 < 0.0530 /s)。反过来 `wand_fast`0.0667 > 0.05)能吃满 60 /s,代价是
> `[0.05, 0.0667)` 那段无效区间。
>
> **这正是"单一全局 `hard` 撞上逐杖饱和点"的必然取舍**:`hard` 偏小则慢杖有无效区间,偏大则
> 快杖被截断。0.05 选择了**让无效区间只留在一把杖上**,因为无效区间是"花了钱没效果"(欺骗),
> 而吃不满硬顶只是"上限没那么高"(诚实)。若日后要彻底消除,正确做法是让 `hard` 逐杖化
> (即把它变成 `cores.json` 的一个字段),而不是继续挪这个全局数 —— 那属于新的设计立项。
### 2.3 公式模块 —— `AttributeFormula`
**所有加成公式集中在一个专门模块**,与状态持有分离。
+4 -2
View File
@@ -15,8 +15,10 @@ func _ready() -> void:
# 装备法杯:wand_basic + spark_bolt
var loadout: Dictionary = WandPreset.make_default_loadout()
# 本场景绕过 CombatManager,故须自行注入法杖运算力份额(否则 cpu_limit=0 → MAX_OPS 退化)
PlayerStats.remove_modifiers_from("core")
PlayerStats.add_modifier("cpu_limit", "flat", float(loadout["core"].cpu_limit), "core")
# 顺序理由见 combat_manager._rebuild_wandremove 必须先于 add(否则重复进入会累积),
# 而两者又必须都先于 equip_wandequip_wand 里的 cpu_limit<=0 守卫读的就是注入结果)
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()
+12
View File
@@ -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)
+2 -2
View File
@@ -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:
+2 -1
View File
@@ -93,7 +93,8 @@ func equip_wand(core: CoreDefinition, compiled: CompiledDeck) -> void:
PlayerStats.mana_heat = 0.0
# 0 守卫:cpu_limit=0 → MAX_OPS 退化,法术近乎全静默。放在换杖处而非施法处——施法端 2 次/秒
# 会刷屏埋掉根因,且此处拿得到杖名。兜的是「新调用点忘了注入」与「cores.json 某杖配成 0」,
# 二者上游无任何报错(缺 attributes.json / attr_id 打错已分别由 player_stats.gd:173/191 报出)
# 二者上游无任何报错(缺 attributes.json / attr_id 打错已分别由 PlayerStats._load_attr_definitions
# 与 PlayerStats.add_modifier 报出)
if PlayerStats.cpu_limit <= 0:
push_error("PlayerManager: 装备「%s」后 cpu_limit=0,法术执行预算将退化为 40 步;检查该杖 cores.json 的 cpu_limit 与 source=\"core\" 加成注入" % (core.display_name if core else ""))
@@ -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: