Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a5f2c1271 | ||
|
|
9361bf8767 | ||
|
|
84c10725e2 | ||
|
|
ba7c885291 | ||
|
|
d259205db2 | ||
|
|
f3d07c04f8 | ||
|
|
04ca402f3c | ||
|
|
9f71108719 | ||
|
|
1aeae19187 | ||
|
|
7884e43147 |
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://chtol501pvkq3
|
||||
@@ -0,0 +1 @@
|
||||
uid://by5xvwsbrofb
|
||||
@@ -0,0 +1 @@
|
||||
uid://b0vs2krneteax
|
||||
@@ -0,0 +1 @@
|
||||
uid://bxtcaicg3ko7l
|
||||
@@ -0,0 +1 @@
|
||||
uid://bv7imd44hcnwy
|
||||
@@ -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" }
|
||||
}
|
||||
|
||||
@@ -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` 逐杖 3–8,以 `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.01,2026-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.1(soft)** | 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.1(soft)** | 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.01(6.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.1(soft) | 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.05,30 /s)。反过来 `wand_fast`(0.0667 > 0.05)能吃满 60 /s,代价是
|
||||
> `[0.05, 0.0667)` 那段无效区间。
|
||||
>
|
||||
> **这正是"单一全局 `hard` 撞上逐杖饱和点"的必然取舍**:`hard` 偏小则慢杖有无效区间,偏大则
|
||||
> 快杖被截断。0.05 选择了**让无效区间只留在一把杖上**,因为无效区间是"花了钱没效果"(欺骗),
|
||||
> 而吃不满硬顶只是"上限没那么高"(诚实)。若日后要彻底消除,正确做法是让 `hard` 逐杖化
|
||||
> (即把它变成 `cores.json` 的一个字段),而不是继续挪这个全局数 —— 那属于新的设计立项。
|
||||
|
||||
### 2.3 公式模块 —— `AttributeFormula`
|
||||
|
||||
**所有加成公式集中在一个专门模块**,与状态持有分离。
|
||||
|
||||
@@ -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_wand:remove 必须先于 add(否则重复进入会累积),
|
||||
# 而两者又必须都先于 equip_wand(equip_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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user