docs: 攻击/护甲命名统一与类型-奖励抽象分离,落地格面资源批并补验收收口

- 命名统一:攻击类型 {base,dmg_a,dmg_b}->{normal,pierce,magic};护甲 {none,a,b}->{light,ward,heavy}

- 抽象分离:攻击类型=伤害属性轴,充能投桶归奖励定义 charge_grants(开格/标中旗/踩雷/清图仅作触发源)

- 格面资源批(文档+registry):段序七->五、行型 15->10、极性路由取代固定兑付、可选类道具删除

- 验收收口:registry/README/active 登记充能当量折算缺口与两处落差;playtest 陈旧键名注记订正

- 实跑:冒烟 201 项 0 失败,四探针 exit=0 无 FAIL
This commit is contained in:
DKChan committed 2026-09-15 23:17:40 +08:00
1 parent 88a204635f
commit ec0bdde7e0
26 files changed
+586 -479

No files matched your search

@@ -224,25 +224,25 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s
## 2026-09-12 试玩批次 1 → v3 数值批(F7 流程实践)
## 2026-09-12 伤害类型 × 护甲克制批(用户拍板——三段伤害 + 标错惩罚落地)
## 2026-09-12 攻击类型 × 护甲克制批(用户拍板——三段伤害 + 标错惩罚落地)
**触发**:设计会话(本日)定稿"怪物护甲类型(无甲/护甲A/护甲B)× 角色伤害三段构成(基础/伤害A/伤害B)"的克制对策系统(穿甲弹/高爆弹式三角)+ 标错旗随机惩罚。设定表经用户多轮审阅确认"没有问题"后落程序(GDD 正式修订另批进行——本批以表格结论为实现依据)。
**触发**:设计会话(本日)定稿"怪物护甲类型(轻甲/魔法护甲/重甲)× 角色伤害三段构成(普通/穿刺/魔法)"的克制对策系统(穿甲弹/高爆弹式三角)+ 标错旗随机惩罚。(本段写于 2026-09-12,原文使用位置名 无甲/护甲A/护甲B 与 基础/伤害A/伤害B——2026-09-15 命名统一批已全仓改为语义名,映射见下方该批小节。)设定表经用户多轮审阅确认"没有问题"后落程序(GDD 正式修订另批进行——本批以表格结论为实现依据)。
**拍板结构 → 落点**:
| 设定 | 实现落点 |
|---|---|
| 敌人护甲类型:无甲 / 护甲A / 护甲B | EnemyTable 新字段 `armor_type`({none,a,b},缺省 none 向后兼容)· 映射:goblin=none / bugbear=a / ogre_chief=b(教学梯度候选,provisional)· B2.spawn 行值快照入单位字典 |
| 伤害三段:①基础 ②伤害A(开格充能)③伤害B(标雷充能) | B1 战斗段主角先手攻击改三分量:基础 = atk×(1+attack_up)(**不吃克制**,attack_up 消费面不变);伤害A = 本轮开出的资格格计数 ×`damage_a_per_cell`(初始展开格不计——reward_eligible=false);伤害B = 首次标中雷 ×`damage_b_per_flag`(与 weaken 同键每雷实例一次去重——忘拔旗不重复充能);三分量各自 floor 后相加,hero_attack 行携带 base/dmg_a/dmg_b 分量(P2 归因) |
| 克制矩阵:特化 ×150 / 无甲 ×100 / 对位甲 ×50 | SettlementTable 新字段 `armor_effectiveness`({base,dmg_a,dmg_b}×{none,a,b} percent,缺项回退 100)——刀/AOE/清图奖励**恒定表值不吃克制**(道具=万能稳定伤害,暂定待拍板) |
| 伤害A 单轮封顶 | `damage_a_cap = 8`(与 ap_per_round 对齐——防连锁轮一次充能十几个格把特化甲怪一拍秒杀;0 = 不封顶) |
| 敌人护甲类型:轻甲 / 魔法护甲 / 重甲 | EnemyTable 新字段 `armor_type`({light,ward,heavy},缺省 light 向后兼容)· 映射:goblin=light / bugbear=ward / ogre_chief=heavy(教学梯度候选,provisional)· B2.spawn 行值快照入单位字典 |
| 伤害三段:①普通 ②穿刺 ③魔法(后两者由充能供给) | B1 战斗段主角先手攻击改三分量:普通 = atk×(1+attack_up)(**不吃克制**,attack_up 消费面不变);穿刺/魔法 = 充能点数(按攻击类型分桶)×克制(分量各自 floor 后相加);hero_attack 行携带 normal/pierce/magic 分量(P2 归因)。**攻击类型是伤害的属性轴,与来源无关**(2026-09-15 抽象分离批) |
| 充能供给(奖励侧定义,可改) | SettlementTable 字段 `charge_grants`(触发源 → {attack_type, per_unit, cap}):现行 safe_cell(本轮开格,初始展开格不计 reward_eligible=false)→ 穿刺桶 1/格、单轮计数上限 8;flag_hit(首次标中雷,与 weaken 同键每雷实例一次去重)→ 魔法桶 2/雷。换桶/换量/换成增益或道具都只改本字典——攻击类型与矩阵不动 |
| 克制矩阵:克制 ×150 / 中性 ×100 / 被克 ×50 | SettlementTable 新字段 `armor_effectiveness`({normal,pierce,magic}×{light,ward,heavy} percent,缺项回退 100)——刀/AOE/清图奖励**恒定表值不吃克制**(道具=万能稳定伤害,暂定待拍板) |
| 标错旗 → 随机轻惩罚三选一(时效 1 结算) | `_apply_miss_backlash()`:① `enrage` 暴走(敌 +25% 进 enemy_output 池)② 敌方伏击 +2 层(复用 ambush 行)③ `off_guard` 失衡(我方 +25% 进 hero_defense 池 = 多挨打);新 B3 行 ×2(dur=1 与 weaken/ambush 同款先兑付后衰减);沿用 `_flag_miss_seen` 每雷格实例一次去重 |
| 随机载体 | **零新流确定性派生**:(x×7 + y×13 + 板序号×5) mod 3——同种子可回放、不扰动既有三条封闭流取号序;正式侧候选 = RngService 新增封闭流(待拍板) |
| 呈现 | 敌人卡环点行常驻护甲标签(无甲/护甲A/护甲B——克制对策可读前提)· 结算明细主角攻击行拆"(基础 X + 开格 Y + 标雷 Z)" · 反噬效果行入明细流 · settlement_snapshot 增 armor/charge 字段(JSONL 数值分析面) |
| 呈现 | 敌人卡环点行常驻护甲标签(轻甲/魔法护甲/重甲——克制对策可读前提)· 结算明细主角攻击行拆"(普通 X + 穿刺 Y + 魔法 Z)" · 反噬效果行入明细流 · settlement_snapshot 增 armor/charge 字段(JSONL 数值分析面) |
**数值全部 provisional 待拍板/调参**(登记于 enemy.tres / settlement.tres 注释):damage_a_per_cell=1 · damage_a_cap=8 · damage_b_per_flag=2 · 矩阵 150/100/50 · enrage/off_guard 25% · 伏击 +2 层 · 护甲映射 goblin/bugbear/ogre——C3 调参时按 F6 供给平衡式复算(三段充能是新增伤害供给,裸装败北/基线通关率两条冒烟路径已复验仍成立)。
**数值全部 provisional 待拍板/调参**(登记于 enemy.tres / settlement.tres 注释):charge_grants(开格→穿刺桶 1/格 · 计数上限 8 · 标中旗→魔法桶 2/雷 · 无上限)· 矩阵 150/100/50 · enrage/off_guard 25% · 伏击 +2 层 · 护甲映射 goblin/bugbear/ogre——C3 调参时按 F6 供给平衡式复算(三段充能是新增伤害供给,裸装败北/基线通关率两条冒烟路径已复验仍成立)。
**正式侧义务(GDD/registry 修订清单,待文档批)**:B2 GDD(EnemyTable armor_type 字段 + schema)、B1 GDD(F3 主角攻击三分量构成 + flag_miss 反噬编排 + 克制矩阵载体)、B3 GDD(六行 MVP 状态表 + source_class flag_miss 授予)、A2 OQ1(标错惩罚候选升级为三选一随机池 + 重复性口径=每雷格实例一次)、registry(enemy/settlement_table/status_effect 条目 + A3 条款②锚与条款③不等式重推导——标中旗收益从纯 weaken 变"weaken+伤害B 充能")、C3(数值 provisional 接管)。lore 侧:`design/research/lore-nest-metaphor-2026-09-12.md`(伤害A=地利/伤害B=封印暴露、反噬=封印失败——待新会话拍板后写回 game-concept)。
**正式侧义务(GDD/registry 修订清单,待文档批)**:B2 GDD(EnemyTable armor_type 字段 + schema)、B1 GDD(F3 主角攻击三分量构成 + flag_miss 反噬编排 + 克制矩阵载体)、B3 GDD(六行 MVP 状态表 + source_class flag_miss 授予)、A2 OQ1(标错惩罚候选升级为三选一随机池 + 重复性口径=每雷格实例一次)、registry(enemy/settlement_table/status_effect 条目 + A3 条款②锚与条款③不等式重推导——标中旗收益从纯 weaken 变"weaken+魔法 充能")、C3(数值 provisional 接管)。lore 侧:`design/research/lore-nest-metaphor-2026-09-12.md`(穿刺=地利/魔法=封印暴露、反噬=封印失败——待新会话拍板后写回 game-concept)。
**验证**:冒烟 **161/161**(+12:护甲快照三档传播 / 克制矩阵三行表值 / hero_attack 行三分量字段〔全战斗回放 ×2〕/ 标错旗 flag_miss+反噬效果行 / 标中雷充能进主角攻击 / 反噬派生同种子回放)· ui_probe PASS · ui_tune_probe PASS · ui_click_probe(done=true、选用失效=0、问题=0)。
@@ -383,7 +383,7 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s
**背景**:用户(扫雷强玩家)指出"完全正确开图/标雷、零踩雷 ⇒ 存活无压力"——现行扫描只有弱策略基线档(不标旗/邻格盲挖/朴素选道具),技能轴不可测;registry `calibration_gap`(真人显著强于模拟者)此前只有方向无机制。本批 = 测量工具批。
**落地变更**:
- `auto_player.gd` 增完美策略求解器(`perfect_action`/`_deduce`):single + subset 推理不动点(A1 F1 `measure_inferability` 同族规则集,**去 safe_set oracle**——约束未知邻格含雷格,比生成期闸保守);**零盲挖**(只挖已证安全格 = 构造保证零踩雷,扫描末断言兜底违例 quit(1))· **已证雷格全标旗**(旗先于挖——轮末 weaken + 伤害B 首次充能,雷上旗不阻连锁)· 卡死且资源可 → `b1.player_request_refresh()` 轮内刷新 · 否则停手(残 AP 1:1 入 RAP)。输入只经 A2 查询面(reveal_record/is_revealed/has_flag),**禁读棋盘雷区真值**——测推理上界,非全知作弊。core 层零改动;
- `auto_player.gd` 增完美策略求解器(`perfect_action`/`_deduce`):single + subset 推理不动点(A1 F1 `measure_inferability` 同族规则集,**去 safe_set oracle**——约束未知邻格含雷格,比生成期闸保守);**零盲挖**(只挖已证安全格 = 构造保证零踩雷,扫描末断言兜底违例 quit(1))· **已证雷格全标旗**(旗先于挖——轮末 weaken + 魔法 首次充能,雷上旗不阻连锁)· 卡死且资源可 → `b1.player_request_refresh()` 轮内刷新 · 否则停手(残 AP 1:1 入 RAP)。输入只经 A2 查询面(reveal_record/is_revealed/has_flag),**禁读棋盘雷区真值**——测推理上界,非全知作弊。core 层零改动;
- `window_selection` 增 `heal_no_waste` 选项(缺口 < 治疗量不用;完美档启用)+ 完美档轮次门解除(round_gate=0);
- `level_autoplay.gd` 增 `policy` 参数(默认 "normal"——tune_sim_controller/defense_scan/level_debug 既有调用零改动);完美档驱动 `_autoplay_battle_perfect`(move/dig/flag 走 A2 settle_batch、refresh 走 B1 编排、连续拒绝 ≥3 停手防死循环);st 增 `mine_hits`/`refreshes` 计数(mine_hit 证据行埋点);
- `level_monte_carlo.gd` 增第 3 CLI 参 `[policy]`(normal/perfect/both,缺省 normal 兼容既有文档命令)+ 策略/踩雷/刷新列;脚注 F5 带文案同步 [30,50] 带锚定稿。
@@ -409,3 +409,41 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s
- **验收结论:≤40% 线用数值区间杠杆族到不了**——完美玩家结构性优势(零踩雷 = 零直伤 + 全标旗 = weaken −65% 顶格 + 快杀 = 敌出手次数最少)不受值域方差影响。三选待裁决:①承伤硬地板(每结算不可减免平伤——S_min 压缩,B1/B2 域);②weaken 饱和/递减语义(拆"全标旗顶格"基石,B3/B1 域);③极端宽度(数值可用但毁基线档与可读性锚——建议否决)。**本批区间机制保留**(对普通玩家方差体验有效,中点锚不伤当量)。
**验证**:冒烟 **154/149+5 全绿**(区间批 5 新断言 + 既有全部回归含遭遇骰分布/回放)。
## 2026-09-15 攻击/护甲命名统一批(用户拍板"一并修改,统一称呼"——纯改名,数值零改动)
**触发**:GDD 评审(战斗结算)问题 1——位置键 `{base, dmg_a, dmg_b}` / `{none, a, b}` 不承载语义(a/b 是纯位置占位符:既说不出来源,也说不出来克制谁),且同一件事在仓内已分裂成四种写法(敌人系统 `{无, 甲, 重甲}` / 03-内容规模 `无甲/甲A/甲B` / registry `{none, a, b}` / 本 README `无甲/护甲A/护甲B`)。
**新词汇(全仓唯一现行词)**:
| 面 | 新 | 旧(作废) |
|---|---|---|
| 攻击类型(结算表矩阵行键 / hero_attack 行分量) | `normal` / `pierce` / `magic` = 普通 / 穿刺 / 魔法 | `base` / `dmg_a` / `dmg_b` = 基础 / 伤害A / 伤害B |
| 护甲类型(`armor_type` / 矩阵列键) | `light` / `ward` / `heavy` = 轻甲 / 魔法护甲 / 重甲 | `none` / `a` / `b` = 无甲 / 护甲A / 护甲B |
| 结算表字段 | `charge_grants`(触发源→{attack_type, per_unit, cap},同日第二批抽象分离后定型) | `damage_a_per_cell` / `damage_a_cap` / `damage_b_per_flag`("类型×来源"焊死命名;同名批曾短暂改 `pierce_per_cell` / `pierce_cap` / `magic_per_flag`,随即作废) |
| 结算快照 round_facts | `charge_counts`(各触发源计数)+ `charge_points`(按攻击类型分桶点数) | `charge_dmg_a` / `charge_dmg_b`(既有 JSONL 留存文件仍用旧键) |
映射沿教学梯度:goblin `none`→`light`、bugbear `a`→`ward`、ogre_chief `b`→`heavy`;**矩阵数值逐格不动**(pierce→ward 150 / pierce→heavy 50 / magic→heavy 150 / magic→ward 50 / normal 全 100)。
**同日第二批 · 攻击类型与奖励抽象分离**(用户裁定"攻击类型的定义和格子奖励要分离开;开格|标雷 → 奖励;奖励如何定义是可以修改的;不要将两者绑定"):攻击类型 = 伤害的属性轴(普通 / 穿刺 / 魔法,只决定查甲效矩阵哪一行,定义里不含任何棋盘来源);充能 = 按攻击类型分桶的附加伤害储备,**谁能充、充哪个桶 = 奖励定义**(`charge_grants`;开格 / 标中旗 / 踩雷 / 清图一律只作奖励触发源)。原 `pierce_per_cell` / `magic_per_flag` 式命名(类型与来源焊在一根名字里)作废。冒烟新增三条断言:奖励定义在位 ×2 + 行为断言(把"标中旗"奖励改投穿刺桶 → 同一战斗魔法分量归零;还原后步骤流逐行相同——证明改道唯一入口是奖励定义)。
**落点**:GDD(战斗结算 v0.5 / 敌人系统 / 难度曲线与Boss / 03-内容规模)+ registry(`enemy.armor_type`、`settlement_table.armor_effectiveness` 与三字段 + 批次注记)+ 切片(`enemy_table.gd` `ARMOR_TYPES`、`settlement_table.gd` `ARMOR_KEYS` / `ATTACK_TYPE_KEYS`〔原 `COMPONENT_KEYS`〕、`enemy.tres`、`settlement.tres`、`damage_math.gd`、`unit_container.gd`、`battle_orchestrator.gd`、`enemy_card_view.gd`〔敌卡常驻标签改 轻甲 / 魔法护甲 / 重甲〕、`battle_log_view.gd`〔拆行文案改"(普通 X + 穿刺 Y + 魔法 Z)"〕、`smoke_all.gd` / `damage_probe.gd` / `auto_player.gd`);playtest 观测记录保留当时键名并附现名。
**验证(本批实跑)**:冒烟 **201 项检查 0 失败**(含分离批三条新断言:奖励定义在位 ×2 + 行为断言"标中旗奖励改投穿刺桶 → 魔法分量归零、还原后步骤流逐行相同")· `ui_probe` PASS · `ui_tune_probe` PASS · `ui_click_probe`(done=true、选用失效=0、问题=0)· `ui_layout_probe` PASS——四探针 exit=0、FAIL 行 0。命名批纯改名零行为变更;分离批为结构重构,数值链路等价(旧"计数×每单位×克制"与现行"计数→按奖励定义折算点数→×克制"结果相同,冒烟全量回归含回放断言未动)。
## 2026-09-15 验收 + 收口批(两批改动闭合核对;零数值零机制改动;不碰 v0.7 文件)
**背景**:2026-09-15 两批(攻击/护甲命名统一 v0.5 + 攻击类型/奖励抽象分离 v0.6)已完成;本批只读核对"文档口径 ↔ 切片实现"是否闭合,并收口遗留开口。**范围纪律**:`战斗结算.md` / `奖励道具经济.md` 属 v0.7 格面资源批所有者文件、口径仍在移动——本批不改;③ 收口项落在 `registry` + 本 README + `active.md`,GDD 侧只报告、待 v0.7 批整体收口后回填。
**核对结论(两批核心语义 9 项全闭合)**:`charge_grants` 键与值(safe_cell→pierce 1/格 cap 8 · flag_hit→magic 2/雷 cap 0)· 甲效矩阵 9 格(normal 全 100 / pierce→ward 150、heavy 50 / magic→heavy 150、ward 50)· `armor_type` 三值(light/ward/heavy)· `hero_attack` 行分量字段(normal/pierce/magic)· round_facts 键(`charge_counts`/`charge_points`)· 敌卡标签文案(轻甲/魔法护甲/重甲)· 明细行拆行文案"(普通 X + 穿刺 Y + 魔法 Z)"· 攻击类型定义不含来源 · 矩阵数值逐格不动——GDD/registry/tres/代码逐项一致。残留旧名扫描仅命中批次注记"原名"与历史观测记录;来源-类型硬绑扫描确认代码只剩两处计数埋点(`_charge_counts["safe_cell"]` / `["flag_hit"]`)+ `charge_grants` 键 + 行 `source_class` 归因。
**实跑(本批)**:冒烟 **201 项检查 0 失败**;四场景探针 `ui_probe` / `ui_tune_probe` / `ui_click_probe` / `ui_layout_probe` 全部 **exit=0、无 FAIL 行**。
**收口(③,落在非 GDD 文件)**:
- ① **收益对照表充能当量覆盖缺口**:登记于本 README + registry + `active.md`(GDD 收益对照表回填待 v0.7 批收口后)。候选口径 = 充能当量 = Σ各桶点数 × 当轮目标护甲平均甲效倍率 ÷ 100(与直伤同类 1 伤害 = 1 当量,守 行动经济 条款①单一当量);补式 / 登记待拍板二选一待回填批裁定。
- ② **两处已知落差登记**:a) `charge_grants` 触发源可扩展 vs 切片仅两个触发源计数埋点(新增源须加埋点);b) `safe_cell`/`flag_hit` 来源标识跨命名空间同名(明细行 `source_class` / `charge_grants` 键;RewardTable.pool_tag 已随格面资源批改 `cell`)。
- ③ playtest 观测记录的快照键"现名"注记由中间名订正为现行名 `charge_counts`/`charge_points`。
**注意(口径移动)**:本批运行期间 **v0.7 格面资源批**已在 GDD(战斗结算 v0.7 / 奖励道具经济 v0.4)与 registry 落地——段序七→五、行型 15→10、极性路由取代三套固定兑付、可选类道具删除。切片代码尚未同步(registry 注明"切片代码修改随新会话执行、指导已产出"),故本文以上各历史批次条目保留当时陈述。
**两轴分离措辞不变**:攻击类型 = 伤害属性轴(定义不含来源);充能 = 按攻击类型分桶的附加伤害储备,投桶归奖励定义 `charge_grants`;开格/标中旗/踩雷/清图只作奖励触发源。
@@ -61,8 +61,7 @@ var _perfect_hit_false := 0
var _enemy_cap := 0 # EnemyTable 行值快照(伏击 cap)
var _m_amb := 0 # StatusEffectTable.ambush.magnitude 快照
var _hero_atk := 0 # 主角攻击基值快照(encounter.hero_atk;E4 收口——表值运行期不可变)
var _charge_dmg_a := 0 # 伤害A 充能:本轮开出的资格格计数(轮末结算消费后随 facts 重置)
var _charge_dmg_b := 0 # 伤害B 充能:本轮首次标中雷计数 ×damage_b_per_flag(每雷实例一次)
var _charge_counts: Dictionary = {} # 本轮充能计数 {奖励触发源: 计数}——投哪个攻击类型桶由 settlement.charge_grants(奖励定义)决定,本类不认识"开格=穿刺"这类绑定
func _init(db_ref, rng_ref, shared_reward_economy = null) -> void:
@@ -206,7 +205,7 @@ func _settle() -> void:
if evt.kind == "mine":
mine_cells.append(evt.cell)
elif evt.kind == "safe" and evt.reward_eligible:
_charge_dmg_a += 1 # 伤害A 充能:探索本身计费,与该格奖励是否兑付成功无关(初始展开格 reward_eligible=false 不计)
_charge_counts["safe_cell"] = int(_charge_counts.get("safe_cell", 0)) + 1 # 充能计数(开格 = 奖励触发源;只计数,投哪个桶由 charge_grants 决定);初始展开格 reward_eligible=false 不计
_grant_cell(evt.cell, ri, "safe_cell", evt.get("reward_snapshot"))
# reward_eligible=false(initial 通道)不入库——战斗结算 义务②(行动经济 EC5 委派闭环)
for evt in _facts_flags:
@@ -352,7 +351,7 @@ func _judge_flag(evt: Dictionary, ri: int) -> void:
return # 去重跳过(资格以雷格实例为键——排雷玩法 诚实重复上报的对侧);零明细行
_flag_consumed[cell] = true
_perfect_hit_true += 1
_charge_dmg_b += int(db.settlement.damage_b_per_flag) # 伤害B 充能:与 weaken 消费同键(每雷实例一次——忘拔旗不重复充能)
_charge_counts["flag_hit"] = int(_charge_counts.get("flag_hit", 0)) + 1 # 充能计数(标中旗 = 奖励触发源;与 weaken 消费同键,每雷实例一次——忘拔旗不重复充能)
var ok: bool = _status_effects.apply_effect("weaken", {"source_class": "flag_hit", "origin_cell": cell, "round_index": ri})
if ok:
var row: Dictionary = db.status_effect_row("weaken")
@@ -511,23 +510,33 @@ func _apply_selected_item(window_index: int, ri: int) -> void:
func _combat_segment(ri: int) -> void:
var L := _status_effects.pool_layers("ambush") # 授予段后的当轮值;池共享不消耗
# ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动)。
# 三段伤害构成(2026-09-12 用户拍板):基础(atk×(1+attack_up),不吃克制)+ 伤害A(开格充能×克制)+ 伤害B(标雷充能×克制),
# 按目标护甲类型分别乘克制系数后 floor 相加;attack_up 仍只乘基础攻击(刀/AOE 恒定表值不吃修正不变)。
# 三段伤害构成(2026-09-12 用户拍板;2026-09-15 命名统一批 + 攻击类型/奖励抽象分离批):普通(atk×(1+attack_up),不吃克制)
# + 穿刺/魔法(由充能供给——充能按攻击类型分桶,谁能充/充哪个桶归奖励定义 charge_grants)。攻击类型只管查甲效矩阵哪一行,与来源无关。
var hero_target = _enemy_system.default_target()
if hero_target != null:
var armor: String = String(hero_target.get("armor_type", "none"))
var armor: String = String(hero_target.get("armor_type", "light"))
var up := clampi(_pool_effective("attack_up"), int(db.settlement.pool_clamp_lo), int(db.settlement.pool_clamp_hi))
var base := maxi(0, int(floor(float(_hero_atk) * (100.0 + float(up)) / 100.0)))
var comp_a := _charge_damage_component("dmg_a", _charge_dmg_a, armor)
var comp_b := _charge_damage_component("dmg_b", _charge_dmg_b, armor)
var d_hero: int = base + comp_a + comp_b
var points := _charge_points() # {攻击类型: 充能点数}——奖励定义投递的结果(本段不认识"开格/标雷")
var comp: Dictionary = {}
var d_hero := 0
for atk_type in SettlementTable.ATTACK_TYPE_KEYS:
var c: int = 0
if atk_type == "normal":
c = maxi(0, int(floor(float(_hero_atk) * (100.0 + float(up)) / 100.0))) # 普通分量走攻击力基线:吃增益、不吃克制
else:
c = _charge_component(int(points.get(atk_type, 0)), atk_type, armor)
comp[atk_type] = c
d_hero += c
var payload: Dictionary = {"target": hero_target.display_name}
payload.merge(comp)
if d_hero > 0:
_enemy_system.apply_damage(hero_target, d_hero)
_emit_row("damage", "hero_attack", Vector2i(-1, -1), ri, "主角攻击", d_hero, {"target": hero_target.display_name, "killed": not hero_target.alive, "base": base, "dmg_a": comp_a, "dmg_b": comp_b})
payload["killed"] = not hero_target.alive # 击杀标记在扣血后取(原语义不变)
_emit_row("damage", "hero_attack", Vector2i(-1, -1), ri, "主角攻击", d_hero, payload)
else:
# 零值伤害行(战斗结算 边界表:"合成后为 0 时不调扣血接口,改写零值伤害行——免伤与零输出必须可见";
# 敌方侧同款口径已就绪——照抄。三分量字段齐备保 P2 归因不断链)
_emit_row("damage", "hero_attack", Vector2i(-1, -1), ri, "主角攻击(被减伤归零)", 0, {"target": hero_target.display_name, "base": base, "dmg_a": comp_a, "dmg_b": comp_b})
# 敌方侧同款口径已就绪——照抄。分量字段齐备保 P2 归因不断链)
_emit_row("damage", "hero_attack", Vector2i(-1, -1), ri, "主角攻击(被减伤归零)", 0, payload)
for u in _enemy_system.query_alive("enemy"):
if int(_enemy_system.hero_unit().hp) <= 0:
break # hero hp=0 立即短路(死尸挨打行是归因噪音——用户拍板)
@@ -552,19 +561,33 @@ func _resolve_damage(d_raw: int) -> int:
return DamageMath.pool_damage(d_raw, p_hero, p_enemy)
# 伤害A/B 充能分量:充能 × 每单位值 × 克制 percent → floor(伤害A 先套单轮充能上限——防连锁轮失控)
func _charge_damage_component(component: String, count: int, armor: String) -> int:
var per: int = int(db.settlement.damage_a_per_cell) if component == "dmg_a" else int(db.settlement.damage_b_per_flag)
var charge := count
if component == "dmg_a" and int(db.settlement.damage_a_cap) > 0:
charge = mini(charge, int(db.settlement.damage_a_cap))
return DamageMath.charge_component(charge, per, _armor_effectiveness(component, armor))
# 充能点数合成:奖励定义(charge_grants:触发源→{attack_type, per_unit, cap})× 本轮各源计数 → {攻击类型: 点数}
# cap = 单轮计数上限(0 = 不封顶);投桶只读 attack_type 字段——换桶/换量是奖励侧改动,本类不认识"开格=穿刺"这类绑定
func _charge_points() -> Dictionary:
var pts: Dictionary = {}
for src in db.settlement.charge_grants.keys():
var g: Dictionary = db.settlement.charge_grants[src]
var attack_type: String = String(g.get("attack_type", ""))
var per: int = int(g.get("per_unit", 0))
var cap: int = int(g.get("cap", 0))
var n: int = int(_charge_counts.get(src, 0))
if cap > 0:
n = mini(n, cap)
if attack_type == "" or per <= 0 or n <= 0:
continue
pts[attack_type] = int(pts.get(attack_type, 0)) + n * per
return pts
# 克制矩阵读值(SettlementTable.armor_effectiveness:{分量: {护甲: percent}}——缺项回退 100 中性)
func _armor_effectiveness(component: String, armor: String) -> int:
# 充能分量:点数(已含 per_unit × 计数换算)× 克制 percent → floor
func _charge_component(points: int, attack_type: String, armor: String) -> int:
return DamageMath.charge_component(points, _armor_effectiveness(attack_type, armor))
# 克制矩阵读值(SettlementTable.armor_effectiveness:{攻击类型: {护甲: percent}}——缺项回退 100 中性)
func _armor_effectiveness(attack_type: String, armor: String) -> int:
var tbl: Dictionary = db.settlement.armor_effectiveness
var row: Dictionary = tbl.get(component, {})
var row: Dictionary = tbl.get(attack_type, {})
return int(row.get(armor, 100))
@@ -602,7 +625,7 @@ func _emit_composition_effects(side: String, ri: int) -> void:
func _emit_settlement_snapshot(verdict: String) -> void:
var enemies: Array = []
for u in _enemy_system.query_alive("enemy"):
enemies.append({"id": String(u.enemy_id), "hp": int(u.hp), "max_hp": int(u.max_hp), "atk": int(u.atk), "beat": query_beat_of(u), "armor": String(u.get("armor_type", "none"))})
enemies.append({"id": String(u.enemy_id), "hp": int(u.hp), "max_hp": int(u.max_hp), "atk": int(u.atk), "beat": query_beat_of(u), "armor": String(u.get("armor_type", "light"))})
var buffs: Array = []
for eff in _status_effects.query_active("hero"):
buffs.append({"id": eff.effect_id, "side": "hero", "layers": eff.layers, "left": eff.remaining_settlements})
@@ -636,7 +659,7 @@ func _emit_settlement_snapshot(verdict: String) -> void:
"buffs": buffs,
"ambush_layers": _status_effects.pool_layers("ambush"),
"inventory_in_stock": _reward_economy.in_stock_count(),
"round_facts": {"path": _facts_path, "safe": safe_n, "mines": mines_hit, "flag_events": _facts_flags.size(), "charge_dmg_a": _charge_dmg_a, "charge_dmg_b": _charge_dmg_b},
"round_facts": {"path": _facts_path, "safe": safe_n, "mines": mines_hit, "flag_events": _facts_flags.size(), "charge_counts": _charge_counts.duplicate(), "charge_points": _charge_points()},
})
@@ -719,7 +742,7 @@ func presentation_snapshot() -> Dictionary:
"display_name": String(u.display_name),
"hp": int(u.hp), "max_hp": int(u.max_hp),
"enemy_id": String(u.enemy_id),
"armor_type": String(u.get("armor_type", "none")),
"armor_type": String(u.get("armor_type", "light")),
"beat": String(_enemy_system.query_beat(u)) if alive else "",
"ring_size": ring.size(),
"ring_index": int(u.get("steps", 0)) % maxi(ring.size(), 1),
@@ -794,8 +817,7 @@ func _reset_round_facts() -> void:
_facts_reveals.clear()
_facts_flags.clear()
_facts_path = ""
_charge_dmg_a = 0 # 三段伤害充能随轮 facts 重置(每轮重新充能——结算段消费当轮值)
_charge_dmg_b = 0
_charge_counts.clear() # 充能计数随轮 facts 重置(每轮重新攒——结算段按奖励定义折算当轮点数)
# ---- 行协议(Core Rule 14:公共字段 + 行类型特有字段)----
@@ -13,7 +13,6 @@ static func pool_damage(d_raw: int, p_hero: int, p_enemy: int) -> int:
return maxi(0, v)
# 伤害A/B 充能分量:充能(调用方已套单轮充能上限)× 每单位值 × 克制 percent → floor
static func charge_component(charge: int, per_unit: int, effectiveness_percent: int) -> int:
var pts := charge * per_unit
return int(floor(float(pts) * float(effectiveness_percent) / 100.0))
# 充能分量:充能点数(调用方已按奖励定义换算点数、并按源套单轮计数上限)× 克制 percent → floor
static func charge_component(points: int, effectiveness_percent: int) -> int:
return int(floor(float(points) * float(effectiveness_percent) / 100.0))
@@ -67,7 +67,7 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override:
"heavy_multiplier": int(row.heavy_multiplier),
"pattern": Array(row.pattern), # 行值快照(拷贝)
"advantage_per_round_cap": int(row.advantage_per_round_cap),
"armor_type": String(row.get("armor_type", "none")), # 护甲类型快照(克制矩阵消费面——战斗结算·战斗段)
"armor_type": String(row.get("armor_type", "light")), # 护甲类型快照(克制矩阵消费面——战斗结算·战斗段;2026-09-15 命名统一批:light/ward/heavy)
"steps": 0,
"alive": true,
"is_hero": false,
@@ -10,8 +10,9 @@ extends Resource
const PATTERN_MVP := ["attack", "charge", "heavy"]
const PATTERN_RESERVED := ["guard", "boost", "hinder", "summon"]
# 护甲类型封闭枚举(伤害类型 × 护甲克制批——克制矩阵本体在 SettlementTable)
const ARMOR_TYPES := ["none", "a", "b"]
# 护甲类型封闭枚举(攻击类型 × 护甲克制批——克制矩阵本体在 SettlementTable;
# 2026-09-15 命名统一批:位置键 {none, a, b} → 语义键 {light, ward, heavy} = 轻甲 / 魔法护甲 / 重甲)
const ARMOR_TYPES := ["light", "ward", "heavy"]
func validate() -> Array:
@@ -77,10 +78,10 @@ func validate() -> Array:
var cap: int = int(row.get("advantage_per_round_cap", -1))
if cap < 0 or cap > 16:
errs.append({"category": "domain", "message": "%s advantage_per_round_cap 越域 [0,16]" % eid})
# armor_type(可省略——缺省 none 向后兼容)
var at: String = String(row.get("armor_type", "none"))
# armor_type(可省略——缺省 light 向后兼容)
var at: String = String(row.get("armor_type", "light"))
if not ARMOR_TYPES.has(at):
errs.append({"category": "enum", "message": "%s armor_type 未知枚举: %s(域 {none, a, b})" % [eid, at]})
errs.append({"category": "enum", "message": "%s armor_type 未知枚举: %s(域 {light, ward, heavy})" % [eid, at]})
# 每行集恰一敌种(行集内 enemy_id 唯一)
var rk := "%d_%d" % [L, D]
if rowset_species.has(rk):
@@ -14,15 +14,16 @@ extends Resource
@export var pool_clamp_lo: int = -100 # 结构冻结
@export var pool_clamp_hi: int = 100 # 结构冻结
@export var round_direction: String = "floor" # 结构冻结——单一取整规则
# ---- 伤害类型 × 护甲克制批(2026-09-12 用户拍板结构;数值 provisional 待调参)----
# 主角攻击 = 基础(atk×(1+attack_up),不吃克制)+ 开格充能×克制 + 标雷充能×克制(分量各自 floor 后相加)
@export var damage_a_per_cell: int = 1 # 伤害A 充能:本轮每开 1 资格格(reward_eligible 安全格)+N
@export var damage_a_cap: int = 8 # 伤害A 单轮充能上限(0 = 不封顶;8 与 ap_per_round 对齐)
@export var damage_b_per_flag: int = 2 # 伤害B 充能:每颗首次标中雷 +N(沿用每雷实例一次去重——结构上限)
@export var armor_effectiveness: Dictionary = {} # 克制矩阵 percent:{分量 {base|dmg_a|dmg_b}: {护甲 {none|a|b}: percent}}
# ---- 攻击类型 × 护甲克制批 + 充能奖励定义(2026-09-12 用户拍板结构;2026-09-15 命名统一批 / 抽象分离批;数值 provisional)----
# 主角攻击 = 普通(atk×(1+attack_up),不吃克制)+ 穿刺 + 魔法(后两者由充能供给),三分量各自 floor 后相加。
# 【两条抽象分离,勿焊死】攻击类型 = 伤害的属性轴,只决定查 armor_effectiveness 哪一行,定义里不含任何棋盘来源;
# 充能 = 按攻击类型分桶的附加伤害储备,"谁往桶里放东西"属**奖励的定义**(开格/标雷只是奖励触发源,奖励内容可改)。
# 原 damage_a_per_cell / damage_a_cap / damage_b_per_flag 三字段即"类型×来源"焊死的产物,已由 charge_grants 取代。
@export var charge_grants: Dictionary = {} # 奖励侧定义:触发源 → {attack_type(投哪个桶), per_unit(每单位点数), cap(单轮计数上限,0 = 不封顶)}
@export var armor_effectiveness: Dictionary = {} # 克制矩阵 percent:{攻击类型 {normal|pierce|magic}: {护甲 {light|ward|heavy}: percent}}
const ARMOR_KEYS := ["none", "a", "b"]
const COMPONENT_KEYS := ["base", "dmg_a", "dmg_b"]
const ARMOR_KEYS := ["light", "ward", "heavy"]
const ATTACK_TYPE_KEYS := ["normal", "pierce", "magic"]
const FROZEN_SEGMENT_ORDER := ["grant", "item_window", "buff_debuff", "item", "combat", "decay", "verdict"]
const P_PERFECT_DOMAIN := [100, 200]
@@ -41,17 +42,22 @@ func validate() -> Array:
errs.append({"category": "frozen", "message": "round_direction 结构冻结 floor: %s" % round_direction})
if pool_clamp_lo != -100 or pool_clamp_hi != 100:
errs.append({"category": "frozen", "message": "pool_clamp 域结构冻结 [-100,+100]"})
# 伤害类型 × 护甲克制批新字段域校验
if damage_a_per_cell < 0 or damage_a_per_cell > 999:
errs.append({"category": "domain", "message": "damage_a_per_cell 越域 [0,999]: %d" % damage_a_per_cell})
if damage_a_cap < 0 or damage_a_cap > 999:
errs.append({"category": "domain", "message": "damage_a_cap 越域 [0,999](0 = 不封顶): %d" % damage_a_cap})
if damage_b_per_flag < 0 or damage_b_per_flag > 999:
errs.append({"category": "domain", "message": "damage_b_per_flag 越域 [0,999]: %d" % damage_b_per_flag})
for comp in COMPONENT_KEYS:
var row: Dictionary = armor_effectiveness.get(comp, {})
# 充能奖励定义域校验(触发源 → {attack_type, per_unit, cap})
for src in charge_grants.keys():
var g: Dictionary = charge_grants[src]
var g_at: String = String(g.get("attack_type", ""))
if not ATTACK_TYPE_KEYS.has(g_at):
errs.append({"category": "enum", "message": "charge_grants.%s.attack_type 未知攻击类型: %s(域 %s)" % [src, g_at, str(ATTACK_TYPE_KEYS)]})
var g_per: int = int(g.get("per_unit", -1))
if g_per < 0 or g_per > 999:
errs.append({"category": "domain", "message": "charge_grants.%s.per_unit 越域 [0,999]: %d" % [src, g_per]})
var g_cap: int = int(g.get("cap", -1))
if g_cap < 0 or g_cap > 999:
errs.append({"category": "domain", "message": "charge_grants.%s.cap 越域 [0,999](0 = 不封顶): %d" % [src, g_cap]})
for atk_type in ATTACK_TYPE_KEYS:
var row: Dictionary = armor_effectiveness.get(atk_type, {})
for armor in ARMOR_KEYS:
var pct: int = int(row.get(armor, -1))
if pct < 0 or pct > 999:
errs.append({"category": "domain", "message": "armor_effectiveness.%s.%s 越域 [0,999]: %d" % [comp, armor, pct]})
errs.append({"category": "domain", "message": "armor_effectiveness.%s.%s 越域 [0,999]: %d" % [atk_type, armor, pct]})
return errs
@@ -18,7 +18,7 @@ rows = [{
"heavy_multiplier": 2,
"pattern": ["attack"],
"advantage_per_round_cap": 4,
"armor_type": "none"
"armor_type": "light"
}, {
"level_index": 1,
"node_depth": 1,
@@ -33,7 +33,7 @@ rows = [{
"heavy_multiplier": 2,
"pattern": ["attack"],
"advantage_per_round_cap": 4,
"armor_type": "none"
"armor_type": "light"
}, {
"level_index": 1,
"node_depth": 2,
@@ -47,7 +47,7 @@ rows = [{
"heavy_multiplier": 2,
"pattern": ["attack", "charge", "heavy"],
"advantage_per_round_cap": 4,
"armor_type": "a"
"armor_type": "ward"
}, {
"level_index": 1,
"node_depth": 3,
@@ -62,7 +62,7 @@ rows = [{
"heavy_multiplier": 2,
"pattern": ["attack", "charge", "heavy"],
"advantage_per_round_cap": 4,
"armor_type": "a"
"armor_type": "ward"
}, {
"level_index": 1,
"node_depth": 4,
@@ -76,7 +76,7 @@ rows = [{
"heavy_multiplier": 2,
"pattern": ["attack", "attack", "charge", "heavy"],
"advantage_per_round_cap": 4,
"armor_type": "b"
"armor_type": "heavy"
}, {
"level_index": 2,
"node_depth": 0,
@@ -90,7 +90,7 @@ rows = [{
"heavy_multiplier": 2,
"pattern": ["attack", "charge", "heavy"],
"advantage_per_round_cap": 4,
"armor_type": "a"
"armor_type": "ward"
}]
; 切片调参登记 v3(2026-09-12 试玩批次 1 触发——F7 流程;C3 GDD F5 对照):
@@ -103,9 +103,10 @@ rows = [{
; 遭遇随机化批(2026-09-12 用户拍板):D0 [1,2]/D1 [2,3] hp13/D3 [2,3] hp18——数量区间骰(encounter 流,B1 掷骰);
; D2/Boss 固定(新敌种首遇教学位不加骰——C3 GDD Core Rule 8)。期望总量 ≈304(+8%),F6 带内。
;
; 护甲类型批(2026-09-12 用户拍板:伤害类型 × 护甲类型克制——无甲/护甲A/护甲B):
; armor_type ∈ {none, a, b}(缺省 = none 向后兼容);克制矩阵本体在 settlement.tres.armor_effectiveness。
; 映射(教学梯度:无甲→甲A→甲B):goblin=none · bugbear=a · ogre_chief=b(L2 bugbear 同 a)——候选方案,
; 护甲类型批(2026-09-12 用户拍板:攻击类型 × 护甲类型克制;2026-09-15 命名统一批——原位置键/位置名作废):
; armor_type ∈ {light, ward, heavy} = 轻甲 / 魔法护甲 / 重甲(缺省 = light 向后兼容;原 {none, a, b});
; 克制矩阵本体在 settlement.tres.armor_effectiveness。
; 映射(教学梯度:轻甲→魔法护甲→重甲):goblin=light · bugbear=ward · ogre_chief=heavy(L2 bugbear 同 ward)——候选方案,
; 数值与映射均 provisional 待用户调参。
; 敌方攻击区间批(2026-09-14 用户拍板②第二步——"上限掺运气"敌方侧:atk 上阶/下阶,中点恒等行值;
@@ -28,32 +28,46 @@ effect_pool_map = {
pool_clamp_lo = -100
pool_clamp_hi = 100
round_direction = "floor"
damage_a_per_cell = 1
damage_a_cap = 8
damage_b_per_flag = 2
charge_grants = {
"safe_cell": {
"attack_type": "pierce",
"per_unit": 1,
"cap": 8
},
"flag_hit": {
"attack_type": "magic",
"per_unit": 2,
"cap": 0
}
}
armor_effectiveness = {
"base": {
"none": 100,
"a": 100,
"b": 100
"normal": {
"light": 100,
"ward": 100,
"heavy": 100
},
"dmg_a": {
"none": 100,
"a": 150,
"b": 50
"pierce": {
"light": 100,
"ward": 150,
"heavy": 50
},
"dmg_b": {
"none": 100,
"a": 50,
"b": 150
"magic": {
"light": 100,
"ward": 50,
"heavy": 150
}
}
; 伤害类型 × 护甲克制批(2026-09-12 用户拍板结构,数值 provisional 待用户/C3 调参):
; 主角攻击 = 基础(atk×(1+attack_up),对全部护甲 100%)+ 伤害A(开格充能×克制)+ 伤害B(标雷充能×克制),
; 三分量各自 floor 后相加;克制语义 = 特化甲 ×150 / 无甲 ×100 / 对位甲 ×50(穿甲弹/高爆弹式对策三角)。
; damage_a_per_cell=1 · damage_a_cap=8(单轮充能封顶,防连锁轮失控;0 = 不封顶)· damage_b_per_flag=2
; (每颗首次标中雷充能——沿用每雷实例一次去重,忘拔旗不重复充能)。
; 攻击类型 × 护甲克制批(2026-09-12 用户拍板结构;2026-09-15 命名统一批 + 抽象分离批,数值 provisional 待用户/C3 调参):
; 主角攻击 = 普通(atk×(1+attack_up),对全部护甲 100%)+ 穿刺 + 魔法(后两者由充能供给),三分量各自 floor 后相加;
; 克制语义 = 克制 ×150 / 中性 ×100 / 被克 ×50(穿甲弹/高爆弹式对策三角)。
; 【两条抽象分离,勿焊死】攻击类型 {normal, pierce, magic} = 普通 / 穿刺 / 魔法(原 {base, dmg_a, dmg_b})=
; 伤害的属性轴,只决定查矩阵哪一行,定义里不含任何棋盘来源;护甲类型 {light, ward, heavy} = 轻甲 / 魔法护甲 / 重甲
; (原 {none, a, b})是敌单位属性。充能 = 按攻击类型分桶的附加伤害储备:谁能充、充哪个桶 = 奖励定义。
; charge_grants 现行供给(奖励侧定义,可整体替换——换桶/换量/换成增益或道具都不触动攻击类型与矩阵):
; safe_cell(本轮每开 1 资格格)→ 穿刺桶 1/格、单轮计数上限 8(0 = 不封顶);
; flag_hit(每颗首次标中雷,每雷实例一次去重——忘拔旗不重复充能)→ 魔法桶 2/雷,无上限。
; 原位置字段 damage_a_per_cell / damage_a_cap / damage_b_per_flag("类型×来源"焊死的命名)已废除。
; enrage(敌方暴走 +25% 进 enemy_output 池)/ off_guard(我方失衡 +25% 进 hero_defense 池)=
; 标错惩罚池两行(第三项 = 复用 ambush +2 层);时效均 1 结算(用户拍板"短点,比如 1 回合")。
; 减伤墙调参批(2026-09-13,playtest-2026-09-12 报告 Q1 触发):拟改 pool_clamp_lo=-60 被 schema
@@ -172,9 +172,9 @@ func _format_row(row: Dictionary, agg_count := -1) -> String:
var extra := ""
if row.get("baseline", null) != null:
extra = "(基线 %d + 伏击 %d)" % [int(row.baseline), int(row.attach)]
elif row.get("dmg_a", null) != null:
# 主角攻击三分量拆行(伤害×护甲克制批——P2 归因:这刀里哪段是开格/标雷挣的)
extra = "(基础 %d + 开格 %d + 标雷 %d)" % [int(row.base), int(row.dmg_a), int(row.dmg_b)]
elif row.get("pierce", null) != null:
# 主角攻击三分量拆行(攻击类型×护甲克制批——P2 归因:这刀里哪段是穿刺/魔法挣的)
extra = "(普通 %d + 穿刺 %d + 魔法 %d)" % [int(row.normal), int(row.pierce), int(row.magic)]
var kill := " ⚔击杀!" if row.get("killed", false) else ""
var body := "%s -%d%s%s%s%s" % [name_, v, extra, tail, kill, suffix]
var body_col := "#e07050" if row.get("killed", false) else ("#9a8a72" if v == 0 else "#e0a050")
@@ -12,8 +12,8 @@ extends PanelContainer
const COL_INK := Color8(51, 38, 27)
const COL_BEAT := {"attack": Color8(198, 61, 34), "charge": Color8(90, 108, 140), "heavy": Color8(143, 47, 106)}
const BEAT_NAMES := {"attack": "攻击", "charge": "蓄力", "heavy": "重击"}
# 护甲类型呈现(伤害×护甲克制批——可读性前提:玩家须能看出该用开格还是标雷打它)
const ARMOR_NAMES := {"none": "无甲", "a": "护甲A", "b": "护甲B"}
# 护甲类型呈现(攻击类型×护甲克制批——可读性前提:玩家须能看出该用穿刺还是魔法打它)
const ARMOR_NAMES := {"light": "轻甲", "ward": "魔法护甲", "heavy": "重甲"}
var _portrait: Label
var _name_label: Label
@@ -21,7 +21,7 @@ var _hp_bar: ProgressBar
var _intent_badge: Label
var _ring: Label
var _status_label: Label
var _armor := "none" # 护甲类型快照(set_data 写入、set_intent 渲染——环点行常驻标签)
var _armor := "light" # 护甲类型快照(set_data 写入、set_intent 渲染——环点行常驻标签)
var _dead := false
@@ -71,7 +71,7 @@ func set_data(d: Dictionary) -> void:
_hp_bar.max_value = maxi(1, int(d.get("max_hp", 1)))
_hp_bar.value = int(d.get("hp", 0))
_set_order(int(d.get("order", 1)))
_armor = String(d.get("armor_type", "none"))
_armor = String(d.get("armor_type", "light"))
set_intent(String(d.get("beat", "")), int(d.get("ring_size", 0)), int(d.get("ring_index", 0)))
var ambush: int = int(d.get("ambush_layers", 0))
_status_label.text = "伏击池 ×%d(全体共享)" % ambush if ambush > 0 else ""
@@ -111,7 +111,7 @@ func set_intent(beat: String, ring_size: int, ring_index: int) -> void:
func _armor_tag(armor: String) -> String:
return String(ARMOR_NAMES.get(armor, "无甲"))
return String(ARMOR_NAMES.get(armor, "轻甲"))
# 死亡呈现:卡体灰化 + 碎裂退场(禁瞬消)——灰化残位 + 裂纹字符 + 击破标记
@@ -71,7 +71,7 @@ static func window_selection(s: Dictionary, items: Array, opts: Dictionary = {})
# ==================== 完美策略档(2026-09-14 用户拍板:OQ7 技能轴上界测量仪——只作测量,非玩法数值) ====================
# 策略语义:零盲挖(只挖已证安全格 ⇒ 构造保证零踩雷)· 已证雷格全标旗(旗先于挖——旗 = 纯收益
# 〔轮末 weaken + 伤害B 首次充能〕且雷上旗不阻连锁)· 卡死且刷新资源可用 → 轮内刷新
# 〔轮末 weaken + 魔法 首次充能〕且雷上旗不阻连锁)· 卡死且刷新资源可用 → 轮内刷新
# (经 settlement.player_request_refresh 核心编排)· 否则停手(残 AP 1:1 转存 RAP——下轮刷新资源)。
# 推理规则集 = 棋盘生成 F1 measure_inferability 同族(single + subset——solver_rules=single_subset 冻结域);
# 口径差异 = unknown 集不含 棋盘生成 生成期 safe_set oracle(真实玩家约束的未知邻格含雷格)——比生成期闸
@@ -211,8 +211,8 @@ func _analyze_round(db, math, rows: Array, beats: Dictionary) -> Dictionary:
String(beats.get(int(row.get("round_index", 0)), "?")), baseline, attach, raw, pd, pe, tick, expected, zeroed])
ticks.append({"raw": raw, "tick": tick, "expected": expected, "ok": tick == expected})
elif t == "damage" and String(row.get("source_class", "")) == "hero_attack":
lines.append("主角攻击 %d(base %d + 开格 %d + 标雷 %d)→ %s" % [
int(row.get("value", 0)), int(row.get("base", 0)), int(row.get("dmg_a", 0)), int(row.get("dmg_b", 0)), String(row.get("target", ""))])
lines.append("主角攻击 %d(普通 %d + 穿刺 %d + 魔法 %d)→ %s" % [
int(row.get("value", 0)), int(row.get("normal", 0)), int(row.get("pierce", 0)), int(row.get("magic", 0)), String(row.get("target", ""))])
return {"pd": pd, "pe": pe, "grants": grants, "lines": lines, "ticks": ticks}
@@ -332,12 +332,12 @@ func _smoke_full_battle(L: int, D: int, label: String) -> void:
has_verdict = true
if row.get("source_class", "") == "hero_attack":
hero_attacks += 1
# 三段伤害构成行(伤害×护甲克制批):hero_attack 行必须携带 base/dmg_a/dmg_b 分量(P2 归因)
if not (row.has("base") and row.has("dmg_a") and row.has("dmg_b")):
# 三段伤害构成行(攻击类型×护甲克制批):hero_attack 行必须携带 normal/pierce/magic 分量(P2 归因)
if not (row.has("normal") and row.has("pierce") and row.has("magic")):
hero_breakdown_ok = false
check(has_verdict, "%s 步骤流含判定行" % label)
check(hero_attacks >= 1, "%s 主角先手攻击行存在(2026-09-11 拍板①)×%d" % [label, hero_attacks])
check(hero_breakdown_ok, "%s 主角攻击行携带三分量 base/dmg_a/dmg_b(克制批)" % label)
check(hero_breakdown_ok, "%s 主角攻击行携带三分量 normal/pierce/magic(克制批)" % label)
if run1.terminated and run1.result == "victory":
var lc = _last_settlement.level_clear() # 拍板③:关卡结束清库路径(战斗域不再清)
check(lc.has("remaining"), "%s level_clear 返回快照计数(剩余 %d)" % [label, int(lc.get("remaining", -1))])
@@ -609,39 +609,63 @@ func _smoke_difficulty_table() -> void:
_db.difficulty.rows.clear() # 还原空表(后续冒烟/关卡断言基线)
# ---- 伤害类型 × 护甲克制 + 标错惩罚冒烟(2026-09-12 克制批:护甲快照/克制矩阵/充能/反噬三选一)----
# ---- 攻击类型 × 护甲克制 + 标错惩罚冒烟(2026-09-12 克制批:护甲快照/克制矩阵/充能/反噬三选一)----
func _smoke_damage_types() -> void:
print("--- 伤害类型 × 护甲克制 + 标错惩罚 ---")
print("--- 攻击类型 × 护甲克制 + 标错惩罚 ---")
# ① 护甲类型快照传播(spawn 行值 → 单位字典)
var enemy_system = load("res://core/unit_container.gd").new(_db)
check(enemy_system.spawn(1, 0, 50) and String(enemy_system.all_enemies()[0].armor_type) == "none", "goblin 护甲 = none(教学位无甲)")
check(enemy_system.spawn(1, 0, 50) and String(enemy_system.all_enemies()[0].armor_type) == "light", "goblin 护甲 = light(轻甲——教学位)")
var enemy_system_b = load("res://core/unit_container.gd").new(_db)
check(enemy_system_b.spawn(1, 2, 50) and String(enemy_system_b.all_enemies()[0].armor_type) == "a", "bugbear 护甲 = a")
check(enemy_system_b.spawn(1, 2, 50) and String(enemy_system_b.all_enemies()[0].armor_type) == "ward", "bugbear 护甲 = ward(魔法护甲)")
var enemy_system_c = load("res://core/unit_container.gd").new(_db)
check(enemy_system_c.spawn(1, 4, 50) and String(enemy_system_c.all_enemies()[0].armor_type) == "b", "ogre_chief 护甲 = b")
# ② 克制矩阵表值在位(三角:特化 150 / 中性 100 / 对位 50)
check(enemy_system_c.spawn(1, 4, 50) and String(enemy_system_c.all_enemies()[0].armor_type) == "heavy", "ogre_chief 护甲 = heavy(重甲)")
# ② 克制矩阵表值在位(三角:克制 150 / 中性 100 / 被克 50)
var am: Dictionary = _db.settlement.armor_effectiveness
check(int(am.dmg_a.a) == 150 and int(am.dmg_a.b) == 50 and int(am.dmg_a.none) == 100, "伤害A 克制行:甲A 150 / 无甲 100 / 甲B 50")
check(int(am.dmg_b.b) == 150 and int(am.dmg_b.a) == 50 and int(am.dmg_b.none) == 100, "伤害B 克制行:甲B 150 / 无甲 100 / 甲A 50")
check(int(am.base.none) == 100 and int(am.base.a) == 100 and int(am.base.b) == 100, "基础伤害全护甲中性 100(不吃克制)")
# ③ 标错惩罚 + 标雷充能(Boss 行 150HP 多轮样本;板真相直读保证 hit/miss 确定性)
check(int(am.pierce.ward) == 150 and int(am.pierce.heavy) == 50 and int(am.pierce.light) == 100, "穿刺克制行:魔法护甲 150 / 轻甲 100 / 重甲 50")
check(int(am.magic.heavy) == 150 and int(am.magic.ward) == 50 and int(am.magic.light) == 100, "魔法克制行:重甲 150 / 轻甲 100 / 魔法护甲 50")
check(int(am.normal.light) == 100 and int(am.normal.ward) == 100 and int(am.normal.heavy) == 100, "普通攻击全护甲中性 100(不吃克制)")
# ②′ 充能奖励定义在位(抽象分离批:触发源 → 攻击类型桶;攻击类型定义里不含任何来源)
var grants: Dictionary = _db.settlement.charge_grants
var g_safe: Dictionary = grants.get("safe_cell", {})
var g_flag: Dictionary = grants.get("flag_hit", {})
check(String(g_safe.get("attack_type", "")) == "pierce" and int(g_safe.get("per_unit", 0)) == 1 and int(g_safe.get("cap", -1)) == 8, "充能奖励定义:开格 → 穿刺桶 1/格(单轮计数上限 8)")
check(String(g_flag.get("attack_type", "")) == "magic" and int(g_flag.get("per_unit", 0)) == 2 and int(g_flag.get("cap", -1)) == 0, "充能奖励定义:标中旗 → 魔法桶 2/雷(无计数上限)")
# ②″ 奖励可改(行为断言):把「标中旗」奖励从魔法桶改投穿刺桶 → 同一战斗魔法分量归零;还原后步骤流逐行相同
# (证明改道唯一入口 = 奖励定义,攻击类型与甲效矩阵不参与;玩家可见语义见 战斗结算 GDD 战斗段)
var run_base = _run_flag_backlash_round(20260912)
var base_magic := 0
for row in run_base.rows:
if String(row.get("source_class", "")) == "hero_attack":
base_magic += int(row.get("magic", 0))
var save_grant: Dictionary = _db.settlement.charge_grants["flag_hit"]
_db.settlement.charge_grants["flag_hit"] = {"attack_type": "pierce", "per_unit": 2, "cap": 0}
var run_swap = _run_flag_backlash_round(20260912)
var swap_magic := 0
for row in run_swap.rows:
if String(row.get("source_class", "")) == "hero_attack":
swap_magic += int(row.get("magic", 0))
_db.settlement.charge_grants["flag_hit"] = save_grant
check(base_magic >= 1, "充能落桶基线:标中旗奖励进魔法桶(魔法分量合计 %d)" % base_magic)
check(swap_magic == 0, "改投穿刺桶后:主角攻击行魔法分量归零(×%d——改奖励即改道,攻击类型定义未动)" % swap_magic)
check(JSON.stringify(run_base.rows) == JSON.stringify(_run_flag_backlash_round(20260912).rows), "还原奖励定义后步骤流逐行相同(改道只经奖励定义,矩阵零改动)")
# ③ 标错惩罚 + 标中充能(奖励定义现投魔法桶;Boss 行 150HP 多轮样本;板真相直读保证 hit/miss 确定性)
var run1 = _run_flag_backlash_round(20260912)
var miss_rows := 0
var backlash_rows := 0
var dmg_b_round := -1
var dmg_b_seen := 0
var magic_round := -1
var magic_seen := 0
for row in run1.rows:
if row.get("type", "") == "flag_miss":
miss_rows += 1
if row.get("type", "") == "effect" and String(row.get("source_class", "")) == "flag_miss":
backlash_rows += 1 # 反噬效果行(暴走/伏击/失衡三选一)
if row.get("source_class", "") == "hero_attack" and int(row.get("dmg_b", 0)) >= 1:
dmg_b_round = int(row.get("round_index", -1))
dmg_b_seen = int(row.get("dmg_b", 0))
if row.get("source_class", "") == "hero_attack" and int(row.get("magic", 0)) >= 1:
magic_round = int(row.get("round_index", -1))
magic_seen = int(row.get("magic", 0))
check(miss_rows >= 1, "标错旗产生 flag_miss 行(×%d)" % miss_rows)
check(backlash_rows >= 1, "标错旗触发随机反噬效果行(×%d——敌增伤/敌buff/我方debuff 三选一)" % backlash_rows)
check(dmg_b_round >= 1, "标中雷充能进主角攻击(dmg_b=%d @R%d——每雷实例一次)" % [dmg_b_seen, dmg_b_round])
check(magic_round >= 1, "标中雷充能进主角攻击(magic=%d @R%d——每雷实例一次)" % [magic_seen, magic_round])
# ④ 同种子回放(确定性派生反噬——零新流同种子逐行相同)
var run2 = _run_flag_backlash_round(20260912)
check(JSON.stringify(run1.rows) == JSON.stringify(run2.rows), "标错惩罚轮回放断言:同种子步骤流逐行相同(反噬派生确定性)")