From 131d1047e1c11c8c52f0dd08d2cb72e682504559 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 16 Sep 2026 20:54:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(slice):=20=E6=8A=A4=E7=94=B2=E7=AD=89?= =?UTF-8?q?=E7=BA=A7=E6=8E=B7=E7=82=B9=E6=89=B9=E6=94=B6=E7=BC=96=E2=80=94?= =?UTF-8?q?=E2=80=94d20=20=E5=88=A4=E5=AE=9A=E5=BC=8F+defense=5Fup?= =?UTF-8?q?=E2=86=92ac=5Fup=20=E6=8D=A2=E8=A3=85+armor=5Froll=20=E6=B5=81?= =?UTF-8?q?=EF=BC=88=E5=86=92=E7=83=9F=20233=20=E9=A1=B9=E5=85=A8=E7=BB=BF?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 判定式:1d20 ≥ AC+attack_bonus 全额,低掷三档折扣,nat20 暴击×2,下限 max(1,·) - 换装:defense_up 退役 → ac_up(+1 AC/层×2);hero AC10 / 敌方 0 暂定 - rng_service 增 armor_roll 封闭流;settlement.tres 七掷点参数 - 文档:战斗结算 v0.10/敌人系统 v0.4/难度曲线与Boss v0.5/增益状态 v0.4/地图战斗资源 v0.6 + registry + 06-待办清单 - EV 口径注记:AC12 按判定式 28.75%(提案 32.5% 不符,按规范登记) - 顺手清偿 supply_scan.gd 遗留 .uid --- design/gdd/03-内容规模.md | 2 +- design/gdd/06-待办清单.md | 12 +- design/gdd/systems/冒险循环/难度曲线与Boss.md | 11 +- design/gdd/systems/战斗循环/地图战斗资源.md | 7 +- design/gdd/systems/战斗循环/增益状态.md | 15 +- design/gdd/systems/战斗循环/战斗结算.md | 37 +- design/gdd/systems/战斗循环/敌人系统.md | 15 +- design/registry/entities.yaml | 36 +- production/session-state/active.md | 4 + .../core/battle_orchestrator.gd | 96 ++- .../core/damage_math.gd | 25 + .../core/rng_service.gd | 10 +- .../core/unit_container.gd | 1 + .../data/schemas/enemy_table.gd | 4 + .../data/schemas/hero_table.gd | 3 + .../data/schemas/settlement_table.gd | 39 +- .../data/schemas/status_effect_table.gd | 6 +- .../data/tables/enemy.tres | 22 +- .../data/tables/hero.tres | 4 + .../data/tables/reward.tres | 9 +- .../data/tables/settlement.tres | 18 +- .../data/tables/status_effects.tres | 14 +- .../presentation/hud_presenter.gd | 4 +- .../presentation/main.gd | 2 +- .../presentation/ui_assembler.gd | 7 +- .../presentation/views/battle_log_view.gd | 15 +- .../presentation/views/enemy_card_view.gd | 7 +- .../presentation/views/hero_card_view.gd | 7 +- .../presentation/views/status_slot_row.gd | 9 +- .../smoke/damage_probe.gd | 59 +- .../smoke/defense_scan.gd | 57 +- .../smoke/monte_carlo.gd | 4 +- .../smoke/smoke_all.gd | 224 ++++++- .../smoke/supply_scan.gd.uid | 1 - .../tools/log_analysis.py | 612 ++++++++++++++++++ .../tools/指标口径说明.md | 99 +++ .../tools/样例输出_4200批600局.txt | 37 ++ .../tools/样例输出_种子1234局.txt | 54 ++ .../tools/样例输出_种子5678局.txt | 55 ++ 39 files changed, 1461 insertions(+), 182 deletions(-) delete mode 100644 prototypes/dungeon-sweeper-vertical-slice/smoke/supply_scan.gd.uid create mode 100644 prototypes/dungeon-sweeper-vertical-slice/tools/log_analysis.py create mode 100644 prototypes/dungeon-sweeper-vertical-slice/tools/指标口径说明.md create mode 100644 prototypes/dungeon-sweeper-vertical-slice/tools/样例输出_4200批600局.txt create mode 100644 prototypes/dungeon-sweeper-vertical-slice/tools/样例输出_种子1234局.txt create mode 100644 prototypes/dungeon-sweeper-vertical-slice/tools/样例输出_种子5678局.txt diff --git a/design/gdd/03-内容规模.md b/design/gdd/03-内容规模.md index 097f41a8..49a580f6 100644 --- a/design/gdd/03-内容规模.md +++ b/design/gdd/03-内容规模.md @@ -61,7 +61,7 @@ registry 登记 **六种**(数据表未建,属待落地内容):显影尘 | 状态 | 作用方 | 效果 | |---|---|---| | 增伤 | 主角 | 攻击 +50% | -| 减伤 | 主角 | 承伤 −30% | +| 护甲强化 | 主角 | 护甲等级 +1/层(×2 层)——掷点折扣阈值(2026-09-16 换装,原减伤 −30% 退役) | | 失衡 | 主角 | 承伤 +25% | | 伏击 | 敌方 | 每次攻击附加固定伤害 | | 削弱 | 敌方 | 输出 −65%(标中巢穴的兑付) | diff --git a/design/gdd/06-待办清单.md b/design/gdd/06-待办清单.md index 5c1d03e2..3ae91722 100644 --- a/design/gdd/06-待办清单.md +++ b/design/gdd/06-待办清单.md @@ -142,7 +142,7 @@ **归属**:战斗结算提案,[难度曲线与Boss](systems/冒险循环/难度曲线与Boss.md)数值共裁 -**状态**:待办 +**状态**:已裁决(2026-09-16,数字即规范)——掷 1d20;掷点 ≥ 防守方护甲等级 + 攻击加成(结构位 attack_bonus,切片恒 0)→ 全额,低掷按缺口三档折扣(1-3 → ×0.75 · 4-7 → ×0.5 · 8+ → ×0.25,无零档);nat20 暴击该基础攻击分量 ×2 且恒全额(双方同规);任何攻击落地伤害 ≥1(全局 max(1,·),零值伤害行语义退役);armor_class 域 [0,999],主角 10 / 敌方首关 0(暂定)。判定式全 [#战斗结算 v0.10](systems/战斗循环/战斗结算.md) 与 registry settlement_table 条目,切片已落地(smoke 233 项全绿 + level_monte_carlo 复跑验收) #### 护甲等级掷点的随机流归属 @@ -150,7 +150,7 @@ **归属**:[数据配置](systems/表现与支撑/数据配置.md)与战斗结算共裁,随定稿批执行 -**状态**:待办 +**状态**:已裁决(2026-09-16)——新开 armor_roll 封闭流(互异奇盐 0xC2B2AE3D27D4EB2F),scope_event_binding = 每次掷点恰一 scope(逐次掷逐次取号、可逐位回放);黄金向量 m=42 scope0 = 4806095330042243845;rng_service 条目与切片已同步落地 ### 敌人系统 @@ -208,6 +208,14 @@ **归属**:随掷点形式定稿批,投值归[难度曲线与Boss](systems/冒险循环/难度曲线与Boss.md) +**状态**:已裁决(2026-09-16)——域 [0,999];主角 10(暂定)、敌方首关六行 0(暂定)已入表。正式行值拆出为新待办:「敌方护甲等级(armor_class)正式行值」(下方) + +#### 敌方护甲等级(armor_class)正式行值 + +**现状**:armor_class 字段与掷点判定已定稿落地(2026-09-16 护甲等级掷点批);第一关怪物 AC 相对较低(暂定全 0 = 敌方攻击恒全额),正式行值待怪物 / 敌人系统设定批投值——三敌种各自的护甲等级档位(含 Boss)随投值批定,投值后须复算生存链区间(含暴击档与折扣档带宽)并同步 registry。关联条目:难度曲线与Boss「基线通关率越带,加难杠杆扫描未执行」(AC 行值投值与加难扫描合并调) + +**归属**:怪物/敌人系统设定批投值,[难度曲线与Boss](systems/冒险循环/难度曲线与Boss.md)联动复算 + **状态**:待办 ### 增益状态 diff --git a/design/gdd/systems/冒险循环/难度曲线与Boss.md b/design/gdd/systems/冒险循环/难度曲线与Boss.md index 7e8ba4a8..1690aacc 100644 --- a/design/gdd/systems/冒险循环/难度曲线与Boss.md +++ b/design/gdd/systems/冒险循环/难度曲线与Boss.md @@ -95,7 +95,7 @@ MVP 行值并不逐式现算,而是按此式量级直填经切片验证过的 | 输出峰值 / 谷值 | 玩家最好 / 最坏一轮的总输出(分量构成同上) | | 一次性伤害 | 清障炸药一类当场引爆的大额伤害 | -护甲等级带宽口径(2026-09-15 护甲等级批):结算换算层引入护甲等级掷点(必中、低掷按档位折扣,规则归[战斗结算](../战斗循环/战斗结算.md))后,受伤与输出两侧的峰值 / 谷值构成各含一条掷点带宽——同一刀的最坏 / 最好掷点落进谷值 / 峰值。掷点按 EV 中性引入:期望伤害对齐现行表值,现行行值不重调;折扣档位与掷点形式定稿前,区间扫描维持"全额无折扣"口径,带宽项随定稿批补扫。"零红档"验收与三档判定式不变。 +护甲等级带宽口径(2026-09-16 定稿落地):结算换算层引入护甲等级掷点(1d20 必中、低掷按缺口三档折扣、nat20 暴击 ×2 恒全额,规则归[战斗结算](../战斗循环/战斗结算.md))后,受伤与输出两侧的峰值 / 谷值构成各含一条掷点带宽——同一刀的最坏 / 最好掷点(含暴击档与折扣档)落进谷值 / 峰值,带宽补扫义务含暴击档与折扣档。**换装点锚定**:基准 AC10 自带约 −21% 敌方输出期望(AC11 25% / AC12 28.75%),对比被整体退役的 defense_up 满层 −60% 减伤墙是结构性削弱,且伤害下限(任何攻击落地 ≥1)使"零承伤轮"结构性消失——该方向性偏移与下方已登记的加难杠杆扫描合并处理,不单独重调行值;主角行值 10(暂定)、敌方首关 0(暂定,正式行值归怪物设定批)。"零红档"验收与三档判定式不变。 关级通关恒等式:**一关打下来,玩家总输出 ≈ 全关敌方总生命期望 ×(1 + 缓冲);其中奖励侧贡献的输出占比须落在 [15%, 40%] 带内。** @@ -161,7 +161,7 @@ Boss 战的棋盘用本关最大板形(第一关为 12×12),板参数仍 | 每轮行动点 ↔ 停手转存规模 | [行动经济](../排雷循环/行动经济.md) | 联动评估记录归本文,调参所有权在行动经济 | | 削弱强度 ↔ 当量锚 | [增益状态](../战斗循环/增益状态.md)、[战斗结算](../战斗循环/战斗结算.md) | 联动值禁独立调参,修订须重跑锚对照 | | 护甲类型 ↔ 充能奖励(投桶与量) ↔ 克制矩阵 | [战斗结算](../战斗循环/战斗结算.md) | 敌种护甲变更须复算击杀区间;改充能的投桶或量属奖励侧改动,须重跑本表数值 | -| 护甲等级 ↔ 攻击基线 ↔ 区间带宽 | [战斗结算](../战斗循环/战斗结算.md) | 敌种与主角的护甲等级行值变更须复算生存链区间(掷点带宽随折扣档位与攻击基线同向放大);EV 中性锚定现行表值,掷点形式与档位定稿前禁投值 | +| 护甲等级 ↔ 攻击基线 ↔ 区间带宽 | [战斗结算](../战斗循环/战斗结算.md) | 敌种与主角的护甲等级行值变更须复算生存链区间(掷点带宽随折扣档位与攻击基线同向放大);掷点形式与档位已定稿(d20 三档 / nat20 ×2),护甲等级行值变更仍须复算生存链区间并同步 registry——主角 10 / 敌方首关 0 均暂定,敌方正式行值随怪物设定批投值、与加难杠杆扫描合并调 | 五条经济契约的对账由[战斗结算](../战斗循环/战斗结算.md)闭合验收,其中挖掘风险不等式与清图支配性两条的现行验算已破线或存在缺口(现状见其「待办」),重跑义务落在本系统的下一次调参批执行——这是本系统当前最优先的调参义务。完美排雷乘数的调参权在本系统。 @@ -199,7 +199,7 @@ Boss 战的棋盘用本关最大板形(第一关为 12×12),板参数仍 | 优先级 | 内容 | 后置原因 | |---|---|---| | P1 | 第二关深度轴行值(敌人表与棋盘表 D1–D4) | 内容与数值同批,随第二关内容扩展填充;填齐前冒险按单关运行(合法态) | -| P1 | 加难杠杆扫描落带 | 候选杠杆已登记(血量锚走廊、群战上探、注入衰减 0.6),扫描未执行;落带前现行行值维持 provisional-pending-scan | +| P1 | 加难杠杆扫描落带 | 候选杠杆已登记(血量锚走廊、群战上探、注入衰减 0.6),扫描未执行;护甲等级换装点锚定(基准 AC10 自带约 −21% 敌方输出期望 + 减伤墙退役 + 伤害下限)的方向性偏移并入本扫描一并调带(带宽补扫含暴击档与折扣档);落带前现行行值维持 provisional-pending-scan | | P1 | 挖掘风险不等式与清图支配性对账重跑 | 两条对账现行破线 / 缺口,义务落在本文调参批(现状见[战斗结算](../战斗循环/战斗结算.md)「待办」) | | P1 | 真人通关率修正系数标定 | 带按模拟口径定义;试玩批次实测真人显著强于模拟者,系数待后续试玩批次定量 | | P1 | 完美策略档的结构性加难杠杆 | 数值杠杆对完美档已被实测证伪,结构候选未裁决(见「待办」) | @@ -230,7 +230,7 @@ Boss 战的棋盘用本关最大板形(第一关为 12×12),板参数仍 | 表 | 行值 | |---|---| -| 主角表 | 生命上限 50 · 基础攻击 6 | +| 主角表 | 生命上限 50 · 基础攻击 6 · 护甲等级 10(暂定) | | 棋盘参数(第一关 D0/D1) | 10×10 · 雷密度 0.25 · 可推理率目标 0.88 · 生成尝试上限 16 | | 棋盘参数(第一关 D2–D4,含 Boss) | 12×12 · 雷密度 0.25 · 可推理率目标 0.88 · 生成尝试上限 24 | @@ -246,7 +246,7 @@ Boss 战的棋盘用本关最大板形(第一关为 12×12),板参数仍 | 当量参考敞口 | 12 | 变更触发全量对账重跑 | | 注入概率初值 / 衰减 | 0.4 / 0.5(加难候选 0.6) | 注入机制归[地图战斗资源](../战斗循环/地图战斗资源.md) | | 清障炸药威力区间 | 25–55(中点 40) | 清图槽当场引爆 | -| 状态强度 | 增伤 +50 / 减伤 −30 / 削弱 −65 / 伏击 +3(每层) | 联动评估归本文,禁独立调参 | +| 状态强度 | 增伤 +50 / 护甲强化 +1 护甲等级(×2 层,掷点轴——2026-09-16 换装,原减伤 −30 退役) / 削弱 −65 / 伏击 +3(每层) | 联动评估归本文,禁独立调参 | ## 验收标准 @@ -273,3 +273,4 @@ Boss 战的棋盘用本关最大板形(第一关为 12×12),板参数仍 | v0.2 | 2026-09-15 | 教学位掷骰自洽(用户拍板:代码不动、文档对齐):删除"教学位/首遇位固定不掷"豁免口径,教学位地精数量明确为开战掷骰区间 [1,2];D0 行数量列改 [1,2];Boss 血量锚 Σ=153.5 补逐数验算(区间期望口径下闭合) | | v0.3 | 2026-09-15 | 雾格残留清理:关闭「待拍板」的雾相关供给验算失义条目(原供给与对账验算含雾板分层、雾成本负担比等输入分量);第二关新内容投放缺口条目改为引用「已从设计中移除的机制」(见不做清单)。曲线规则、供给带与对账义务不变 | | v0.4 | 2026-09-15 | **护甲等级批(与战斗结算 / 敌人系统 / registry 同批登记)**:生存链区间模型补记护甲等级掷点带宽口径——峰值/谷值构成各含最坏/最好掷点带宽;EV 中性引入成文(期望伤害对齐现行表值、现行行值不重调,折扣档位与掷点形式定稿前区间扫描维持全额口径、带宽项随定稿批补扫,"零红档"验收与三档判定式不变);调参联动表新增"护甲等级 ↔ 攻击基线 ↔ 区间带宽"一行 | +| v0.5 | 2026-09-16 | **护甲等级掷点批·定稿落地(与战斗结算 / 敌人系统 / registry / 切片代码同批)**:带宽口径改写为"换装点锚定"——掷点形式与档位定稿(d20 / 缺口 1-3→×0.75 · 4-7→×0.5 · 8+→×0.25 / nat20 暴击 ×2 恒全额),基准 AC10 自带约 −21% 敌方输出期望(AC11 25% / AC12 28.75%),对比退役的 defense_up 满层 −60% 减伤墙为结构性削弱 + 伤害下限使零承伤轮结构性消失,方向性偏移与加难杠杆扫描合并处理、不单独重调行值;带宽补扫义务含暴击档与折扣档(并入加难扫描批);护甲等级行值:主角 10(暂定)、敌方首关 0(暂定,正式行值归怪物设定批);调参联动表"护甲等级"行改定稿口径;调参表主角表行与状态强度行同步(减伤 −30 → 护甲强化 +1 AC ×2 层) | diff --git a/design/gdd/systems/战斗循环/地图战斗资源.md b/design/gdd/systems/战斗循环/地图战斗资源.md index e5237b3b..79746725 100644 --- a/design/gdd/systems/战斗循环/地图战斗资源.md +++ b/design/gdd/systems/战斗循环/地图战斗资源.md @@ -88,12 +88,12 @@ | 类型 | 正向(奖励) | 负向(惩罚) | |---|---|---| | attack 增伤 | attack_up 增伤 +50%(hero_offense 池) | enrage 敌方暴走 +25%(enemy_output 池) | -| defense 减伤 | defense_up 减伤 −30%(hero_defense 池) | off_guard 我方失衡 +25%(hero_defense 池,承伤加深) | +| defense 护甲 | ac_up 护甲强化 +1 护甲等级/层 ×2 层(AC 掷点阈值轴——2026-09-16 换装,不入池) | off_guard 我方失衡 +25%(hero_defense 池,承伤加深) | | heal 回血 | heal_minor 回血 2–6(满血零兑付照旧) | 直伤:主角 −4(战斗结算直接扣血,不经增益状态) | | blast 群伤 | spark_aoe 全体存活敌各 −4 | ambush 敌方伏击 +2 层(进伏击池) | | weaken 削弱 | weaken 敌方输出 −65%(enemy_output 池) | sapped 我方乏力 −25%(hero_offense 池) | -设计意图:读敌拍板的路线规划——敌方要放大招时抢减伤、怪重叠时抢削弱,惩罚侧则镜像同一批类型。类型清单是封闭的:加第六型等于修订本文档,不是调参数。 +设计意图:读敌拍板的路线规划——敌方要放大招时抢护甲强化、怪重叠时抢削弱,惩罚侧则镜像同一批类型。类型清单是封闭的:加第六型等于修订本文档,不是调参数。 凡"该兑付但没生效"——增益挂满层、满血捡到 heal 正向、场上无敌可打——一律在结算明细里明写一行"奖励未生效",不给折算补偿,也不静默消失。把兑付改成跳过轮末结算直接生效,同样在禁止之列。 @@ -171,7 +171,7 @@ | 行 | 类型 | 极性 | 威力 | 权重 | 档位 | 所在池 | |---|---|---|---|---|---|---| | attack_up | attack | 正向 | —(不消费) | 20 | 2 | 格面池 | -| defense_up | defense | 正向 | —(不消费) | 16 | 2 | 格面池 | +| ac_up | defense | 正向 | —(不消费) | 16 | 2 | 格面池 | | spark_aoe | blast | 正向 | 4 | 16 | 1 | 格面池 | | heal_minor | heal | 正向 | 2–6 | 15 | 1 | 格面池 | | weaken | weaken | 正向 | —(不消费) | 12 | 1 | 格面池 | @@ -209,3 +209,4 @@ | v0.3 | 2026-09-15 | 雾格残留清理:关闭「待拍板」的雾格相关定价约束三条失义条目与随之取消的奖励配置表档位过滤门限 t_fog_min | | v0.4 | 2026-09-15 | **格面资源批(用户拍板,2026-09-15)**:① 核心抽象改为格面资源(CellResource)——安全格与雷格一视同仁、单一类型池装板期摇定,极性由「动作 × 格型」矩阵决定(开安全格/标中雷 = 正向,开雷格/标错安全格 = 负向),取代踩雷固定伏击、标中固定削弱、标错三选一哈希反噬三套固定兑付;② 五型字典(attack/defense/heal/blast/weaken,正负各一行;weaken 负向 sapped 为新行,heal 负向为不经增益状态的直伤);③ 封锁格(初始展开格)恒无资源;④ 清图奖励改述为板级、可配置可替换的清图奖励对象,图类型差异化插槽保留;⑤ **可选类道具(刀/治疗包)整体删除**——供给控制器、注入概率、按板保底、关卡域库存、道具使用窗口的数据面全部下线,RewardConfigTable 随批废止;功能道具经其他渠道加入的结构位登记 P1;⑥ 行动经济对账锚与难度曲线重推义务随批登记(见 registry 修订记录) | | v0.5 | 2026-09-16 | 更名批(用户拍板 2026-09-16):系统更名"奖励道具经济"→"地图战斗资源"——定义双向(正负由动作×格型决定)、格面承载、只在战斗结算生效;文件同步重命名;「待拍板」系统更名条目关闭;英文代号 REWARD_ITEM_ECONOMY 暂沿用 | +| v0.6 | 2026-09-16 | **护甲等级掷点批·换装(与战斗结算 / 增益状态 / registry / 切片同批)**:defense 型正向行 defense_up(减伤 −30%,hero_defense 池)换装为 ac_up(护甲强化 +1 护甲等级/层 ×2 层,显示名暂定"护甲强化")——百分比减伤行无双轨退役;cell_type defense / 权重 16 / 档位 2 零改动(类型频次合计 79 不变);五型字典表与 mvp 行表同步 | diff --git a/design/gdd/systems/战斗循环/增益状态.md b/design/gdd/systems/战斗循环/增益状态.md index 67f05834..dc08145d 100644 --- a/design/gdd/systems/战斗循环/增益状态.md +++ b/design/gdd/systems/战斗循环/增益状态.md @@ -128,8 +128,8 @@ | 例子 | 容器输出 | 消费侧读法 | |---|---|---| -| 削弱叠两层 | −130 个百分点(越出单行取值域,原样输出) | 压在地板 −100% 上:敌方攻击当轮归零 | -| 主角减伤叠两层 | −60 个百分点 | 不触地板:承伤读作 0.4 倍 | +| 削弱叠两层 | −130 个百分点(越出单行取值域,原样输出) | 压在地板 −100% 上:敌方输出乘区归零(落地伤害由战斗结算全局下限保底 ≥1) | +| 主角护甲强化叠两层 | +2 个护甲等级 | 不经任何池:掷点判定阈值 AC10 → AC12(期望减免 21.25% → 28.75%) | 容器还有一条快照纪律:实例的幅度、初始次数、量纲在授予那一刻从表行拷贝。战斗中配置表即使被重载,已授予实例不受任何影响,只影响后续授予。 @@ -142,8 +142,8 @@ MVP 阶段状态表恰有六行:两个主角增益(挖安全格拿)、三 | effect_id | 文本,全表唯一 | 状态标识 | | display_name | 文本 | 界面与结算明细用名 | | target_side | hero / enemy | 作用侧 | -| magnitude | 整数 | 幅度:百分比行 ∈ [−100,100] 且非 0;固定值行 ≥ 1 | -| unit | percent / flat | 百分比修正 / 固定值 | +| magnitude | 整数 | 幅度:百分比行 ∈ [−100,100] 且非 0;固定值行 ≥ 1;护甲等级行 ∈ [−20,20] 且非 0 | +| unit | percent / flat / ac | 百分比修正 / 固定值 / 护甲等级(掷点折扣判定阈值轴——2026-09-16 护甲等级掷点批新增) | | duration_settlements | 整数 [1,99] | 授予时初始剩余结算次数(1 = 一次性) | | stack_policy | stack / refresh / independent | 同名再授予的合并策略 | | max_stack | 整数 [1,16] | 叠层上限,仅叠加策略消费 | @@ -151,7 +151,7 @@ MVP 阶段状态表恰有六行:两个主角增益(挖安全格拿)、三 | 状态 | 效果一句话 | 发给谁 | 幅度(现行值) | 叠层上限 | 来源类别 | |---|---|---|---|---|---| | attack_up 增伤 | 主角攻击提升 | 主角 | +50%(provisional) | 2 | 安全格 | -| defense_up 减伤 | 主角承伤降低 | 主角 | −30%(provisional) | 2 | 安全格 | +| ac_up 护甲强化 | 主角护甲等级提升(掷点折扣判定阈值——AC 轴,不入池) | 主角 | +1 护甲等级/层(2026-09-16 换装,原 defense_up 减伤 −30% 退役) | 2 | 安全格 | | ambush 伏击 | 敌方攻击附带额外伤害 | 敌方 | +3 固定值/层(provisional) | 16 | 踩雷 / 标空旗反噬 | | weaken 削弱 | 敌方攻击被削弱 | 敌方 | −65%(provisional,取值域 [60,85]) | 16 | 标中旗 | | enrage 暴走 | 敌方攻击提升 | 敌方 | +25%(provisional) | 16 | 标空旗反噬 | @@ -206,7 +206,7 @@ MVP 阶段状态表恰有六行:两个主角增益(挖安全格拿)、三 | 参数 | 取值域 | 现行值 | 说明 | |---|---|---|---| | attack_up 幅度 | 百分比 [−100,100] 且非 0 | +50(provisional) | 增伤;当量锚的参照对象(最低档主角增益) | -| defense_up 幅度 | 百分比 [−100,100] 且非 0 | −30(provisional) | 减伤;叠满两层 −60 个百分点 | +| ac_up 幅度 | 护甲等级 [−20,20] 且非 0 | +1/层(2026-09-16 换装定稿,暂定) | 护甲强化;掷点折扣判定阈值(AC10 → 11 → 12),不入任何池、消费归[战斗结算](战斗结算.md)掷点判定 | | ambush 幅度 | 固定值 ≥ 1 | +3(provisional) | 伏击;量纲定义见[敌人系统](敌人系统.md) | | weaken 幅度 | 百分比取值域 [60,85],取负生效 | −65(provisional) | 削弱;须过"标中旗锚"对照 | | enrage 幅度 | 百分比 [−100,100] 且非 0 | +25(provisional) | 暴走;标错反噬三池之一,进敌方输出修正池 | @@ -216,7 +216,7 @@ MVP 阶段状态表恰有六行:两个主角增益(挖安全格拿)、三 非调参的冻结项:三类合并策略语义、两种量纲语义、来源类别枚举、"先兑付后衰减"时序、拒绝原子性、零随机——改动走本文修订流程,不走调参。 -联动纪律:增伤与削弱是当量锚的两端;减伤与[难度曲线与Boss](../冒险循环/难度曲线与Boss.md)的承伤预期联动;削弱的单轮层数上限受[行动经济](../排雷循环/行动经济.md)的每轮行动点约束。这些联动值禁止独立调参,修订须重跑锚对照并同步 registry。 +联动纪律:增伤与削弱是当量锚的两端;护甲强化与[难度曲线与Boss](../冒险循环/难度曲线与Boss.md)的承伤预期联动(掷点折扣轴——AC 行值变更须复算生存链区间);削弱的单轮层数上限受[行动经济](../排雷循环/行动经济.md)的每轮行动点约束。这些联动值禁止独立调参,修订须重跑锚对照并同步 registry。 ## 验收标准 @@ -243,3 +243,4 @@ MVP 阶段状态表恰有六行:两个主角增益(挖安全格拿)、三 | v0.1 | 2026-09-15 | 自归档 GDD 压缩改写 | | v0.2 | 2026-09-15 | 状态表四行改六行:补暴走(enrage,敌方 +25%)与失衡(off_guard,主角承伤 +25%)两行,承载[战斗结算](战斗结算.md)标错反噬机制(2026-09-12 批已上线);来源类别四选一改五选一(新增标空旗);连带更新"MVP 四行/四类"表述与调参表 | | v0.3 | 2026-09-15 | 并存分支补容量语义(实例数上界 = 受理授予数,有界确定、无满层语义);P2 后置登记并存行高频授予(持续伤害类)的容量复核 | +| v0.4 | 2026-09-16 | **护甲等级掷点批·换装(与战斗结算 / 难度曲线与Boss / registry / 切片同批)**:defense_up(减伤 −30% ×2 层)整行退役换装 ac_up(护甲强化 +1 护甲等级 ×2 层)——unit 新增 ac 量纲(护甲等级阈值轴,幅度域 [−20,20] 且非 0,不入任何池),消费面 = 战斗结算护甲等级掷点判定;例子表与调参表同步;"叠满减伤触池底"的免伤墙随换装结构性消失。注:sapped 行与极性路由措辞的文档同步归增益状态既有修订批(本批不扩 scope) | diff --git a/design/gdd/systems/战斗循环/战斗结算.md b/design/gdd/systems/战斗循环/战斗结算.md index ba368fc1..5573b659 100644 --- a/design/gdd/systems/战斗循环/战斗结算.md +++ b/design/gdd/systems/战斗循环/战斗结算.md @@ -65,7 +65,7 @@ | ④ 衰减段 | 移除到期的临时效果;每轮恰好一次,含末轮——清图奖励先兑付、效果后衰减 | | ⑤ 判定段 | 先判主角倒下(负),再判敌人清空(胜),都不满足则继续下一轮 | -结算本体是纯逻辑步骤流:零界面依赖、禁止动画等待,随机消费只走封闭随机流。触发只读行动经济的相位读数与轮末标记,这条消息有棋盘路径和停手路径两个发射源,到达先后不承载语义。这些性质共同保证结算可回放:同样的种子、同样的事件序列,跑出的步骤流逐位复现——录像回放与断言复测都建立在它上面。随机消费恰两处:战斗开始掷骰定敌方实际数量与攻击基线浮动,属于初始化随机、发生在结算步骤流之外;护甲等级掷点(2026-09-15 护甲等级批引入)属于回合内随机、发生在步骤流之内——两处都消费封闭随机流,掷点流归属见「待办」。 +结算本体是纯逻辑步骤流:零界面依赖、禁止动画等待,随机消费只走封闭随机流。触发只读行动经济的相位读数与轮末标记,这条消息有棋盘路径和停手路径两个发射源,到达先后不承载语义。这些性质共同保证结算可回放:同样的种子、同样的事件序列,跑出的步骤流逐位复现——录像回放与断言复测都建立在它上面。随机消费恰两处:战斗开始掷骰定敌方实际数量与攻击基线浮动,属于初始化随机、发生在结算步骤流之外;护甲等级掷点属于回合内随机、发生在步骤流之内——两处都消费封闭随机流,掷点消费专用的 `armor_roll` 封闭流(逐次掷逐次取号,可逐位回放)。 举证:五段为流程编排,无计算;段序常量存配置表,加载时校验与定稿顺序完全一致,不一致拒绝启动。 @@ -76,7 +76,7 @@ | 行型 | 什么时候写 | |---|---| | 效果行 | 增益/减益段每条效果一行:本次授予贡献值与池合成值分列,防误读 | -| 伤害行 | 每次伤害一行:目标、伤害分量、修正终值、击杀标记;吃护甲等级掷点的分量,掷点读数与折扣档位随行留档(行型封闭清单不变) | +| 伤害行 | 每次伤害一行:目标、伤害分量、修正终值、击杀标记;吃护甲等级掷点的分量,掷点读数与折扣档位随行留档,nat20 暴击随行标记并金框高亮(行型封闭清单不变) | | 治疗行 | 治疗生效一行:封顶后的真实回复量 | | 溢出未生效行 | 踩雷超出伏击上限,超出的每颗一行 | | 奖励未生效行 | 满层拒收、满血治疗等合法例外一行 | @@ -174,25 +174,27 @@ percent 类(每结算、每层): E = |层数 × 行值| ÷ 100 × 两条都是奖励侧现行定义(暂存结算表 `charge_grants`,正式归属见「待办」):换桶、换量、或把开格奖励换成增益与道具,都不触动攻击类型与甲效矩阵。 -**护甲等级掷点:必中、非归零,只吃基础攻击分量**(2026-09-15 拍板新增)。防守方由此多了一条数值轴——护甲等级(数据键 `armor_class`,provisional):攻击永远命中,没有 miss、没有零收益白回合;只有吃掷点的分量在掷点低于防守方护甲等级时按减伤档位打折,档位再重也不归零。掷点只吃基础攻击分量:主角先手攻击的普通分量,敌方攻击拍与重击拍的基线伤害(基线 × 重击倍率);穿刺与魔法两个充能分量不吃。棋盘来源一律无视护甲等级——伏击附加、道具伤害、清障炸药、奖励即时伤害全部必中全额,这是本机制的边界切口,与"奖励/惩罚必中"同构。护甲等级(数值轴)与护甲类型(轻甲 / 魔法护甲 / 重甲的克制属性轴)是两个概念,文档、表字段与代码键不得混用。 +**护甲等级掷点:必中、非归零,只吃基础攻击分量**(2026-09-16 定稿落地,判定式与全部数值已拍板)。防守方由此多了一条数值轴——护甲等级(数据键 `armor_class`):攻击永远命中,没有 miss、没有零收益白回合;只有吃掷点的分量在掷点低于防守方护甲等级时按减伤档位打折,档位再重也不归零。判定式:攻击结算时掷 1d20,掷点 ≥ 防守方护甲等级 + 攻击加成 → 全额;掷点 < 护甲等级按缺口(护甲等级 − 掷点)打折——缺口 1-3 → ×0.75,缺口 4-7 → ×0.5,缺口 8+ → ×0.25,三档皆非零档。暴击:天然 20(nat20)使该次基础攻击分量 ×2,双方同规,且 nat20 恒全额(暴击与折扣互斥)。伤害下限:任何攻击落地伤害至少 1(全局 max(1, ·))——"被减伤归零 v=0"零值伤害行语义随批退役,重档折扣的攻击行在行文本记"被压制"。掷点范围:只有基础攻击分量吃掷点——主角先手攻击的普通分量,敌方攻击拍与重击拍的基线伤害(基线 × 重击倍率);穿刺与魔法两个充能分量不吃;棋盘来源一律无视护甲等级——伏击附加、爆裂、清障炸药、奖励即时伤害全部必中全额不掷,这是本机制的边界切口,与"奖励/惩罚必中"同构。护甲等级(数值轴,掷点打折)与护甲类型(属性轴,克制乘区)严格分离:护甲类型不做护甲等级映射,克制矩阵 150/100/50 消费面不变,两概念在文档、表字段与代码键中不得混用。数值载体:主角基准护甲等级 10(暂定),ac_up(护甲强化)+1/层 ×2 层在其上加算;敌方首关行值暂定 0,正式行值归怪物设定批(登记[待办清单](../../06-待办清单.md))。攻击加成(`attack_bonus`)为结构位预留字段,切片恒 0,未来归元进度 / 武器附魔投值。掷点时机在结算战斗段内、每次基础攻击伤害计算时判定,消费 `armor_roll` 封闭流(逐次掷逐次取号,可逐位回放)。 -掷点折扣按 EV 中性引入:期望伤害对齐现行表值,现行数值不重调;[难度曲线与Boss](../冒险循环/难度曲线与Boss.md)的生存链区间模型按掷点带宽口径复核,"零红档"验收保留。掷点形式与数值域未定,见「待办」。 +EV 参考数(按判定式复算,作对照锚写进文档):基准 AC10 期望减免 21.25%,AC11 25%,AC12 28.75%;对比旧 defense_up 满层 −60% 的减伤墙已整体退役换装为 AC(无双轨)。微调旋钮 = 档位边界与档位值,不动护甲等级值。换装点锚定:基准 AC10 自带约 −21% 敌方输出期望,该方向性偏移与[难度曲线与Boss](../冒险循环/难度曲线与Boss.md)已登记的加难杠杆扫描合并处理;其生存链区间模型按掷点带宽口径复核(带宽含暴击档与折扣档),"零红档"验收保留。 -**结论**:先合成、再取整、全程非负;吃掷点分量必中非归零、低掷按档位折扣;溢杀丢弃、治疗封顶、死亡即时判定。 +**结论**:先合成、再取整、全程非负;吃掷点分量必中非归零、低掷按档位折扣、nat20 暴击翻倍恒全额、落地伤害保底 ≥1;溢杀丢弃、治疗封顶、死亡即时判定。 ``` -敌方一次攻击 = floor( 裸伤害 × 主角防御乘数 × 敌方输出乘数 );伏击为后置加法,不进乘区 +敌方一次攻击 = max(1, floor( 掷点档位基线 + 伏击附加 ) × 主角防御乘数 × 敌方输出乘数 );伏击为后置加法,不进乘区、不掷不折 乘数 = 1 + 池合成值 ÷ 100;池内加算、每池独立封顶 ±100 百分点 → 乘数域 [0, 2] -护甲等级掷点:掷点 ≥ 防守方护甲等级 → 全额;掷点 < 护甲等级 → 吃掷点分量 × 档位折扣(取整沿全局向下取整) +护甲等级掷点:掷 1d20,掷点 ≥ 护甲等级 + 攻击加成 → 全额;低掷按缺口查档——1-3 → ×0.75 · 4-7 → ×0.5 · 8+ → ×0.25(无零档) +暴击:nat20 → 该次基础攻击分量 ×2 且恒全额(双方同规) +伤害下限:任何攻击落地伤害 ≥ 1(全局 max(1, ·)——折扣与池归零都不产生 v=0 伤害行) 吃掷点分量 = 主角普通分量 / 敌方攻击拍与重击拍基线;穿刺、魔法、伏击附加与棋盘来源伤害不掷不折 治疗量 = min( 当前生命 + 表值, 生命上限 ) ``` | 边界 | 规则 | |---|---| -| 修正池封顶 | 同侧百分比修正加算后每池独立收口 ±100 百分点;乘数可为 0——敌人输出归零、主角本结算免伤,是合法设计态而非异常 | -| 零值伤害 | 合成后为 0 时不调扣血接口,改写零值伤害行——免伤与零输出必须可见;折扣取整后为 0 走同一条既有路,不新增行型 | -| 护甲等级掷点 | 必中非归零:掷点低于防守方护甲等级按档位折扣、档位不设零档;只吃基础攻击分量(主角普通分量、敌方攻击/重击拍基线),棋盘来源伤害不掷不折 | +| 修正池封顶 | 同侧百分比修正加算后每池独立收口 ±100 百分点;乘数可为 0——输出压制触池底时落地伤害由全局下限保底 ≥1(叠满免伤归零的结构面已随护甲等级换装消失) | +| 伤害下限 | 任何攻击落地伤害至少 1(全局 max(1, ·));"被减伤归零 v=0"零值伤害行语义退役,重档折扣行文本记"被压制"——免伤与零输出仍逐行可见 | +| 护甲等级掷点 | 必中非归零:掷 1d20 对防守方护甲等级 + 攻击加成(恒 0),低掷按缺口三档折扣、档位不设零档;nat20 暴击 ×2 恒全额;只吃基础攻击分量(主角普通分量、敌方攻击/重击拍基线),棋盘来源伤害不掷不折 | | 溢杀 | 一刀超出目标剩余生命的部分丢弃:不转移给下一个敌人、不存续、无负生命中间态 | | 治疗 | 生命不溢出上限;满血正向治疗零兑付,记"奖励未生效"行 | | 死亡判定 | 生命归 0 即倒下;敌人受击即死;主角倒下战斗段立即短路;判定段先判主角、再判敌人(防御性次序) | @@ -280,9 +282,11 @@ percent 类(每结算、每层): E = |层数 × 行值| ÷ 100 × | 完美乘数 | percent {100, 200} | 200(provisional) | 100 为仅高光、无第二份;域外加载拒绝 | | 充能奖励定义 `charge_grants` | Dictionary:触发源 → {attack_type, per_unit, cap} | 开格 → 穿刺 1/格(计数上限 8)· 标中旗 → 魔法 2/雷(无上限)(provisional) | 奖励侧定义:附加伤害储备按攻击类型分桶,本字典决定哪个源投哪个桶、每单位几点、单轮计数上限;整表替换不改攻击类型与甲效矩阵(正式归属见「待办」) | | 甲效矩阵 | percent | 克制 150 / 中性 100 / 被克 50(provisional) | 键集 = 攻击类型 {普通, 穿刺, 魔法} × 护甲类型 {轻甲, 魔法护甲, 重甲},3×3 满格结构冻结;数值归调参 | -| 护甲等级掷点 | 形式未定:d20 / 百面骰 / 区间均匀 | 未定(provisional,见「待办」) | 回合内随机:每吃掷点分量逐次掷骰,掷点低于防守方护甲等级触发档位折扣;流归属未定(见「待办」) | -| 减伤档位表 | 掷点差距档 → 折扣百分比,档数未定 | 未定(provisional) | 分级折扣载体;档位不设零档(非归零);EV 中性——期望伤害对齐现行表值,现行数值不重调 | -| 护甲等级 `armor_class`(防守方字段) | 未定(随掷点形式定稿) | 未定(provisional) | 字段本体在敌人表与主角表(归[敌人系统](敌人系统.md)),行值投值归[难度曲线与Boss](../冒险循环/难度曲线与Boss.md);与护甲类型(克制属性轴)两概念不得混用 | +| 护甲等级掷点 | 骰面 int [2,1000] · nat 最大面 [1,骰面] · 暴击倍率 percent [100,999] | d20 · nat20 · ×200(2026-09-16 拍板定稿) | 回合内随机:每吃掷点分量逐次掷骰,掷点低于防守方护甲等级 + 攻击加成触发档位折扣;nat20 暴击 ×2 且恒全额(双方同规);消费 `armor_roll` 封闭流(逐次掷逐次取号) | +| 减伤档位表 | 边界升序 + 档位值降序 percent [1,99],档数 = 边界数 + 1 | 边界 [3, 7] · 档位 [75, 50, 25](2026-09-16 拍板定稿) | 缺口 1-3 → ×0.75 · 4-7 → ×0.5 · 8+ → ×0.25;档位不设零档(非归零);EV 参考数:AC10 期望减免 21.25% / AC11 25% / AC12 28.75%;微调旋钮 = 本表,不动护甲等级值 | +| 伤害下限 `damage_floor` | int [1, 999] | 1(2026-09-16 拍板定稿) | 任何攻击落地伤害全局 max(1, ·);零值伤害行语义随批退役 | +| 攻击加成 `attack_bonus` | int [0, 999] | 0(结构位) | 掷点阈值 = 防守方护甲等级 + 攻击加成;切片恒 0,未来归元进度 / 武器附魔投值 | +| 护甲等级 `armor_class`(防守方字段) | int [0, 999] | 主角 10(暂定)· 敌方首关 0(暂定) | 字段本体在敌人表与主角表(归[敌人系统](敌人系统.md)),行值投值归[难度曲线与Boss](../冒险循环/难度曲线与Boss.md);敌方正式行值归怪物设定批(见待办清单);与护甲类型(克制属性轴)两概念不得混用 | | 段序常量 | 固定五段 | 结构冻结 | 加载期恒等校验,违例拒绝启动 | | 池封顶 | ±100 百分点 | 结构冻结 | 乘数域随之 [0, 2] | | 取整方向 | 向下取整 | 结构冻结 | 单一规则,非调参项 | @@ -299,9 +303,9 @@ percent 类(每结算、每层): E = |层数 × 行值| ÷ 100 × - 伏击守恒:blast 负向兑付超出上限时兑付数等于上限,超出部分逐颗记"溢出未生效"行;多敌共享同一伏击池值。 - 完美排雷:零误标且至少一面标中时第二份兑付恰一次并发高光行;出现误标或零标中则无高光;计数换图重置。 - 战斗段次序:主角先手后逐敌"查攻击、结算伤害、推进"各恰一次;主角倒下立即短路,剩余敌人不行动。 -- 护甲等级掷点:吃掷点分量(主角普通分量、敌方攻击/重击拍基线)逐次掷骰——掷点不低于防守方护甲等级全额兑付,低于则按档位折扣且档位不设零档;穿刺/魔法分量与棋盘来源伤害(伏击附加、道具、清障炸药、奖励即时)不掷不折。 -- 折扣零值归路:折扣取整后为 0 走既有零值伤害行,不新增行型;掷点读数与档位随该伤害行留档。 -- 数值边界:合成后零伤害不扣血、写零值行;溢杀丢弃不转移;治疗封顶不溢出;全程无负值。 +- 护甲等级掷点:吃掷点分量(主角普通分量、敌方攻击/重击拍基线)逐次掷 d20——掷点 ≥ 防守方护甲等级 + 攻击加成全额兑付;低掷按缺口查档(1-3 → ×0.75 · 4-7 → ×0.5 · 8+ → ×0.25),档位不设零档;nat20 暴击该分量 ×2 且恒全额;穿刺/魔法分量与棋盘来源伤害(伏击附加、爆裂、清障炸药、奖励即时)不掷不折。 +- 伤害下限与留档:任何攻击落地伤害 ≥1,全流水零 v=0 伤害行、零"被减伤归零"行;掷点读数、护甲等级与档位随伤害行留档,nat20 行带暴击标记。 +- 数值边界:伤害下限保底后全程无零值/负值伤害;溢杀丢弃不转移;治疗封顶不溢出。 - 出口分发:先判主角倒下(负)再判敌清空(胜);停手与耗尽轮恒同图,清图轮才允许换图。 - 关卡收束:关卡结束清空魔法道具背包恰一次并返回清空前计数;战斗结束不清背包。 - 可回放(封闭随机流口径):同种子同输入两遍步骤流逐位复现;随机消费仅限封闭随机流——开战掷骰(初始化随机)与护甲等级掷点(回合内随机),无未登记随机源、零界面等待。 @@ -325,3 +329,4 @@ percent 类(每结算、每层): E = |层数 × 行值| ÷ 100 × | v0.7 | 2026-09-15 | **格面资源批(用户拍板四点)**:① 段序七段 → 五段——道具使用窗口与道具段随可选类道具整体删除(2026-09-15 用户裁定)下线,段序常量与 registry settlement_table 同步修订;② 授予段改述为"结算队列 + 抵消过滤":开格 / 标旗 / 撤旗入队,标旗后被撤旗的动作不结算(零兑付、行动点不返);③ 兑付统一走极性矩阵(开安全格 / 标中旗 = 正向,开雷格 / 标错旗 = 负向,字典归奖励道具经济),取代踩雷固定伏击、标中固定削弱、标错三选一哈希反噬三套固定兑付;④ 明细行型 15 → 10(删道具行 / 已消耗未生效行 / 使用被拒行 / 窗口打开锚行 / 窗口跳过锚行),两层豁免集合改两类 + 三类;⑤ 战斗道具背包与库存语义下线,关卡收束只清辅助道具背包;⑥ 收益对照表 flat 类措辞去道具;验收标准改五段全序 + 抵消过滤 + 极性路由正确,删窗口类断言。行动经济 F1 对账锚(伏击共享池)与难度曲线重推义务随批登记(见 registry 修订记录),切片侧修改指导随批产出 | | v0.8 | 2026-09-15 | **护甲等级批(用户拍板登记,只改文档与 registry、不动代码)**:战斗段新增护甲等级掷点规则——必中、非归零,掷点低于防守方护甲等级按减伤档位折扣,没有 miss、没有零收益白回合;只吃基础攻击分量(主角普通分量 / 敌方攻击拍与重击拍基线),穿刺/魔法充能分量不吃;棋盘来源(伏击附加、道具、清障炸药、奖励即时)一律无视;护甲等级(数值轴,数据键 `armor_class`)与护甲类型(克制属性轴)命名区分成文。随机口径修订:结算换算层首次引入回合内随机,可回放契约从"结算路径零随机"改为"封闭随机流可回放"(同种子同输入逐位复现);开战掷骰属初始化随机维持不变。调参表新增护甲等级掷点 / 减伤档位表 / `armor_class` 三行(全部 provisional 未定值);伤害行补掷点读数与折扣档位随行留档(行型 10 类封闭不变);验收标准新增护甲等级掷点与折扣零值归路两条、可回放条改封闭随机流口径;EV 中性引入成文(期望伤害对齐现行表值,"零红档"验收保留);待拍板新增"掷点形式与数值域""掷点的随机流归属"两条 | | v0.9 | 2026-09-16 | 新增「呈现只读面」节(06 清单「呈现只读面契约登记缺口」闭合):把切片已实现的呈现面函数与信号登记为对外契约——呈现状态快照 `presentation_snapshot`、战斗进行中谓词 `is_battle_live`、奖励身份查询 `query_reward_identity`、结算快照信号 `settlement_snapshot`、战斗结束信号 `battle_finished` 五件成文(载荷字段、消费方、销毁前捕获语义),并登记"呈现层唯一状态读取出口、禁链式穿透"的出口纪律与诊断观测面的非契约定位;依赖与输出表 HUD 框架下游行补指路 | +| v0.10 | 2026-09-16 | **护甲等级掷点批·定稿落地(用户拍板,数字即规范——与敌人系统 / 难度曲线与Boss / registry / 切片代码同批)**:判定式定稿——掷 1d20,掷点 ≥ 防守方护甲等级 + 攻击加成(结构位 `attack_bonus`,切片恒 0)→ 全额,低掷按缺口三档折扣(1-3 → ×0.75 · 4-7 → ×0.5 · 8+ → ×0.25,无零档);nat20 暴击该基础攻击分量 ×2 且恒全额(双方同规);伤害下限全局 max(1, ·)(`damage_floor` = 1)——"被减伤归零 v=0"零值伤害行语义退役,重档折扣行文本记"被压制";百分比减伤增益 defense_up 整体退役换装 ac_up(护甲强化 +1 AC/层 ×2 层,`unit=ac` 新量纲、不入任何池,无双轨),off_guard 与池结构零改动;护甲等级与护甲类型分离成文(数值轴 / 属性轴,不做映射)。数值载体:主角 armor_class 10(暂定)、敌方首关 0(暂定,正式行值归怪物设定批,登记待办清单);掷点流归属定稿 = `armor_roll` 封闭流(逐次掷逐次取号,可逐位回放),调参表掷点/档位/下限/加成五行由未定改拍板值;EV 参考数成文(AC10 21.25% / AC11 25% / AC12 28.75%,按判定式复算),换装点锚定(基准 AC10 自带约 −21% 敌方输出期望)与加难杠杆扫描合并处理;验收标准改写掷点条目 + 新增伤害下限与留档条目;切片同批:settlement.tres 七参数入表、战斗段掷点判定/暴击/下限/行留档(roll/ac/tier/crit 字段)、armor_roll 流接入 rng_service(黄金向量 m=42 scope0 = 4806095330042243845)、零值伤害行分支删除、冒烟护栏与掷点行为断言(nat20 种子 / AC12 档位分布 / 池触底保底 / 棋盘必中)、英雄卡与敌人卡 AC 展示、ac_up 状态槽图标、nat20 金框高亮 | diff --git a/design/gdd/systems/战斗循环/敌人系统.md b/design/gdd/systems/战斗循环/敌人系统.md index 7ad3de2c..3b7bf796 100644 --- a/design/gdd/systems/战斗循环/敌人系统.md +++ b/design/gdd/systems/战斗循环/敌人系统.md @@ -80,11 +80,11 @@ | 敌种 | 行为环 | 护甲类型 | 护甲等级 | 现行定位 | |---|---|---|---|---| -| 地精 | 攻击(单拍环) | 轻甲 | 未定(provisional) | 首发杂兵;教学位节点 ×[1,2],开战掷定 | -| 熊地精 | 攻击 / 蓄力 / 重击(三拍环) | 魔法护甲 | 未定(provisional) | 首发主力;重击倍率 2 | -| 食人魔酋长 | 攻击 / 攻击 / 蓄力 / 重击(四拍 Boss 环) | 重甲 | 未定(provisional) | 首发关底 Boss;重击倍率 2 | +| 地精 | 攻击(单拍环) | 轻甲 | 0(暂定) | 首发杂兵;教学位节点 ×[1,2],开战掷定 | +| 熊地精 | 攻击 / 蓄力 / 重击(三拍环) | 魔法护甲 | 0(暂定) | 首发主力;重击倍率 2 | +| 食人魔酋长 | 攻击 / 攻击 / 蓄力 / 重击(四拍 Boss 环) | 重甲 | 0(暂定) | 首发关底 Boss;重击倍率 2 | -护甲类型(轻甲 / 魔法护甲 / 重甲)与攻击类型的克制结算矩阵归[战斗结算](战斗结算.md),本系统只提供每个敌种的护甲类型数据。护甲等级是另一条数值轴(防守方字段,基础攻击掷点的折扣判定读数——2026-09-15 护甲等级批新增):敌人表与主角表各增 `armor_class` 一列,掷点与折扣的结算归[战斗结算](战斗结算.md),行值投值归[难度曲线与Boss](../冒险循环/难度曲线与Boss.md)且未定稿——三敌种与主角的护甲等级行值随定稿批落值,两条轴的概念与字段不得混用。举证:生成与护甲为数据面规则,无计算。 +护甲类型(轻甲 / 魔法护甲 / 重甲)与攻击类型的克制结算矩阵归[战斗结算](战斗结算.md),本系统只提供每个敌种的护甲类型数据。护甲等级是另一条数值轴(防守方字段,基础攻击掷点的折扣判定读数——2026-09-15 护甲等级批新增,2026-09-16 定稿落地):敌人表与主角表各增 `armor_class` 一列(域 [0, 999]),掷点与折扣的结算归[战斗结算](战斗结算.md),行值投值归[难度曲线与Boss](../冒险循环/难度曲线与Boss.md)——三敌种首关暂定 0(第一关怪物 AC 相对较低,正式行值待怪物设定投值,登记[待办清单](../../06-待办清单.md)),主角暂定 10,两条轴的概念与字段不得混用。举证:生成与护甲为数据面规则,无计算。 ### 行为环:敌人按排好的剧本轮流出招,走到哪一拍零随机 @@ -202,7 +202,7 @@ | unit_count_max | [unit_count, 3],可省略 | 缺省 = 定值 | 数量区间上界;开战时在区间内掷定本战实际数量 | | atk_min / atk_max | [0, 999] 可选配对 | ±3 浮动(provisional) | 攻击基线区间:开战掷定一次、整战持久;缺省 = 定值行 | | armor_type | {轻甲, 魔法护甲, 重甲},缺省轻甲 | 地精轻甲 / 熊地精魔法护甲 / 酋长重甲 | 克制结算归[战斗结算](战斗结算.md) | -| armor_class(护甲等级) | 未定(随掷点形式定稿,provisional) | 未定 | 防守方护甲等级——基础攻击掷点的折扣判定读数,掷点结算归[战斗结算](战斗结算.md);与 armor_type(克制属性轴)两概念不得混用 | +| armor_class(护甲等级) | int [0, 999],缺省 0 | 0(首关暂定——正式行值归怪物设定批) | 防守方护甲等级——基础攻击掷点的折扣判定读数,掷点结算归[战斗结算](战斗结算.md);与 armor_type(克制属性轴)两概念不得混用 | | hp | [1, 999] | provisional | 单位生命基线(难度修饰前) | | atk | [0, 999] | provisional | 攻击基线;0 合法 = 纯伏击伤敌种 | | heavy_multiplier | [1, 16] | 2(provisional) | 无重击拍的敌种不消费,非校验项 | @@ -215,9 +215,9 @@ |---|---|---|---| | max_hp | [1, 999] | provisional(出处待查) | 主角生命上限 | | atk | [1, 999] | 6(原型锚 provisional) | 基础攻击;配 0 加载期拒绝——基础攻击恒在 | -| armor_class(护甲等级) | 未定(随掷点形式定稿,provisional) | 未定 | 主角作为防守方的护甲等级,口径同敌人表;掷点结算归[战斗结算](战斗结算.md) | +| armor_class(护甲等级) | int [0, 999] | 10(暂定,2026-09-16 拍板) | 主角作为防守方的护甲等级基准,ac_up(护甲强化 +1/层 ×2 层)在其上加算;掷点结算归[战斗结算](战斗结算.md) | -三敌种现行行值(全部 provisional,出处 registry):地精教学位 24 生命 / 7 攻击 ×[1,2](开战掷定)、群行 13/5 ×[2,3];熊地精 40/9 ×1、群行 18/6 ×[2,3]、深层锚行 30/7;食人魔酋长 150/9 ×1。护甲等级行值未定(provisional,见「待办」)。 +三敌种现行行值(全部 provisional,出处 registry):地精教学位 24 生命 / 7 攻击 ×[1,2](开战掷定)、群行 13/5 ×[2,3];熊地精 40/9 ×1、群行 18/6 ×[2,3]、深层锚行 30/7;食人魔酋长 150/9 ×1。护甲等级行值首关暂定 0(正式行值归怪物设定批,登记[待办清单](../../06-待办清单.md))。 结构冻结(非调参项,修订走本文流程):行为拍枚举封闭、死亡即不动手、先作用后推进、伏击层池共享读法、附加后置加法、伏击按主角血量点计量纲、固定顺位、每坐标恰一敌种、零随机(口径修订 2026-09-15:本系统自身语义零随机不变——行为环与顺位由结算次数唯一决定;随机消费一律在[战斗结算](战斗结算.md)结算换算层按封闭随机流掷定,可回放契约 = 同种子同输入逐位复现)。联动纪律:单轮上限 × 攻击基线(支配边界联动)、数量 × 上限(单结算上界双线性)、倍率 × 攻击(重击拍支配)禁止独立调参,联调须重跑方向性预检并同步 registry(见待办)。 @@ -245,3 +245,4 @@ | v0.1 | 2026-09-15 | 自归档 GDD 压缩改写 | | v0.2 | 2026-09-15 | 教学位掷骰口径自洽(随[难度曲线与Boss](../冒险循环/难度曲线与Boss.md)同批修订):unit_count 字段说明、地精敌种定位与现行行值句中"教学位固定 1 只/固定不掷"改为 ×[1,2] 开战掷定 | | v0.3 | 2026-09-15 | **护甲等级批(用户拍板登记,与战斗结算 / 难度曲线与Boss / registry 同批,不动代码)**:敌人表与主角表 schema 各增 armor_class(护甲等级)字段(取值域随掷点形式定稿,provisional);三敌种表补护甲等级列(行值未定);护甲等级(数值轴)与护甲类型(克制属性轴)命名区分成文;结构冻结清单"零随机"措辞按新口径修订——本系统自身语义零随机不变,掷点随机在战斗结算换算层按封闭随机流消费、可回放契约 = 同种子同输入逐位复现;待拍板新增"护甲等级行值未定"条目 | +| v0.4 | 2026-09-16 | **护甲等级掷点批·定稿落地(与战斗结算 / 难度曲线与Boss / registry / 切片代码同批)**:armor_class 域定稿 [0, 999](缺省 0 向后兼容);主角行值 10(暂定)、三敌种首关行值 0(暂定)——"行值未定"改"暂定值 + 待办",敌方正式行值归怪物设定批(第一关怪物 AC 相对较低,值待怪物设定投值,登记待办清单);敌种表护甲等级列同步落值;调参表两行改域与现行值 | diff --git a/design/registry/entities.yaml b/design/registry/entities.yaml index 2038ae4b..07343588 100644 --- a/design/registry/entities.yaml +++ b/design/registry/entities.yaml @@ -20,10 +20,10 @@ entities: owner: design/gdd/systems/表现与支撑/数据配置.md attributes: kind: autoload - streams: [board_gen, reward_roll, encounter] - stream_names: 当前封闭清单(增流需改代码 + P1 支柱评审;combat_var 已于 2026-09-07 full 评审删除——玩法随机不预置基础设施;**encounter 增流 2026-09-12 用户拍板**——遭遇随机化批 难度曲线与Boss GDD Core Rule 8,scope_event_binding = 每次战斗实例化恰一 scope,挖掘决策域之外 P1 合规) + streams: [board_gen, reward_roll, encounter, armor_roll] + stream_names: 当前封闭清单(增流需改代码 + P1 支柱评审;combat_var 已于 2026-09-07 full 评审删除——玩法随机不预置基础设施;encounter 增流 2026-09-12 用户拍板——遭遇随机化批,scope_event_binding = 每次战斗实例化恰一 scope;**armor_roll 增流 2026-09-16 用户拍板**——护甲等级掷点批,回合内随机首次入流:scope_event_binding = 每次掷点恰一 scope〔逐次掷逐次取号、可逐位回放〕,消费面 = 战斗结算 吃掷点分量判定) stream_facade: open_stream(stream) 取号(计数器+1→派生→返回已播种实例);derive_at(stream, scope) 纯函数供测试/预览(不推计数器) - scope_event_binding: board_gen=每次生成新扫雷图(棋盘生成);reward_roll=每次 board install(格奖励批 + 清图奖励批)(地图战斗资源——2026-09-09 接口漂移改写,原"每次奖励结算 roll 批次";地图战斗资源 Rule 2/AC-1 为契约源);encounter=每次战斗实例化恰一 scope(战斗结算 begin_battle 实例化期、扫雷开始前——unit_count 区间均匀骰,挖掘决策域之外 P1 合规) + scope_event_binding: board_gen=每次生成新扫雷图(棋盘生成);reward_roll=每次 board install(格奖励批 + 清图奖励批)(地图战斗资源——2026-09-09 接口漂移改写,原"每次奖励结算 roll 批次";地图战斗资源 Rule 2/AC-1 为契约源);encounter=每次战斗实例化恰一 scope(战斗结算 begin_battle 实例化期、扫雷开始前——unit_count 区间均匀骰,挖掘决策域之外 P1 合规);armor_roll=每次掷点恰一 scope(战斗结算 战斗段吃掷点分量逐次判定——1d20,护甲等级掷点批 2026-09-16);armor_roll=每次掷点恰一 scope(战斗结算 战斗段吃掷点分量逐次判定——1d20,护甲等级掷点批 2026-09-16) unseeded_access: 致命中止 snapshot_contract: 续玩/复盘快照 = master_seed + 各流 scope 计数器(消费方 存档序列化) referenced_by: [棋盘生成, 战斗结算, 地图战斗资源, 冒险结构, 存档序列化] @@ -34,15 +34,17 @@ entities: attributes: variables: [SALT_s, master_seed, scope] output_range: "[0, 2^63)" - golden_vectors: 见《数据配置》Formulas-R2 向量表(6 常量:board_gen m=42 scope0-3 · reward_roll m=42 scope0=7422916049646135537 · encounter m=42 scope0=1061841193803499209——2026-09-14 数据配置 漂移批 encounter 增流补向量) + golden_vectors: 见《数据配置》Formulas-R2 向量表(7 常量:board_gen m=42 scope0-3 · reward_roll m=42 scope0=7422916049646135537 · encounter m=42 scope0=1061841193803499209 · armor_roll m=42 scope0=4806095330042243845——2026-09-16 护甲等级掷点批 armor_roll 增流补向量) salt_board_gen: 0xA0761D6478BD642F salt_reward_roll: 0xE7037ED1A0B428DB salt_encounter: 0x8EBC6AF09C88C6E3(2026-09-14 encounter 增流——互异奇常数 ≠ 乘数) + salt_armor_roll: 0xC2B2AE3D27D4EB2F(2026-09-16 armor_roll 增流——互异奇常数 ≠ 乘数) + salt_armor_roll: 0xC2B2AE3D27D4EB2F(2026-09-16 armor_roll 增流——互异奇常数 ≠ 乘数) multiplier: 0x9E3779B97F4A7C15 impl_spec: wrap64=二补数自然回绕;禁 float 中间量、禁对中间量用 %、禁"主动修复溢出";常量用分段表达式书写;掩码收尾 reproducibility_claim: 同冒险内同流各 scope 无碰撞 + 跨版本恒定(不做跨 master_seed 全局单射声明) master_seed_int64_min: 致命(abs 恒等回绕→与 0 静默别名) - set_master_seed_precondition: 仅全流计数器全零时受理(3 流——2026-09-14 数据配置 漂移批口径) + set_master_seed_precondition: 仅全流计数器全零时受理(4 流——2026-09-14 数据配置 漂移批口径 + 2026-09-16 护甲等级掷点批 armor_roll 增流) referenced_by: [棋盘生成, 战斗结算, 地图战斗资源, 冒险结构, 存档序列化] - name: difficulty_row_resolution @@ -123,13 +125,13 @@ entities: max_stack: int [1,16](仅 stack 消费——refresh/independent 行内容约定恒 1,非校验项) mvp_content: attack_up: { display_name: 增伤, target_side: hero, magnitude: "+50 percent", duration_settlements: 1, max_stack: 2, stack_policy: stack, source_class: safe_cell } # 2026-09-11 两级奖励批 D=2→1(当次结算语义) - defense_up: { display_name: 减伤, target_side: hero, magnitude: "-30 percent", duration_settlements: 1, max_stack: 2, stack_policy: stack, source_class: safe_cell } # 2026-09-11 两级奖励批 D=2→1;2026-09-13 减伤墙批 −50→−30(叠满两层 −60 帽——难度曲线与Boss 投值,增益状态/registry 2026-09-14 接口漂移批回写落地) + ac_up: { display_name: 护甲强化, target_side: hero, magnitude: "+1 ac", duration_settlements: 1, max_stack: 2, stack_policy: stack, source_class: safe_cell } # 2026-09-16 护甲等级掷点批换装(原 defense_up 减伤 −30 percent ×2 层整行退役、无双轨——历史:2026-09-11 两级奖励批 D=2→1;2026-09-13 减伤墙批 −50→−30);unit=ac 新量纲(护甲等级阈值轴 [−20,20] 且非 0),不入 effect_pool_map 任何池,消费面 = 战斗结算掷点判定(AC10 基准 + 层数×幅度;EV 期望减免 AC10 21.25% / AC12 28.75%) ambush: { display_name: 伏击(敌方增益), target_side: enemy, magnitude: "+3 flat", duration_settlements: 1, max_stack: 16, stack_policy: stack, source_class: mine_hit } weaken: { display_name: 削弱(标中旗 debuff), target_side: enemy, magnitude: "-65 percent", duration_settlements: 1, max_stack: 16, stack_policy: stack, source_class: flag_hit } enrage: { display_name: 暴走(敌方增伤), target_side: enemy, magnitude: "+25 percent", duration_settlements: 1, max_stack: 16, stack_policy: stack, source_class: flag_miss } # 2026-09-12 标错反噬批上线——三池之一(进 enemy_output 池) off_guard: { display_name: 失衡(我方承伤加深), target_side: hero, magnitude: "+25 percent", duration_settlements: 1, max_stack: 16, stack_policy: stack, source_class: flag_miss } # 2026-09-12 标错反噬批上线——三池之一(进 hero_defense 池,承伤 ×1.25);2026-09-15 格面资源批起 = defense 型负向(标错 defense 型安全格 / 踩到 defense 型雷格兑付) sapped: { display_name: 乏力(我方输出降低), target_side: hero, magnitude: "-25 percent", duration_settlements: 1, max_stack: 16, stack_policy: stack, source_class: flag_miss } # 2026-09-15 格面资源批新增——weaken 型负向(标错 weaken 型安全格 / 踩到 weaken 型雷格兑付),进 hero_offense 池(attack_up 镜像);值 provisional - value_status: 七行值全部 provisional(2026-09-15 四行→六行——标错反噬批 enrage/off_guard 两行收编;六行→七行——格面资源批 sapped 收编为 weaken 型负向;伏击复用为反噬第三池项〔现为 blast 型负向〕),验收锚——weaken→行动经济 F1 条款 2(地图战斗资源/战斗结算)· ambush 量纲→敌人系统(条款 3,**量纲已定稿见 ambush_unit_definition**)· 对照表→战斗结算(条款 1)· **weaken 已按 战斗结算 F0 裁决修订 −25→−65(2026-09-10,值域 [60,85]、终值共裁 战斗结算 OQ2——见 anchor_warning 回执)** + value_status: 七行值全部 provisional(2026-09-15 四行→六行——标错反噬批 enrage/off_guard 两行收编;六行→七行——格面资源批 sapped 收编为 weaken 型负向;伏击复用为反噬第三池项〔现为 blast 型负向〕;**2026-09-16 护甲等级掷点批:defense_up 行退役换装 ac_up〔护甲强化 +1 ac ×2 层〕——行数不变、一行换装,ac 幅度暂定值随掷点批定稿),验收锚——weaken→行动经济 F1 条款 2(地图战斗资源/战斗结算)· ambush 量纲→敌人系统(条款 3,**量纲已定稿见 ambush_unit_definition**)· 对照表→战斗结算(条款 1)· **weaken 已按 战斗结算 F0 裁决修订 −25→−65(2026-09-10,值域 [60,85]、终值共裁 战斗结算 OQ2——见 anchor_warning 回执)** ambush_unit_definition: **量纲已定稿(2026-09-09 敌人系统 GDD Rule 6/F2——registry OQ2 关闭条件兑现)**:+3 flat = 每层伏击为每次攻击类行为(attack/heavy)附加 3 点主角 HP 伤害(后置加法、不进倍乘);charge 拍零兑付(空拍蒸发);值仍 provisional(锚验收归 地图战斗资源/战斗结算——行动经济 F1 条款 3;敌人系统 供给 cap 表值与单结算自然边界口径见 clause3_supply 条目) anchor_warning: weaken 行 vs 行动经济 F1 条款 2 锚两独立线性代理同向预检不过(百分点·结算暴露 25 vs 100;原型战斗量级 ≈1 vs ≈4 当量)——战斗结算 对照验收优先裁决,登记 战斗结算 强制检查点输入。**修复空间不对称(2026-09-09 增补)**:每结算瞬时当量归一 m_w≥51 可行(F2 越域界随移 AP 界 −408%/表界 −816%);每 AP 总暴露归一 m_w>100 被 percent ∈ [-100,100] 域禁止(该分支不可行)——OQ1 前置步骤:战斗结算 先钉死归一口径,口径落定前禁止数值修订;落总暴露则现实解 = 地图战斗资源 重定最低档参照。**参照对象迁移注记(2026-09-09 地图战斗资源 评审;2026-09-11 回迁)**:最低档安全格 buff 当量的参照对象曾迁 defense_up 当量(attack_up 时为死行——地图战斗资源 F4 迁移)——**2026-09-11 用户裁决"基础攻击恒存在"(HeroTable.atk 域 [1,999]、0 加载拒绝、MVP=6)后 attack_up 复活,参照对象回迁 attack_up 当量**(消费面 = 主角战斗段先手攻击——战斗结算 GDD 同批;defense_up 迁移退役为历史记录,attack_up 入池时 tier_min_buff 重算);本条与 clause3_supply.direction_precheck 的 25 vs 100 预检算术系 attack_up 口径历史登记——defense_up |−50|×dur2 与 attack_up 数值巧合全等(100pp),今日结论不变;**迁移后预检未按新参照重跑**的义务已于 2026-09-14 战斗结算 重算批兑现(F0/F1 按 attack_up 当量重跑 + F1 锚对照实物改 attack_up 行——本迁移注记关闭)。**F0 裁决回执(2026-09-10 战斗结算 GDD——红警关闭)**:归一口径 = **每结算瞬时当量**(总暴露分支被 percent schema 域禁止 + 与"单结算自然边界"已锁事实冲突,排除);m_w 修订 −25 → **值域 [60,85]/默认 −65**(mvp_content.weaken 已同批改写——锚两腿 @D_ref=12:7.8 > 6.0 严格大于 ✓ / 7.8 ≥ E_dig 4.5 ✓〔E_dig 读数 2026-09-14 战斗结算 重算批按 难度曲线与Boss v3 曲线重推——原 7.2 随口径退役〕);地板 = 乘数零点 −100pp、交叉层数 = ⌈100/m_w⌉ = 2(原"7 层触地板"预读按 −400% 合成带估读,更正);defense_up 参照保持(总暴露分支的"重定参照"现实解未采纳);终值 = 地图战斗资源/战斗结算 共裁(战斗结算 OQ2);增益状态 GDD 正文不改以本条为准(收编先例) rule4_exemption: 定义表非进度成长表,键 = effect_id(数据配置 Rule 4 豁免,schema 注释承载) @@ -291,7 +293,7 @@ entities: cross_field_validation: 加载期致命族——池空(cell 池 ≥1 正向行 weight>0)· 五型正向行齐备 · 五型负向行齐备(字典完整性——缺任一极性行致命)· clear_default 恒正当量(≥1 活行 ∧ power≥1 ∨ effect_id 非空——行动经济 条款④"清图奖励>0"的行级兑现口径)· percent 行 effect_id 必填/flat 行恒空 · flat 行 power≥1 # 2026-09-15 重写——旧 floor_supply_missing / no_damage_row 随可选类删除失义退役 mechanics: 格面资源身份 install 期预分配(roll_board 恰一次、行主序、消费 reward_roll 封闭流恰一 scope 批〔格批 + 清图批尾一号〕;全部非封锁格逐一摇定类型——安全格与雷格同池同分布、封锁格不入摇号清单;Ready 后只读、无重掷 API)· **极性不在资源内**——「动作 × 格型」矩阵(开安全格/标中雷 = 正向、开雷格/标错安全格 = 负向、标旗后撤旗 = 零兑付)字典定义归 地图战斗资源、路由执行归 战斗结算 授予段(结算队列 + 抵消过滤——标旗后被撤旗不结算)· 授予期零随机(身份恒定跨轮;换图映射消亡)· 负向恒即时(不入库、无库存语义——库存已随可选类删除下线)· 满层零兑付 = 合法例外 + "奖励未生效"行 · 重复受理幂等(同格重复 grant / 同批次重复 grant_clear → 同一快照/身份 + 诊断;受理/兑付恰一次归 战斗结算×②③) api: 封闭枚举入口(provisional,实现期 ADR 定名)——roll_board / query_identity(返回 {reward_id, cell_type, tier} 引用级投影;未开格恒可查——魔法道具 桥接结构前提;清单外坐标 → null + 诊断)/ grant(同格重复受理幂等 + 诊断)/ grant_clear(同批次重调幂等 + 诊断)/ retire_mapping / equivalence_supply(全活行投影 + tier_min_buff)+ weighted_pick 纯函数面;新增入口 = 接口漂移,新增入口须随附全输入域语义表 # 2026-09-15 删 stock/use/query_inventory/clear_inventory(库存轴下线) - mvp_content: 十行(2026-09-15 格面资源批;值 provisional 归 难度曲线与Boss)——格面池正向五行(attack_up buff→attack_up w20 t2 / defense_up buff→defense_up w16 t2 / spark_aoe flat p4 w16 t1〔blast〕/ heal_minor flat [2,6] w15 t1〔heal〕/ weaken buff→weaken w12 t1)+ 负向五行纯字典(enrage attack〔→enemy_output〕/ off_guard defense〔→hero_defense〕/ 直伤 heal flat p4〔不经增益状态〕/ ambush+2 blast〔stacks=2〕/ sapped weaken〔→hero_offense〕)+ clear_blast(flat [25,55] w1 t2 clear_default——完美板 ×2 期望中点 80);类型频次 = 正向行权重(20/16/16/15/12 合计 79);tier_min_buff = 2 + mvp_content: 十行(2026-09-15 格面资源批;值 provisional 归 难度曲线与Boss)——格面池正向五行(attack_up buff→attack_up w20 t2 / ac_up buff→ac_up w16 t2〔护甲强化——2026-09-16 护甲等级掷点批换装,原 defense_up 减伤 −30% 退役;cell_type defense/权重/档位零改动〕 / spark_aoe flat p4 w16 t1〔blast〕/ heal_minor flat [2,6] w15 t1〔heal〕/ weaken buff→weaken w12 t1)+ 负向五行纯字典(enrage attack〔→enemy_output〕/ off_guard defense〔→hero_defense〕/ 直伤 heal flat p4〔不经增益状态〕/ ambush+2 blast〔stacks=2〕/ sapped weaken〔→hero_offense〕)+ clear_blast(flat [25,55] w1 t2 clear_default——完美板 ×2 期望中点 80);类型频次 = 正向行权重(20/16/16/15/12 合计 79);tier_min_buff = 2 direction_precheck: **刀供给方向预检随可选类删除失义退役(2026-09-15)**——历史读数见底部修订记录;新对账锚 = 行动经济 条款②③ 左臂(正向格面资源当量供给)随格面资源批重推,义务归 难度曲线与Boss 调参批 settlement_obligations: 四项(战斗结算 GDD 门禁评审缺项即 blocking,清单全文见 地图战斗资源 GDD Dependencies)——① 结算段序骨架(授予→增益/减益→战斗→衰减→判定)② 极性路由受理 + 每格恰一次 + 满层零兑付"奖励未生效"行(含聚合计数字段)③ clear_reward 授予路径(增益状态 OQ4 关闭条件)④ 生命周期编排(battle_over retire 映射消亡)+ roll_board 恰一次 # 2026-09-15 由六项收拢(窗口/道具段与库存编排随可选类删除失义) hud_obligations: 三项(缺项即 blocking)——① 已开格资源身份恒定可读(挖掘期规划面)+ 未开格类型可查面的呈现深度(预读边界待办)② 结算明细行(效果行/"奖励未生效"〔聚合计数〕/标旗未中行/直伤行)③ H2 清图奖励行强调呈现 # 2026-09-15 由五项收拢(库存常读条/道具窗口随可选类删除失义) @@ -315,14 +317,14 @@ entities: p_perfect: int {100,200} percent(knob,默认 200 provisional——完美排雷清图奖励**第二份兑付触发**;2026-09-10 战斗结算 评审批修订:真实双份交付载体〔Core Rule 13#2〕——100 = 仅高光、200 = 双份交付,非整数倍数乘数无运行时载体、域外加载致命;上界受护栏 B 约束〔双读数:D_ref 口径 24 / DPS 严格读数 ≈8,裁决归 难度曲线与Boss〕) segment_order: StringList(结构冻结——段序骨架 [grant, buff_debuff, combat, decay, verdict];加载期恒等拍板序校验违例致命,防实现乱序静默漂移;2026-09-15 格面资源批五段——item_window/item 两段随可选类删除下线,旧七段序见底部修订记录) source_label_map: Dictionary source_class→文案键(结构冻结——P2 归因呈现映射;键集 = 增益状态 source_class ∪ {判定扩展, enemy_action, flag_miss}——2026-09-10 战斗结算 评审批修订;2026-09-15 格面资源批删"道具"扩展键;新增键 = 接口漂移) - effect_pool_map: Dictionary effect_id→pool {hero_offense|hero_defense|enemy_output|flat_attach}(结构冻结——F2 效果语义映射,增益状态 Rule 7 载体;键 ⊆ StatusEffectTable 活行集、悬空加载期致命;新 effect_id 上线须随批登记——2026-09-15 格面资源批随 sapped 行收编登记:sapped → hero_offense) + effect_pool_map: Dictionary effect_id→pool {hero_offense|hero_defense|enemy_output|flat_attach}(结构冻结——F2 效果语义映射,增益状态 Rule 7 载体;键 ⊆ StatusEffectTable 活行集、悬空加载期致命;新 effect_id 上线须随批登记——2026-09-15 格面资源批随 sapped 行收编登记:sapped → hero_offense;**2026-09-16 护甲等级掷点批:删 defense_up → hero_defense 键**〔减伤百分比行退役,hero_defense 池现仅 off_guard 正向行〕;ac_up 为 AC 阈值轴效果、不入任何池) pool_clamp_lo: int pp(结构冻结 −100——percent 池地板域下界,乘数域随之 [0,2];2026-09-10 统一八字段口径) pool_clamp_hi: int pp(结构冻结 +100——percent 池地板域上界;原"七字段 pool_clamp_lo_hi"措辞作废,数据配置 GDD 行按收编先例以本条为准) round_direction: enum {floor}(结构冻结——单一取整规则) charge_grants: Dictionary 奖励触发源→{attack_type, per_unit, cap}(**2026-09-15 攻击类型/奖励抽象分离批新增**——攻击类型 = 伤害的属性轴(定义不含来源),充能 = 按攻击类型分桶的附加伤害储备;"哪类棋盘事实充哪个桶、每单位多少点"归本字典 = 奖励定义,替换字典即改奖励内容,攻击类型与 armor_effectiveness 不动;现行 safe_cell(开格)→ pierce 1/格 cap 8 · flag_hit(标中旗)→ magic 2/雷 cap 0=不封顶;**原 pierce_per_cell / pierce_cap / magic_per_flag("类型×来源"焊死命名,更早为 damage_a_per_cell / damage_a_cap / damage_b_per_flag)同批作废**;数值 provisional 归 难度曲线与Boss;正式归属(地图战斗资源 的奖励类别/效果面)见 战斗结算「待办」) armor_effectiveness: Dictionary 攻击类型×护甲→百分比(键集结构冻结 {normal, pierce, magic} × {light, ward, heavy};默认 normal 全 100 / pierce→ward=150、heavy=50 / magic→heavy=150、ward=50——克制 ×150 / 中性 ×100 / 被克 ×50;**2026-09-15 命名统一批:改为语义键,原 {base, dmg_a, dmg_b} × {none, a, b} 位置键作废**;数值 provisional 归 难度曲线与Boss) - ac_roll: 护甲等级掷点(**2026-09-15 护甲等级批新增,未定稿**——形式 d20 / 百面骰 / 区间均匀未定、数值域未定,登记 战斗结算「待办」;回合内随机 = 结算换算层首次引入;吃掷点分量 = 主角先手攻击普通分量 / 敌方攻击拍与重击拍基线伤害;掷点 ≥ 防守方护甲等级 → 全额、掷点 < 护甲等级 → 档位折扣,必中非归零、无 miss 无零收益白回合;穿刺/魔法充能分量与棋盘来源伤害〔伏击附加 / 道具 / 清障炸药 / 奖励即时〕不掷不折) - ac_discount_tiers: Dictionary 掷点差距档→折扣百分比(**2026-09-15 护甲等级批新增,未定稿**——档数与数值未定 provisional 归 难度曲线与Boss;档位不设零档〔非归零〕,折扣后取整沿全局 floor、为 0 走既有零值伤害行不新增行型;EV 中性——期望伤害对齐现行表值,现行数值不重调,难度曲线与Boss 生存链区间补记掷点带宽口径、"零红档"验收保留) + ac_roll: 护甲等级掷点(**2026-09-16 护甲等级掷点批定稿,数字即规范**——骰面 roll_faces=20、nat 最大面 crit_roll=20、暴击倍率 crit_multiplier=200、攻击加成 attack_bonus=0〔结构位,元进度/武器附魔未来投值〕、伤害下限 damage_floor=1、流名 armor_roll_stream="armor_roll";判定式:掷 1d20,掷点 ≥ 防守方护甲等级 + 攻击加成 → 全额、低掷按缺口查档〔见 ac_discount_tiers〕,nat20 暴击该基础攻击分量 ×2 且恒全额〔双方同规〕;任何攻击落地伤害 ≥ damage_floor〔全局 max(1,·)——零值伤害行语义退役,重档折扣行文本记"被压制"〕;吃掷点分量 = 主角先手攻击普通分量 / 敌方攻击拍与重击拍基线伤害,穿刺/魔法充能分量与棋盘来源伤害〔伏击附加 / 爆裂 / 清障炸药 / 奖励即时〕必中全额不掷;回合内随机、消费 armor_roll 封闭流逐次掷逐次取号,可逐位回放) + ac_discount_tiers: Dictionary 缺口档边界→档位值 percent(**2026-09-16 定稿**——roll_tier_bounds=[3,7] · roll_tier_discounts=[75,50,25],档数 = 边界数 + 1;缺口 1-3 → ×0.75 · 4-7 → ×0.5 · 8+ → ×0.25,档位不设零档〔非归零〕,取整沿全局 floor;EV 参考数〔按判定式复算〕= AC10 期望减免 21.25% / AC11 25% / AC12 28.75%;微调旋钮 = 边界与档位值,不动 AC 值;难度曲线与Boss 生存链区间带宽补扫含暴击档与折扣档、"零红档"验收保留) validation: segment_order 违例致命 · effect_pool_map 悬空致命 · p_perfect 域 {100,200}(2026-09-10 载体域——100 = "完美必须 ≥ 不加倍"机制语义下界,200 = 双份交付)——战斗结算 冒烟三行致命 manifest(segorder_violation / poolmap_dangling / pperfect_oob) mechanics: 结算段序骨架唯一权威载体(授予→增益/减益→战斗→衰减→判定——2026-09-15 格面资源批五段,item_window/item 两段随可选类删除下线)· **随机口径(2026-09-15 护甲等级批修订)**:结算换算层引入回合内随机(ac_roll 护甲等级掷点——必中非归零、低掷按 ac_discount_tiers 档位折扣),掷点流归属未定〔rng_service 流封闭清单 board_gen/reward_roll/encounter 不含结算内消费——新增流须改代码+P1 支柱评审、复用 encounter 则改"实例化期恰一 scope"绑定,登记 战斗结算「待办」,rng_service 条目随定稿批修订〕· 可回放契约 = **封闭随机流可回放**(同 master_seed 同意图序列 ⇒ 步骤流逐位复现——数据配置 AC-15 落点,原"结算路径零随机 / 步骤流逐行相同"口径随批作废;开战掷区间 / 装板摇号属初始化随机维持不变)· 调用序钉死 query_attack→apply_damage→advance · 衰减恒执行含末轮 · 高光双落载体(perfect_sweep P_perfect——2026-09-10 修订为真实双份兑付载体 / 标中旗 highlight 标记)· 流水行协议字段分层(公共 7 字段 + 行类型特有 target/effect_id/pool_value/聚合计数——2026-09-10 评审批修订;行类型封闭枚举 10 类——2026-09-15 格面资源批由 15 类收拢:删道具行 / 已消耗未生效行 / 使用被拒行 / 窗口打开锚行 window_open / 窗口跳过锚行 window_skip 五类随可选类删除下线〔2026-09-15 窗口锚行批曾由 13 扩为 15,同日回撤〕;行来源断言两层豁免集合同批修订 = 行型豁免 {decay, verdict} × 来源类别豁免 {hero_attack, enemy_action, clear_reward},豁免不等于免检——豁免行仍受公共字段与各自语义约束,清单改动须同步 战斗结算 明细流水一节与 battle_log_view 格式化)· **穿刺/魔法 充能与护甲克制结算规格 = 战斗结算 GDD F6(W3 补全,2026-09-14 重算批;2026-09-15 命名统一批)**——挖掘期充能/战斗段消费/单轮生命周期、不吃 percent、逐分量 armor_effectiveness 查表 floor 相加 closure_receipt: 行动经济 F1 条款①②③⑤闭合 owner = 战斗结算(2026-09-10 F0–F5 首版验算完成——F0 瞬时当量口径 + m_w 值域裁决见 status_effect.anchor_warning;条款③ clause3_supply 闭合回执;条款⑤ clause5_dominance 条目;actuals p10 交付后强制重跑) @@ -637,3 +639,15 @@ entities: # 本表同步:owner 路径(reward_table / reward_config / aux_items 三处)与 referenced_by、各属性文本中的系统名提及全部改新名; # 英文代号 REWARD_ITEM_ECONOMY / AUXILIARY_ITEMS 与条目键 reward_/aux_ 暂沿用(代码标识符与工具链口径),与中文名的对齐另批执行。 # 底部历史注释行保留各批原文旧名作为修订留痕,现行口径以上方条目为准。 + +# 2026-09-16 护甲等级掷点批·定稿落地(用户拍板 2026-09-16,数字即规范——与 战斗结算 v0.10 / 敌人系统 v0.4 / +# 难度曲线与Boss v0.5 / 增益状态 v0.4 / 地图战斗资源 v0.6 / 03-内容规模 / 切片代码同批): +# ① settlement_table.ac_roll / ac_discount_tiers 两字段由"未定稿"改拍板值(d20 / 缺口 1-3→×0.75 · 4-7→×0.5 · 8+→×0.25 / +# nat20 暴击 ×2 恒全额 / damage_floor=1 / attack_bonus=0 结构位 / armor_roll_stream="armor_roll"); +# ② rng_service 增流 armor_roll(回合内随机首次入流,scope_event_binding = 每次掷点恰一 scope)+ +# salt_armor_roll + 黄金向量(m=42 scope0 = 4806095330042243845)+ 重播种前提改 4 流; +# ③ enemy / hero 两条目 armor_class 由"取值域未定"改 [0,999]:主角 10(暂定)、敌方首关 0(暂定——正式行值归怪物/ +# 敌人系统设定批,登记待办清单,随加难杠杆扫描合并调); +# ④ status_effect:defense_up 行退役换装 ac_up(护甲强化 +1 ac ×2 层,unit=ac 新量纲,不入任何池); +# ⑤ reward_table:defense 型正向行 defense_up → ac_up(显示名暂定"护甲强化",权重/档位/cell_type 零改动); +# ⑥ settlement_table.effect_pool_map 删 defense_up 键(hero_defense 池现仅 off_guard 正向行——×0 免伤墙结构面消失)。 diff --git a/production/session-state/active.md b/production/session-state/active.md index 4f89e683..6b0e1c3f 100644 --- a/production/session-state/active.md +++ b/production/session-state/active.md @@ -145,3 +145,7 @@ 55. **文档三补丁批(2026-09-16,06 清单第一类免拍板项闭环,零代码零数值)**:① 战斗表现 v0.2 新增「一屏样张的验收标准」(内容清单 + 五项按序判定 + 判定回退,四件锁定物过判口径落到 art-bible §1.4/§3.1/§4.1 与 battle.md 四区/双舞台已冻结口径上);② 战斗结算 v0.9 新增「呈现只读面」节——切片已实现的 presentation_snapshot / is_battle_live / query_reward_identity / settlement_snapshot / battle_finished 五件登记为对外契约(载荷字段、唯一读取出口纪律、诊断观测面非契约定位);③ 音频 v0.2 新增「临时音效的覆盖清单」(必覆盖五挂点 / 选配二 / 明确不覆盖二 + 切片验收口径),顺手修明细行型 15→10 陈旧两处(格面资源批漏同步)。06 清单三条改「已解决(2026-09-16)」留痕。 56. **06 清单改版批(2026-09-16,用户两点拍板,纯文档)**:① 术语——「待拍板」全仓改「待办」:清单 git mv 更名 06-待办清单.md;20 份系统文档存根节与正文引用、02-系统清单(v0.6 行)、doc-conventions(v7 行,第 8 节规则更名「待办单点登记」)、code-conventions、architecture、registry 活条目同步;版本历史行与 registry # 批注按「历史不改写」保留旧称。② 格式——清单由四列表格改为四循环 H2 + 系统 H3 + 每条事项 H4 + 粗体属性段(现状/归属/状态),70 条原样搬运(67 待办 + 3 已解决)。自检:条目数对账、旧文件名活引用清零、重叠文件(entities.yaml/难度曲线与Boss)diff 核对仅含本批改动;并行 AC 换装批的切片文件集未触碰未提交。 + +57. **护甲等级掷点批 · 定稿落地(2026-09-16,用户拍板"数字即规范"——切片代码 + 文档 + registry 同批,终局修复无伤通关根因)**:**判定式**:攻击结算掷 1d20,掷点 ≥ 防守方 AC + attack_bonus(结构位恒 0)→ 全额,低掷按缺口三档折扣(1-3→×0.75 · 4-7→×0.5 · 8+→×0.25,无零档);nat20 暴击该基础攻击分量 ×2 恒全额(双方同规);伤害下限全局 max(1,·)("被减伤归零 v=0"零值伤害行退役,重档行文本记"被压制");掷点只吃基础攻击分量(主角普通分量 / 敌方攻击与重击拍基线),穿刺/魔法充能与棋盘来源必中全额。**换装**:defense_up(减伤 −30% ×2)整行退役 → ac_up(护甲强化 +1 AC/层 ×2 层,unit=ac 新量纲、不入任何池);hero AC 10 / 敌方首关 0(暂定——正式行值归怪物设定批,新待办已登记);off_guard 与池结构零改动。**实现**:rng_service 增 armor_roll 流(salt 0xC2B2AE3D27D4EB2F,每次掷点恰一 scope,黄金向量 m=42 scope0=4806095330042243845);settlement.tres 七掷点参数(roll_faces/bounds[3,7]/discounts[75,50,25]/crit_roll/crit_multiplier/damage_floor/armor_roll_stream + attack_bonus)+ effect_pool_map 删 defense_up;hero/enemy 表 + armor_class 字段(域 [0,999]);battle_orchestrator 战斗段掷点判定 + damage_math 纯函数(roll_discount_percent / rolled_component)+ 伤害行随行 roll/ac/tier/crit 留档;presentation:英雄卡/敌人卡 AC 展示位、ac_up 状态槽图标◇、nat20 金框高亮行、开始页帮助文案表值驱动(顺手修 AP 8→6 遗留硬编码)。**验证**:冒烟 **233 项 0 失败**(+22:换装护栏〔defense_up 三处零残留 + 免伤墙护栏泛化〕+ 掷点行为面〔查档全掷点域 AC10/AC12 分布 + EV 锚 1575/2000=21.25% + nat20 种子扫描行为断言 + 池触底保底 1 + AC12 档位实际流动 + 棋盘必中〕);fresh 池攻击链回归改掷点口径;三探针 PASS;damage_probe/defense_scan(改 AC 口径)/monte_carlo 工具全通;真实种子逐行核算(seed 4200:roll5/AC11→×0.5→floor(4×1.75×0.6)=4 逐行归因;seed 23 nat20 暴击 24=6×(1+100%)×2);level_monte_carlo -- 30 4200 both:完美档 0/0 通关 **100%**(维持)、承伤 22.4→30.9(减伤墙 −60% 退役 + AC10 −21.25% 兑换净效果,零承伤轮结构性消失)、Boss 场 29.8%→36.0%(带 ≤50% 内)、盲挖档 7% 持平。**文档**:战斗结算 v0.10(掷点节定稿 + 调参表五参数 + 验收改写)/ 敌人系统 v0.4 / 难度曲线与Boss v0.5(换装点锚定:基准 AC10 自带约 −21% 敌方输出期望,与加难杠杆扫描合并调;带宽补扫含暴击档与折扣档)/ 增益状态 v0.4 / 地图战斗资源 v0.6 / 03-内容规模 / registry(rng_service 四流 + settlement ac 两字段定稿 + enemy/hero/reward_table/status_effect + 修订记录)/ 06-待办清单(三项 AC 条目改已裁决留痕 + 新增「敌方护甲等级正式行值」待办)。**已知口径注记**:批提案 EV 参考数 AC12=32.5% 与判定式复算 28.75% 不符,按判定式(规范)登记 28.75% 并在文档注明复算口径。 + +58. **美术素材管线拍板 + 主角样例批(2026-09-16,任务已收口)**:评估结论 = art-bible §1–4 足以支撑 §1.4 格子样张批,全套生产素材缺口 = §5 角色比例 / §8 资产规格(像素尺寸/格式/帧规范未成文,样张候选建议 32px,需拍板)。主角 DND 冒险者站姿像素样例经 PIL 脚本程序化生成(`design/art/samples/`:hero-standing-pose.png 32×48 + 四联测试条〔掩尘/裸土/暗底/灰度〕+ generate_hero_sample.py 字符画生成器;小人蓝主色 / 亮色描边 / 落影底盘 / 3 头身候选 / 对称剪影,art-bible 合规自检全过)。**用户评效不佳,拍板美术素材改走 ComfyUI 自行生成**——角色/场景插画级不再用脚本直出;脚本管线仅留给图标/格面/样张类精度约束资产(如立项);样例三件留存未删。合规校验义务不随管线转移:ComfyUI 出图仍按 art-bible §1.4 验收项(对比度/灰度可辨)做程序化校验。 diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/battle_orchestrator.gd b/prototypes/dungeon-sweeper-vertical-slice/core/battle_orchestrator.gd index 5a33bb2d..08ffb6bd 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/core/battle_orchestrator.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/core/battle_orchestrator.gd @@ -4,7 +4,8 @@ # 战斗结算编排中枢:轮次生命周期唯一 owner · 五段结算(授予→增益/减益→战斗→衰减→判定——2026-09-15 # 格面资源批,道具窗口/道具段随可选类删除下线)· 授予段极性路由(「动作 × 格型」矩阵:开安全格/标中旗 # = 正向,开雷格/标错旗 = 负向;结算队列 + 抵消过滤——标旗后被撤旗以轮末在场旗为基线不判)· -# 10 类行协议可回放步骤流(零随机零 await)· 高光双落(完美排雷 ×2 / 标中旗 highlight)· +# 10 类行协议可回放步骤流(零 await;随机消费 = 开战遭遇骰初始化随机 + 护甲等级掷点回合内随机—— +# 均走封闭命名流,2026-09-16 护甲等级掷点批)· 高光双落(完美排雷 ×2 / 标中旗 highlight)· # Waiting 相位结算触发(不依赖事件到达序)。 # 切片登记:① 事件总线载体 = 直连信号(实现期 ADR 项,provisional)② Waiting 相位刷新通道 2026-09-15 用户拍板整体 # 拆除(仅 Active 挖掘期受理)③ 效果行协议 = 授予段逐 grant 行 + 增益/减益段续存实例组合行。 @@ -402,32 +403,40 @@ func _combat_segment(ri: int) -> void: # ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动;授予段负向直伤致死 → 致电短路不先手)。 # 三段伤害构成(2026-09-12 用户拍板;2026-09-15 命名统一批 + 攻击类型/奖励抽象分离批):普通(atk×(1+attack_up),不吃克制) # + 穿刺/魔法(由充能供给——充能按攻击类型分桶,谁能充/充哪个桶归奖励定义 charge_grants)。攻击类型只管查甲效矩阵哪一行,与来源无关。 + # 护甲等级掷点批(2026-09-16):普通分量吃掷点判定(含暴击);穿刺/魔法充能分量与棋盘来源伤害不掷不折; + # 任何攻击落地伤害 ≥ damage_floor(全局 max(1,·))——"被减伤归零 v=0"零值伤害行分支随批退役。 var hero_alive: bool = int(_enemy_system.hero_unit().get("hp", 0)) > 0 var hero_target = _enemy_system.default_target() if hero_alive and hero_target != null: 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 points := _charge_points() # {攻击类型: 充能点数}——奖励定义投递的结果(本段不认识"开格/标雷") + var roll_read: Dictionary = _armor_roll() # 普通分量掷点(每吃掷点分量逐次掷) + var threshold: int = _defender_ac(hero_target) + int(db.settlement.attack_bonus) + var crit: bool = bool(roll_read.crit) + var discount: int = _roll_discount(int(roll_read.roll), threshold) + var base_val: float = float(_hero_atk) * (100.0 + float(up)) / 100.0 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))) # 普通分量走攻击力基线:吃增益、不吃克制 + c = DamageMath.rolled_component(base_val, discount, crit, int(db.settlement.crit_multiplier), _armor_effectiveness("normal", armor)) 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) - 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, payload) + payload["roll"] = int(roll_read.roll) + payload["ac"] = threshold + payload["tier"] = discount + if crit: + payload["crit"] = true + var d_landed: int = maxi(int(db.settlement.damage_floor), d_hero) + _enemy_system.apply_damage(hero_target, d_landed) + payload["killed"] = not hero_target.alive # 击杀标记在扣血后取(原语义不变) + _emit_row("damage", "hero_attack", Vector2i(-1, -1), ri, _attack_label("主角攻击", crit, discount), d_landed, payload) for u in _enemy_system.query_alive("enemy"): if int(_enemy_system.hero_unit().hp) <= 0: break # hero hp=0 立即短路(死尸挨打行是归因噪音——用户拍板) @@ -436,15 +445,59 @@ func _combat_segment(ri: int) -> void: if beat == "charge": _emit_row("charge", "enemy_action", Vector2i(-1, -1), ri, "%s 蓄力(本轮踩雷白喂)" % u.display_name, 0, {"target": "hero"}) else: - var d_tick := _resolve_damage(int(atk.total)) - if d_tick <= 0: - _emit_row("damage", "enemy_action", Vector2i(-1, -1), ri, "%s 攻击(被减伤归零)" % u.display_name, 0, {"target": "hero", "baseline": int(atk.baseline), "attach": int(atk.attach)}) - else: - _enemy_system.apply_damage(_enemy_system.hero_unit(), d_tick) # 调用序②受击上报 - _emit_row("damage", "enemy_action", Vector2i(-1, -1), ri, "%s 攻击" % u.display_name, d_tick, {"target": "hero", "baseline": int(atk.baseline), "attach": int(atk.attach)}) + # 基线部分吃掷点判定(含暴击)+ 伏击附加(必中全额)→ 合计 → 增益池乘区 → max(1, ·)(伤害下限) + var roll_read_e: Dictionary = _armor_roll() + var threshold_e: int = _defender_ac(_enemy_system.hero_unit()) + int(db.settlement.attack_bonus) + var crit_e: bool = bool(roll_read_e.crit) + var discount_e: int = _roll_discount(int(roll_read_e.roll), threshold_e) + var rolled_base: int = DamageMath.rolled_component(float(int(atk.baseline)), discount_e, crit_e, int(db.settlement.crit_multiplier), 100) + var d_tick := _resolve_damage(rolled_base + int(atk.attach)) + d_tick = maxi(int(db.settlement.damage_floor), d_tick) + _enemy_system.apply_damage(_enemy_system.hero_unit(), d_tick) # 调用序②受击上报 + var payload_e: Dictionary = {"target": "hero", "baseline": int(atk.baseline), "attach": int(atk.attach), + "roll": int(roll_read_e.roll), "ac": threshold_e, "tier": discount_e} + if crit_e: + payload_e["crit"] = true + _emit_row("damage", "enemy_action", Vector2i(-1, -1), ri, _attack_label("%s 攻击" % u.display_name, crit_e, discount_e), d_tick, payload_e) _enemy_system.advance(u) # 调用序③推进——每结算每存活敌恰一次(hero 不在推进域) +# 攻击行显示名(掷点档位随行文本可读——2026-09-16 护甲等级掷点批;零值行语义已退役,重档折扣 = "被压制") +func _attack_label(base: String, crit: bool, discount: int) -> String: + if crit: + return base + "(暴击)" + if discount < 100: + return base + "(被压制)" + return base + + +# 护甲等级掷点(armor_roll 封闭流——逐次掷逐次取号、逐位可回放):返回 {roll, crit} +func _armor_roll() -> Dictionary: + var stream: RandomNumberGenerator = rng.open_stream(String(db.settlement.armor_roll_stream)) + var roll: int = stream.randi_range(1, int(db.settlement.roll_faces)) + return {"roll": roll, "crit": roll == int(db.settlement.crit_roll)} + + +# 掷点折扣读数:nat 最大面恒全额(暴击分支前置),其余查档(缺口 = 阈值 − 掷点) +func _roll_discount(roll: int, threshold: int) -> int: + if roll == int(db.settlement.crit_roll): + return 100 + return DamageMath.roll_discount_percent(roll, threshold, db.settlement.roll_tier_bounds, db.settlement.roll_tier_discounts) + + +# 防守方护甲等级读数(数值轴——与 armor_type 克制属性轴严格分离):敌人 = spawn 期行值快照; +# 主角 = 表值基准 + ac 型效果加算(ac_up 层数×幅度——不入任何池,无 clamp) +func _defender_ac(unit: Dictionary) -> int: + if bool(unit.get("is_hero", false)): + var total: int = int(db.hero.armor_class) + for eff in _status_effects.query_active("hero"): + var row: Dictionary = db.status_effect_row(eff.effect_id) + if not row.is_empty() and String(row.unit) == "ac": + total += int(eff.effective_value) + return total + return int(unit.get("armor_class", 0)) + + # F2 乘区:池内加算 → clamp [-100,+100] → 池间乘算 → floor(单一取整规则)——算术归 DamageMath(O3) func _resolve_damage(d_raw: int) -> int: var p_hero := _clamped_pool("hero_defense") @@ -516,7 +569,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", "light"))}) + 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")), "armor_class": int(u.get("armor_class", 0))}) 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}) @@ -543,7 +596,7 @@ func _emit_settlement_snapshot(verdict: String) -> void: "verdict": verdict, "unit_count": enemies.size(), "spawn_count": _enemy_system.all_enemies().size(), # 本战斗实例化总数(含死亡残位)——unit_count=0 时区分「首轮全灭」vs「空场」(playtest-2026-09-12 报告 Bug#1 误读教训) - "hero": {"hp": int(_enemy_system.hero_unit().get("hp", 0)), "max_hp": int(_enemy_system.hero_unit().get("max_hp", 0)), "atk": int(_enemy_system.hero_unit().get("atk", 0))}, + "hero": {"hp": int(_enemy_system.hero_unit().get("hp", 0)), "max_hp": int(_enemy_system.hero_unit().get("max_hp", 0)), "atk": int(_enemy_system.hero_unit().get("atk", 0)), "armor_class": _defender_ac(_enemy_system.hero_unit())}, "economy": {"phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "refresh_used": _refresh_used_this_round}, "board": board_stats, "enemies": enemies, @@ -615,8 +668,8 @@ func _battle_over(result: String) -> void: # ---- 呈现状态面(E1 封装收口 2026-09-13):呈现层 / headless 自动玩家的唯一读取出口—— # 协作对象(_enemy_system/_status_effects/_reward_economy)私有化后,跨层读取一律经本快照或下列命名面(diagnostic_snapshot 同款模式, # 禁再链式穿透 settlement.enemy_system.hero 一类路径)。载荷 schema 成文(code-conventions 2.1): -# round / battle_result / phase / ap / rap / can_request_refresh / hero{hp,max_hp,atk,alive} / -# enemies[{order,display_name,hp,max_hp,enemy_id,armor_type,beat,ring_size,ring_index,ambush_layers,alive}] / +# round / battle_result / phase / ap / rap / can_request_refresh / hero{hp,max_hp,atk,alive,armor_class} / +# enemies[{order,display_name,hp,max_hp,enemy_id,armor_type,armor_class,beat,ring_size,ring_index,ambush_layers,alive}] / # ambush_layers / hero_effects[](增益状态 query_active 投影) func presentation_snapshot() -> Dictionary: var enemies: Array = [] @@ -632,6 +685,7 @@ func presentation_snapshot() -> Dictionary: "hp": int(u.hp), "max_hp": int(u.max_hp), "enemy_id": String(u.enemy_id), "armor_type": String(u.get("armor_type", "light")), + "armor_class": int(u.get("armor_class", 0)), "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), @@ -641,7 +695,7 @@ func presentation_snapshot() -> Dictionary: var hero := {} var h: Dictionary = _enemy_system.hero_unit() if not h.is_empty(): - hero = {"hp": int(h.hp), "max_hp": int(h.max_hp), "atk": int(h.atk), "alive": bool(h.get("alive", false))} + hero = {"hp": int(h.hp), "max_hp": int(h.max_hp), "atk": int(h.atk), "alive": bool(h.get("alive", false)), "armor_class": _defender_ac(h)} return { "round": round_index, "battle_result": battle_result, diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/damage_math.gd b/prototypes/dungeon-sweeper-vertical-slice/core/damage_math.gd index 5f654b7d..ed5d05e8 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/core/damage_math.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/core/damage_math.gd @@ -3,6 +3,7 @@ # 实现 GDD:design/archive/gdd-2026-09-14/b1-settlement.md(系统 ID B1·战斗结算——registry 追溯锚,命名规范见 CLAUDE.md) # 战斗结算·战斗段伤害公式族(零状态纯算术宿主——规范 1.2「工具性纯函数挂明确宿主类」; # 自 battle_orchestrator 拆出 2026-09-13,封装审查 O3)。池读取与表值读取仍归 战斗结算,本类只做算术。 +# 护甲等级掷点批(2026-09-16):掷点查档与吃掷点分量算术收编本类(判定式参数化——表值零字面量纪律不变)。 class_name DamageMath extends RefCounted @@ -16,3 +17,27 @@ static func pool_damage(d_raw: int, p_hero: int, p_enemy: int) -> int: # 充能分量:充能点数(调用方已按奖励定义换算点数、并按源套单轮计数上限)× 克制 percent → floor static func charge_component(points: int, effectiveness_percent: int) -> int: return int(floor(float(points) * float(effectiveness_percent) / 100.0)) + + +# ---- 护甲等级掷点批(2026-09-16 用户拍板——判定式与档位参数全在 settlement.tres,本类只做算术)---- + +# 掷点折扣查档(纯函数):掷点 ≥ 阈值 → 100 全额;低掷按缺口(阈值 − 掷点)查升序边界落档, +# 返回档位值 percent(档数 = 边界数 + 1:缺口 ∈ [1, bounds[0]] → discounts[0],(bounds[i], bounds[i+1]] → +# discounts[i+1],> bounds 末值 → discounts 末位——档位不设零档,非归零)。nat 最大面的"恒全额"由调用方先行分支。 +static func roll_discount_percent(roll: int, threshold: int, bounds: Array, discounts: Array) -> int: + if roll >= threshold: + return 100 + var tier := 0 + for i in bounds.size(): + if threshold - roll > int(bounds[i]): + tier = i + 1 + return int(discounts[tier]) + + +# 吃掷点分量算术(纯函数):基线值 × 掷点折扣 ×(暴击时)暴击倍率 × 克制 percent → floor(单一取整; +# 乘法交换——折扣/暴击/克制次序无关,取整链保持现登记:每分量恰一次 floor)。暴击恒全额由 discount=100 承载。 +static func rolled_component(base_value: float, discount_percent: int, crit: bool, crit_multiplier: int, effectiveness_percent: int) -> int: + var v: float = base_value * float(discount_percent) / 100.0 + if crit: + v *= float(crit_multiplier) / 100.0 + return int(floor(v * float(effectiveness_percent) / 100.0)) diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/rng_service.gd b/prototypes/dungeon-sweeper-vertical-slice/core/rng_service.gd index e5001301..1eaf83ba 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/core/rng_service.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/core/rng_service.gd @@ -7,13 +7,14 @@ # 取号语义:全新实例首个 open_stream = scope 0(地图战斗资源 预推进术语的登记口径)。 extends Node -const STREAM_NAMES := ["board_gen", "reward_roll", "encounter"] # 2026-09-12 增 encounter(遭遇随机化批,用户拍板——registry rng_service 条目同步) +const STREAM_NAMES := ["board_gen", "reward_roll", "encounter", "armor_roll"] # 2026-09-12 增 encounter(遭遇随机化批);2026-09-16 增 armor_roll(护甲等级掷点批,用户拍板——回合内随机、逐次掷逐次取号,registry rng_service 条目同步) # 分段表达式书写(数据配置 R2 实现级规约——禁 ≥2^63 完整字面量) const GOLDEN_RATIO := (0x9E3779B9 << 32) | 0x7F4A7C15 # 0x9E3779B97F4A7C15 const SALT_BOARD_GEN := (0xA0761D64 << 32) | 0x78BD642F # 0xA0761D6478BD642F const SALT_REWARD_ROLL := (0xE7037ED1 << 32) | 0xA0B428DB # 0xE7037ED1A0B428DB const SALT_ENCOUNTER := (0x8EBC6AF0 << 32) | 0x9C88C6E3 # 0x8EBC6AF09C88C6E3(遭遇骰——registry stream_seed_derivation 权威值;旧值 0x5DEECE66D<<32 超 int64 被截断,黄金向量漂移) +const SALT_ARMOR_ROLL := (0xC2B2AE3D << 32) | 0x27D4EB2F # 0xC2B2AE3D27D4EB2F(护甲掷点流——互异奇常数 ≠ 乘数;黄金向量见冒烟/registry 同批登记) const OUT_MASK := 0x7FFFFFFFFFFFFFFF const INT64_MIN := -9223372036854775808 @@ -22,7 +23,7 @@ const INT64_MIN := -9223372036854775808 var _master_seed: int = -1 # -1 = 未播种(取数即致命,EC7) var master_seed: int: get: return _master_seed -var _counters := {"board_gen": 0, "reward_roll": 0, "encounter": 0} +var _counters := {"board_gen": 0, "reward_roll": 0, "encounter": 0, "armor_roll": 0} func _salt_for(stream: String) -> int: @@ -33,6 +34,8 @@ func _salt_for(stream: String) -> int: return SALT_REWARD_ROLL "encounter": return SALT_ENCOUNTER + "armor_roll": + return SALT_ARMOR_ROLL push_error("[RngService] 未知名流: %s" % stream) assert(false, "未知名流(流集合封闭,EC12)") return 0 @@ -54,7 +57,7 @@ func set_master_seed(seed_val: int) -> void: if seed_val < 0: seed_val = -seed_val push_warning("[RngService] master_seed 负输入 abs 归一化: %d" % seed_val) - assert(_counters["board_gen"] == 0 and _counters["reward_roll"] == 0 and _counters["encounter"] == 0, "冒险中途重播种会击穿同冒险无碰撞前提(仅许全部流计数器全零时调用)") + assert(_counters["board_gen"] == 0 and _counters["reward_roll"] == 0 and _counters["encounter"] == 0 and _counters["armor_roll"] == 0, "冒险中途重播种会击穿同冒险无碰撞前提(仅许全部流计数器全零时调用)") _master_seed = seed_val _counters["board_gen"] = 0 _counters["reward_roll"] = 0 @@ -65,6 +68,7 @@ func new_adventure(seed_val: int) -> void: _counters["board_gen"] = 0 _counters["reward_roll"] = 0 _counters["encounter"] = 0 + _counters["armor_roll"] = 0 set_master_seed(seed_val) diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/unit_container.gd b/prototypes/dungeon-sweeper-vertical-slice/core/unit_container.gd index 303d56f3..009afad0 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/core/unit_container.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/core/unit_container.gd @@ -68,6 +68,7 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override: "pattern": Array(row.pattern), # 行值快照(拷贝) "advantage_per_round_cap": int(row.advantage_per_round_cap), "armor_type": String(row.get("armor_type", "light")), # 护甲类型快照(克制矩阵消费面——战斗结算·战斗段;2026-09-15 命名统一批:light/ward/heavy) + "armor_class": int(row.get("armor_class", 0)), # 护甲等级快照(掷点判定阈值基数——战斗结算·战斗段;2026-09-16 护甲等级掷点批) "steps": 0, "alive": true, "is_hero": false, diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/enemy_table.gd b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/enemy_table.gd index 44637f82..b9b7afa2 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/enemy_table.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/enemy_table.gd @@ -82,6 +82,10 @@ func validate() -> Array: var at: String = String(row.get("armor_type", "light")) if not ARMOR_TYPES.has(at): errs.append({"category": "enum", "message": "%s armor_type 未知枚举: %s(域 {light, ward, heavy})" % [eid, at]}) + # armor_class(可省略——缺省 0 向后兼容;2026-09-16 护甲等级掷点批:防守方掷点判定阈值基数) + var ac: int = int(row.get("armor_class", 0)) + if ac < 0 or ac > 999: + errs.append({"category": "domain", "message": "%s armor_class 越域 [0,999]: %d" % [eid, ac]}) # 每行集恰一敌种(行集内 enemy_id 唯一) var rk := "%d_%d" % [L, D] if rowset_species.has(rk): diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/hero_table.gd b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/hero_table.gd index 141ec95b..22c4d87b 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/hero_table.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/hero_table.gd @@ -8,6 +8,7 @@ extends Resource @export var max_hp: int = 50 # [1,999] knob(provisional,溯源 = 原型锚) @export var atk: int = 6 # [1,999] knob——基础攻击(溯源 = 概念原型 HERO_BASE_ATK,provisional 归 难度曲线与Boss);消费面 = 战斗结算·战斗段先手攻击 +@export var armor_class: int = 10 # [0,999] knob——防守方护甲等级基准(2026-09-16 护甲等级掷点批,暂定 10 归 难度曲线与Boss);消费面 = 战斗结算掷点判定阈值基数(ac_up 效果加算其上) func validate() -> Array: @@ -16,4 +17,6 @@ func validate() -> Array: errs.append({"category": "domain", "message": "max_hp 越域 [1,999]: %d" % max_hp}) if atk < 1 or atk > 999: errs.append({"category": "domain", "message": "atk 越域 [1,999]: %d" % atk}) + if armor_class < 0 or armor_class > 999: + errs.append({"category": "domain", "message": "armor_class 越域 [0,999]: %d" % armor_class}) return errs diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/settlement_table.gd b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/settlement_table.gd index 1c10dcda..43556ec2 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/settlement_table.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/settlement_table.gd @@ -1,7 +1,7 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2) # Date: 2026-09-10 -# 数据配置 SettlementTable schema(战斗结算 回填值;单行全局,Rule 4 豁免)——八字段口径。 +# 数据配置 SettlementTable schema(战斗结算 回填值;单行全局,Rule 4 豁免)——八字段口径 + 克制/充能批 + 护甲等级掷点批。 # 三行致命 manifest:segorder_violation / poolmap_dangling / pperfect_oob。 class_name SettlementTable extends Resource @@ -21,6 +21,19 @@ extends Resource # 原 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}} +# ---- 护甲等级掷点批(2026-09-16 用户拍板,数字即规范——registry settlement_table.ac_roll/ac_discount_tiers 同批定稿)---- +# 判定式:掷 1d20,掷点 ≥ 防守方 AC + 攻击加成 → 全额;低掷按缺口(AC−掷点)查档打折,无 miss、无零收益档; +# nat 最大面(crit_roll)= 暴击:该次基础攻击分量 ×2 且恒全额;任何攻击落地伤害全局下限 damage_floor(≥1)。 +# 掷点只吃基础攻击分量(主角普通分量 / 敌方攻击拍与重击拍基线),穿刺/魔法充能与棋盘来源伤害不掷不折。 +# 微调旋钮 = 档位边界(roll_tier_bounds)与档位值(roll_tier_discounts),不动 AC 值(armor_class 归单位表)。 +@export var roll_faces: int = 20 # [2,1000] knob——掷点骰面(d20) +@export var attack_bonus: int = 0 # [0,999] 结构位——攻击者掷点加成,切片恒 0(元进度/武器附魔未来投值) +@export var roll_tier_bounds: Array = [] # 缺口档边界升序:缺口 ≥ bounds[i] 落入第 i 档([3,7] = 缺口1-3/4-7/8+ 三档) +@export var roll_tier_discounts: Array = [] # 档位值 percent 降序(与 bounds 同长;[75,50,25] = ×0.75/×0.5/×0.25) +@export var crit_roll: int = 20 # [1,roll_faces] knob——nat 最大面 = 暴击触发掷点(恒全额) +@export var crit_multiplier: int = 200 # [100,999] percent knob——暴击倍率(200 = ×2,只乘该次基础攻击分量) +@export var damage_floor: int = 1 # [1,999] knob——任何攻击落地伤害全局下限("被减伤归零 v=0"行语义随批退役) +@export var armor_roll_stream: String = "armor_roll" # 掷点封闭流名(rng_service STREAM_NAMES 成员;逐次掷逐次取号,可逐位回放) const ARMOR_KEYS := ["light", "ward", "heavy"] const ATTACK_TYPE_KEYS := ["normal", "pierce", "magic"] @@ -42,6 +55,30 @@ 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 roll_faces < 2 or roll_faces > 1000: + errs.append({"category": "domain", "message": "roll_faces 越域 [2,1000]: %d" % roll_faces}) + if crit_roll < 1 or crit_roll > roll_faces: + errs.append({"category": "domain", "message": "crit_roll 越域 [1,roll_faces(%d)]: %d" % [roll_faces, crit_roll]}) + if crit_multiplier < 100 or crit_multiplier > 999: + errs.append({"category": "domain", "message": "crit_multiplier 越域 [100,999]: %d" % crit_multiplier}) + if damage_floor < 1 or damage_floor > 999: + errs.append({"category": "domain", "message": "damage_floor 越域 [1,999]: %d" % damage_floor}) + if attack_bonus < 0 or attack_bonus > 999: + errs.append({"category": "domain", "message": "attack_bonus 越域 [0,999]: %d" % attack_bonus}) + if String(armor_roll_stream) == "": + errs.append({"category": "domain", "message": "armor_roll_stream 空"}) + # 档位表形状:bounds 升序且 ∈ [1, faces-1];discounts 降序且 ∈ [1,99](无零档——非归零); + # 档数 = 边界数 + 1(缺口 [1,b0] → discounts[0],(b_i,b_{i+1}] → discounts[i+1],> b 末值 → 末位档) + if roll_tier_bounds.is_empty() or roll_tier_discounts.size() != roll_tier_bounds.size() + 1: + errs.append({"category": "cross_field", "message": "roll_tier_bounds/discounts 形状违例(档数须 = 边界数 + 1): %d 边界 vs %d 档" % [roll_tier_bounds.size(), roll_tier_discounts.size()]}) + else: + for i in roll_tier_bounds.size(): + var b: int = int(roll_tier_bounds[i]) + if b < 1 or b >= roll_faces or (i > 0 and b <= int(roll_tier_bounds[i - 1])): + errs.append({"category": "cross_field", "message": "roll_tier_bounds[%d] 越域/非升序: %d" % [i, b]}) + var d: int = int(roll_tier_discounts[i]) + if d < 1 or d > 99 or (i > 0 and d >= int(roll_tier_discounts[i - 1])): + errs.append({"category": "cross_field", "message": "roll_tier_discounts[%d] 越域/非降序(档位不设零档): %d" % [i, d]}) # 充能奖励定义域校验(触发源 → {attack_type, per_unit, cap}) for src in charge_grants.keys(): var g: Dictionary = charge_grants[src] diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/status_effect_table.gd b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/status_effect_table.gd index 721a2d93..2a964102 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/status_effect_table.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/status_effect_table.gd @@ -9,7 +9,7 @@ extends Resource @export var rows: Array = [] # Array[Dictionary] const TARGET_SIDES := ["hero", "enemy"] -const UNITS := ["percent", "flat"] +const UNITS := ["percent", "flat", "ac"] # 2026-09-16 护甲等级掷点批增 ac——AC 阈值轴效果(ac_up),非池百分比非定点值 const STACK_POLICIES := ["stack", "refresh", "independent"] @@ -37,11 +37,13 @@ func validate() -> Array: if not STACK_POLICIES.has(row.get("stack_policy", "")): errs.append({"category": "enum", "message": "%s stack_policy 出域" % eid}) var m: int = int(row.get("magnitude", 0)) - # 跨字段:percent ∈ [-100,100] 且 ≠0 / flat ≥ 1 + # 跨字段:percent ∈ [-100,100] 且 ≠0 / flat ≥ 1 / ac ∈ [-20,20] 且 ≠0(AC 阈值轴——幅度域独立,防呆上限 20) if unit == "percent" and (m == 0 or m < -100 or m > 100): errs.append({"category": "cross_field", "message": "%s percent magnitude 域错: %d" % [eid, m]}) if unit == "flat" and m < 1: errs.append({"category": "cross_field", "message": "%s flat magnitude 须 ≥1: %d" % [eid, m]}) + if unit == "ac" and (m == 0 or m < -20 or m > 20): + errs.append({"category": "cross_field", "message": "%s ac magnitude 域错([-20,20] 且 ≠0): %d" % [eid, m]}) var dur: int = int(row.get("duration_settlements", 0)) if dur < 1 or dur > 99: errs.append({"category": "domain", "message": "%s duration_settlements 越域 [1,99]: %d" % [eid, dur]}) diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/tables/enemy.tres b/prototypes/dungeon-sweeper-vertical-slice/data/tables/enemy.tres index bb3532ef..88f68a83 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/data/tables/enemy.tres +++ b/prototypes/dungeon-sweeper-vertical-slice/data/tables/enemy.tres @@ -18,7 +18,8 @@ rows = [{ "heavy_multiplier": 2, "pattern": ["attack"], "advantage_per_round_cap": 4, -"armor_type": "light" +"armor_type": "light", +"armor_class": 0 }, { "level_index": 1, "node_depth": 1, @@ -33,7 +34,8 @@ rows = [{ "heavy_multiplier": 2, "pattern": ["attack"], "advantage_per_round_cap": 4, -"armor_type": "light" +"armor_type": "light", +"armor_class": 0 }, { "level_index": 1, "node_depth": 2, @@ -47,7 +49,8 @@ rows = [{ "heavy_multiplier": 2, "pattern": ["attack", "charge", "heavy"], "advantage_per_round_cap": 4, -"armor_type": "ward" +"armor_type": "ward", +"armor_class": 0 }, { "level_index": 1, "node_depth": 3, @@ -62,7 +65,8 @@ rows = [{ "heavy_multiplier": 2, "pattern": ["attack", "charge", "heavy"], "advantage_per_round_cap": 4, -"armor_type": "ward" +"armor_type": "ward", +"armor_class": 0 }, { "level_index": 1, "node_depth": 4, @@ -76,7 +80,8 @@ rows = [{ "heavy_multiplier": 2, "pattern": ["attack", "attack", "charge", "heavy"], "advantage_per_round_cap": 4, -"armor_type": "heavy" +"armor_type": "heavy", +"armor_class": 0 }, { "level_index": 2, "node_depth": 0, @@ -90,7 +95,8 @@ rows = [{ "heavy_multiplier": 2, "pattern": ["attack", "charge", "heavy"], "advantage_per_round_cap": 4, -"armor_type": "ward" +"armor_type": "ward", +"armor_class": 0 }] ; 切片调参登记 v3(2026-09-12 试玩批次 1 触发——F7 流程;C3 GDD F5 对照): @@ -112,3 +118,7 @@ rows = [{ ; 敌方攻击区间批(2026-09-14 用户拍板②第二步——"上限掺运气"敌方侧:atk 上阶/下阶,中点恒等行值; ; roll 时机 = 遭遇骰同流〔encounter〕spawn 期整战持久,每拍抖动否决〔数字可读性锚〕;B2 零随机纪律保持 ; ——收 override 即确定值。宽度 ±3 provisional 归 C3 测量迭代;验收 = 完美策略档 0 加成通关 ≤40%。 + +; 护甲等级掷点批(2026-09-16 用户拍板):armor_class 字段入列(缺省 0 向后兼容)——防守方掷点判定阈值基数, +; 与 armor_type(克制属性轴)两概念严格分离、不做映射。首关六行暂定 0(正式行值归怪物/敌人系统设定批—— +; 登记待办:第一关怪物 AC 相对较低,值待怪物设定投值;投值所有权归 难度曲线与Boss)。 diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/tables/hero.tres b/prototypes/dungeon-sweeper-vertical-slice/data/tables/hero.tres index 33310ef4..c0a75852 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/data/tables/hero.tres +++ b/prototypes/dungeon-sweeper-vertical-slice/data/tables/hero.tres @@ -6,3 +6,7 @@ script = ExtResource("1") max_hp = 50 atk = 6 +armor_class = 10 + +; 护甲等级掷点批(2026-09-16 用户拍板):armor_class = 10(暂定基准,归 难度曲线与Boss; +; EV 参考数按判定式复算 = 期望减免 21.25%);ac_up 增益 +1 AC/层(×2 层 = AC+2)在其上加算。 diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/tables/reward.tres b/prototypes/dungeon-sweeper-vertical-slice/data/tables/reward.tres index 662f57fd..d58a8131 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/data/tables/reward.tres +++ b/prototypes/dungeon-sweeper-vertical-slice/data/tables/reward.tres @@ -15,11 +15,11 @@ rows = [{ "pool_tag": "cell", "tier": 2 }, { -"reward_id": "defense_up", +"reward_id": "ac_up", "cell_type": "defense", "polarity": "positive", -"effect_id": "defense_up", -"display_name": "减伤", +"effect_id": "ac_up", +"display_name": "护甲强化", "power": 0, "weight": 16, "pool_tag": "cell", @@ -130,3 +130,6 @@ rows = [{ ; 区间行(伤害量值区间批 2026-09-14):heal_minor [2,6] · clear_blast [25,55]——中点锚 power 恒等期望当量, ; roll 时机 = 身份预分配批内追加取号(既有取号序零扰动);spark_aoe 定值行无区间字段。 ; 减伤墙调参批(2026-09-13):defense_up 权重 24→16——攻击成为池内最高行(历史注记)。 +; 护甲等级掷点批(2026-09-16 用户拍板):defense 型正向行换装——reward_id/effect_id defense_up → ac_up +; (显示名暂定"护甲强化",AC +1/层 ×2 层);cell_type defense / 权重 16 / tier 2 零改动(类型频次合计 79 不变); +; 百分比减伤行无双轨退役——status_effects.tres defense_up 行同批删除。 diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/tables/settlement.tres b/prototypes/dungeon-sweeper-vertical-slice/data/tables/settlement.tres index 8527c810..548819a2 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/data/tables/settlement.tres +++ b/prototypes/dungeon-sweeper-vertical-slice/data/tables/settlement.tres @@ -18,7 +18,6 @@ source_label_map = { } effect_pool_map = { "attack_up": "hero_offense", -"defense_up": "hero_defense", "weaken": "enemy_output", "ambush": "flat_attach", "enrage": "enemy_output", @@ -57,6 +56,14 @@ armor_effectiveness = { "heavy": 150 } } +roll_faces = 20 +attack_bonus = 0 +roll_tier_bounds = [3, 7] +roll_tier_discounts = [75, 50, 25] +crit_roll = 20 +crit_multiplier = 200 +damage_floor = 1 +armor_roll_stream = "armor_roll" ; 攻击类型 × 护甲克制批(2026-09-12 用户拍板结构;2026-09-15 命名统一批 + 抽象分离批,数值 provisional 待用户/C3 调参): ; 主角攻击 = 普通(atk×(1+attack_up),对全部护甲 100%)+ 穿刺 + 魔法(后两者由充能供给),三分量各自 floor 后相加; @@ -72,3 +79,12 @@ armor_effectiveness = { ; sapped(我方乏力 -25%,hero_offense 池)= attack/defense/weaken 三型负向字典行 ; (极性路由兑付:挖雷格 / 标错安全格按该格 cell_type 取负向行;heal 负向 = 主角直伤不经状态、blast 负向 = ambush+2 层)。 ; 减伤墙调参批(2026-09-13):pool_clamp 域 [-100,+100] 结构冻结不动;减伤杠杆走 status_effects.tres(批注在该表)。 +; 护甲等级掷点批(2026-09-16 用户拍板,数字即规范——战斗结算 GDD 护甲等级掷点节定稿同批): +; roll_faces=20 / roll_tier_bounds=[3,7] / roll_tier_discounts=[75,50,25] / crit_roll=20 / crit_multiplier=200 / +; damage_floor=1 / armor_roll_stream=armor_roll;attack_bonus=0 结构位(元进度/武器附魔未来投值)。 +; 判定式:掷点 ≥ 防守方 AC + attack_bonus → 全额;缺口 1-3 → ×0.75、4-7 → ×0.5、8+ → ×0.25;无 miss 无零收益档; +; nat20 = 暴击 ×2 恒全额(双方同规);任何攻击落地伤害 ≥ damage_floor(全局 max(1,·))。 +; 掷点只吃基础攻击分量(主角普通分量 / 敌方攻击拍与重击拍基线);穿刺/魔法充能与棋盘来源伤害必中全额。 +; EV 参考数(按判定式复算):AC10 期望减免 21.25% / AC11 25% / AC12 28.75%;微调旋钮 = bounds/discounts,不动 AC 值。 +; effect_pool_map 同批删 defense_up 键(减伤百分比行整体退役、无双轨)——ac_up 是 AC 阈值轴效果,不入任何池; +; off_guard(hero_defense 池)保留不动,池结构与乘区公式零改动。 diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/tables/status_effects.tres b/prototypes/dungeon-sweeper-vertical-slice/data/tables/status_effects.tres index b1d78109..f8af2bb1 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/data/tables/status_effects.tres +++ b/prototypes/dungeon-sweeper-vertical-slice/data/tables/status_effects.tres @@ -14,11 +14,11 @@ rows = [{ "stack_policy": "stack", "max_stack": 2 }, { -"effect_id": "defense_up", -"display_name": "减伤", +"effect_id": "ac_up", +"display_name": "护甲强化", "target_side": "hero", -"magnitude": -30, -"unit": "percent", +"magnitude": 1, +"unit": "ac", "duration_settlements": 1, "stack_policy": "stack", "max_stack": 2 @@ -76,3 +76,9 @@ rows = [{ ; 格面资源批(2026-09-15):新增 sapped(乏力——weaken 型负向,hero_offense 池,attack_up 镜像; ; 进 effect_pool_map 同批登记)——状态表六行→七行;enrage/off_guard 语义 = attack/defense 型负向 ; (标错安全格 / 踩到雷格经极性路由兑付,不再绑"标错反噬"单一来源)。 +; 护甲等级掷点批(2026-09-16 用户拍板——百分比减伤增益整体换装为 AC,无双轨): +; defense_up(减伤 −30 percent ×2 层)整行退役(上方 2026-09-13 减伤墙批注随之留档)—— +; "叠满池触底 ×0"免伤墙的结构面随 AC 掷点从根杜绝;新增 ac_up(护甲强化 +1 ac ×2 层)—— +; unit=ac 新量纲(AC 阈值轴效果,不入 effect_pool_map 任何池),消费面 = 战斗结算掷点判定 +; (防守方 AC = 主角表值 armor_class + Σac 型效果层数×幅度);off_guard(失衡 +25 percent,hero_defense 池) +; 保留不动,其余六行零改动——状态表仍七行(一行换装)。 diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/hud_presenter.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/hud_presenter.gd index 7590b3cf..8b3832d6 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/hud_presenter.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/hud_presenter.gd @@ -48,7 +48,7 @@ static func _refresh_hero(stage: BattleStageView, info: HeroInfoPanelView, s: Di var mod_text := "" if mod_total != 0: mod_text = "(%+d%% %s)" % [mod_total, "·".join(sources)] - stage.hero_card.set_data(hp, max_hp, atk, mod_text, _hero_status_summary(s.hero_effects)) + stage.hero_card.set_data(hp, max_hp, atk, mod_text, _hero_status_summary(s.hero_effects), int(hero.get("armor_class", -1))) info.set_hero(hp, max_hp) info.set_attack(atk, mod_text) @@ -80,7 +80,7 @@ static func _refresh_out_of_battle(stage: BattleStageView, info: HeroInfoPanelVi if runner != null: info.set_hero(runner.hero_hp, runner.effective_max_hp()) info.set_attack(runner.effective_atk(), "(关卡推进中)") - stage.hero_card.set_data(runner.hero_hp, runner.effective_max_hp(), runner.effective_atk(), "(关卡推进中)", "") + stage.hero_card.set_data(runner.hero_hp, runner.effective_max_hp(), runner.effective_atk(), "(关卡推进中)", "", int(db.hero.armor_class)) stage.clear_enemies() # 节点间隙清残位敌卡(死亡瞬间的灰化呈现已在结算行驱动完成) info.set_rap(0, int(db.action_economy.rap_refresh_cost)) # battle_over RAP 清零反馈(行动经济 义务) diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd index ea1af09e..14b4a08e 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd @@ -122,7 +122,7 @@ func _build_ui() -> void: _register_modal(m) _tune_sim = TuneSimController.new(db, tune_panel, int(db.hero.max_hp)) - var start: Dictionary = UiAssembler.build_start_overlay(self) + var start: Dictionary = UiAssembler.build_start_overlay(self, db) _start_overlay = start.overlay _seed_edit = start.seed_edit start.lvbtn.pressed.connect(_start_full_level) diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/ui_assembler.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/ui_assembler.gd index 5b80026e..4cef8d1f 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/ui_assembler.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/ui_assembler.gd @@ -83,7 +83,7 @@ static func build_overlays(host: Control, db, base_atk: int, base_hp: int) -> Di # 开始覆盖层(返回 {overlay, box, seed_edit, lvbtn, tunebtn, dbgbtn}——按钮接线归组合根; # 单场遭遇用户入口已删(2026-09-13 用户裁决——单场原语仅探针经 debug_session.start_single 使用)) -static func build_start_overlay(host: Control) -> Dictionary: +static func build_start_overlay(host: Control, db = null) -> Dictionary: var overlay := overlay_bg(host, Color8(28, 22, 18)) var box := overlay_box(overlay) var title := UiKit.label("Dungeon Sweeper", 34, true) @@ -109,7 +109,10 @@ static func build_start_overlay(host: Control) -> Dictionary: var dbgbtn := UiKit.button("调试 · 连续关卡两层(HP 关内延续)") box.add_child(dbgbtn) - var help := UiKit.label("玩法:每轮 8 AP——挖开(1AP)/插旗(1AP)/移动(免费)/连锁(免费)。\n每个非封锁格都藏一种格面资源(增伤/减伤/回血/爆裂/削弱)。开安全格、标中雷 = 正向兑付;\n开雷格、标错安全格 = 负向兑付——全部在轮末结算逐行兑现。\n停手:剩余 AP 转存 RAP;弃图刷新:RAP−8 当场换图继续挖(每轮剩 2 次)。\n敌人全灭胜、主角倒下负。悬停可看三态光标与可挖域微光。", 13, false) + # 玩法帮助文案(表值驱动——2026-09-16 AP 压速批遗留硬编码 8 随手修正;护甲等级掷点批同步"减伤"→"护甲强化") + var help_text := "玩法:每轮 %d AP——挖开(1AP)/插旗(1AP)/移动(免费)/连锁(免费)。\n每个非封锁格都藏一种格面资源(增伤/护甲强化/回血/爆裂/削弱)。开安全格、标中雷 = 正向兑付;\n开雷格、标错安全格 = 负向兑付——全部在轮末结算逐行兑现;吃掷点的基础攻击按护甲等级判定,无 miss、最低保底 1 点。\n停手:剩余 AP 转存 RAP;弃图刷新:RAP−%d 当场换图继续挖(每轮剩 %d 次)。\n敌人全灭胜、主角倒下负。悬停可看三态光标与可挖域微光。" % [ + int(db.action_economy.ap_per_round), int(db.action_economy.rap_refresh_cost), int(db.action_economy.refresh_max_per_round)] + var help := UiKit.label(help_text, 13, false) help.add_theme_color_override("font_color", Color8(178, 166, 146)) box.add_child(help) diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/battle_log_view.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/battle_log_view.gd index faa36b93..5d113a94 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/battle_log_view.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/battle_log_view.gd @@ -5,7 +5,7 @@ # highlight 金框〔flag_hit/clear_reward_h2/perfect_broken〕、击杀标记)。 # Date: 2026-09-12(基础 = 2026-09-11 13 类行协议格式化版;2026-09-15 格面资源批 15→10 类: # 删 item / consumed_ineffective / use_rejected / window_open / window_skip 五类——可选类道具与窗口下线, -# 战斗结算 行协议清单同步) +# 战斗结算 行协议清单同步;2026-09-16 护甲等级掷点批:伤害行补掷点读数/档位随行标注 + nat20 金框高亮) # 高内聚边界:战斗结算 步骤流 → 玩家文案的全部格式化规则内聚于此;来源文案映射来自 SettlementTable。 class_name BattleLogView extends PanelContainer @@ -176,9 +176,20 @@ func _format_row(row: Dictionary, agg_count := -1) -> String: elif row.get("pierce", null) != null: # 主角攻击三分量拆行(攻击类型×护甲克制批——P2 归因:这刀里哪段是穿刺/魔法挣的) extra = "(普通 %d + 穿刺 %d + 魔法 %d)" % [int(row.normal), int(row.pierce), int(row.magic)] + # 护甲等级掷点留档(2026-09-16 掷点批——掷点读数与档位随行可读;吃掷点分量恒带,棋盘来源行不带) + var roll_txt := "" + if row.get("roll", null) != null: + roll_txt = "〔掷 %d vs 护甲 %d" % [int(row.roll), int(row.ac)] + if bool(row.get("crit", false)): + roll_txt += " · 暴击×%d" % int(_db.settlement.crit_multiplier) + elif int(row.tier) < 100: + roll_txt += " · ×%d%%" % int(row.tier) + roll_txt += "〕" var kill := " ⚔击杀!" if row.get("killed", false) else "" - var body := "%s -%d%s%s%s%s" % [name_, v, extra, tail, kill, suffix] + var body := "%s -%d%s%s%s%s%s" % [name_, v, extra, roll_txt, tail, kill, suffix] var body_col := "#e07050" if row.get("killed", false) else ("#9a8a72" if v == 0 else "#e0a050") + if bool(row.get("crit", false)): + return prefix + _gold_frame(body) # nat 最大面暴击 = 金框高亮行 return prefix + "[color=%s]%s[/color]" % [body_col, body] "heal": return prefix + "[color=#7cc47c]%s +%d%s%s[/color]" % [name_, v, tail, suffix] diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/enemy_card_view.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/enemy_card_view.gd index 2a1475af..8d57bd8f 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/enemy_card_view.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/enemy_card_view.gd @@ -22,6 +22,7 @@ var _intent_badge: Label var _ring: Label var _status_label: Label var _armor := "light" # 护甲类型快照(set_data 写入、set_intent 渲染——环点行常驻标签) +var _armor_class := 0 # 护甲等级快照(2026-09-16 护甲等级掷点批——环点行常驻;与护甲类型两轴分立) var _dead := false @@ -72,6 +73,7 @@ func set_data(d: Dictionary) -> void: _hp_bar.value = int(d.get("hp", 0)) _set_order(int(d.get("order", 1))) _armor = String(d.get("armor_type", "light")) + _armor_class = int(d.get("armor_class", 0)) 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 "" @@ -101,13 +103,14 @@ func set_intent(beat: String, ring_size: int, ring_index: int) -> void: else: _intent_badge.text = "意图:" + label + ("▼" if beat == "heavy" else "") # 环点 = 拍数直觉编码(● 已到拍 / ○ 未到)+ 护甲类型标签(克制对策的可读前提——恒常驻非悬停) + # + 护甲等级 AC(掷点折扣阈值——2026-09-16 护甲等级掷点批;与护甲类型两轴分立可读) if ring_size > 0: var dots := "" for i in ring_size: dots += "●" if i == ring_index else "○" - _ring.text = "%s · 环 %s(%d 拍)" % [_armor_tag(_armor), dots, ring_size] + _ring.text = "%s · AC %d · 环 %s(%d 拍)" % [_armor_tag(_armor), _armor_class, dots, ring_size] else: - _ring.text = _armor_tag(_armor) + _ring.text = "%s · AC %d" % [_armor_tag(_armor), _armor_class] func _armor_tag(armor: String) -> String: diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/hero_card_view.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/hero_card_view.gd index 1907b1f2..2f18a079 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/hero_card_view.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/hero_card_view.gd @@ -12,6 +12,7 @@ var _portrait: Label var _hp_bar: ProgressBar var _hp_label: Label var _atk_label: Label +var _ac_label: Label var _status_label: Label @@ -49,15 +50,18 @@ func _init() -> void: col.add_child(_hp_label) _atk_label = UiKit.label("攻击 —", 13, false) col.add_child(_atk_label) + _ac_label = UiKit.label("护甲 —", 13, false) + col.add_child(_ac_label) _status_label = _fixed_label(46) # 缺陷①约束:固定槽高 + clip——状态文案切换零回流 col.add_child(_status_label) -func set_data(hp: int, max_hp: int, atk: int, atk_mod: String, status_summary: String) -> void: +func set_data(hp: int, max_hp: int, atk: int, atk_mod: String, status_summary: String, ac: int = -1) -> void: _hp_bar.max_value = maxi(1, max_hp) _hp_bar.value = hp _hp_label.text = "HP %d / %d" % [hp, max_hp] _atk_label.text = "攻 %d %s" % [atk, atk_mod] + _ac_label.text = "护甲 %d" % ac if ac >= 0 else "护甲 —" _status_label.text = status_summary @@ -65,6 +69,7 @@ func set_idle() -> void: _hp_bar.value = 0 _hp_label.text = "HP —" _atk_label.text = "攻击 —" + _ac_label.text = "护甲 —" _status_label.text = "" diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/status_slot_row.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/status_slot_row.gd index 4684bf46..191aab87 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/status_slot_row.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/status_slot_row.gd @@ -11,11 +11,12 @@ extends HBoxContainer const MAX_SLOTS := 4 # 极性/语义映射(数据源 = SettlementTable.effect_pool_map 效果语义 + StatusEffectTable display_name): # 益 = ▲/◆/▼(金/蓝族);害 = ⚡/⚔/✕/☾(旗朱族)——形状+色相双通道,文字名常驻第三通道。 -# 状态表七行全映射(attack_up/defense_up/weaken/ambush/enrage/off_guard/sapped)——HUD框架 图标键契约 -# "一键一枚专属纹样,查不到的键立即断言 + ? 占位 + 诊断"(2026-09-15 格面资源批补 sapped 键)。 +# 状态表七行全映射(attack_up/ac_up/weaken/ambush/enrage/off_guard/sapped)——HUD框架 图标键契约 +# "一键一枚专属纹样,查不到的键立即断言 + ? 占位 + 诊断"(2026-09-15 格面资源批补 sapped 键; +# 2026-09-16 护甲等级掷点批:defense_up → ac_up 换装)。 const EFFECT_ICON := { "attack_up": ["▲", Color8(176, 122, 20), "益"], - "defense_up": ["◆", Color8(46, 110, 142), "益"], + "ac_up": ["◇", Color8(46, 110, 142), "益"], "weaken": ["▼", Color8(46, 110, 142), "益"], "ambush": ["⚡", Color8(198, 61, 34), "害"], "enrage": ["⚔", Color8(198, 61, 34), "害"], @@ -24,7 +25,7 @@ const EFFECT_ICON := { } const EFFECT_SEMANTIC := { "attack_up": "主角攻击加成(hero_offense 池加算)", - "defense_up": "受到伤害减免(hero_defense 池加算)", + "ac_up": "护甲等级 +1/层(掷点折扣判定阈值——AC 数值轴,受击低掷按档位打折)", "weaken": "敌方输出削弱(enemy_output 池乘算)", "ambush": "敌方全体攻击附加(flat_attach——blast 型负向,战斗段共享不消耗)", "enrage": "敌方输出加成(enemy_output 池加算——attack 型负向)", diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/damage_probe.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/damage_probe.gd index af8baaa9..b7755de6 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/damage_probe.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/damage_probe.gd @@ -1,12 +1,12 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # Date: 2026-09-13(playtest-2026-09-12 报告 Top1——零承伤根因分割探针) -# 逐结算行打印 beat / atk.total / hero_defense 池值 / d_tick,分割报告悬置的两个嫌疑: -# H1 defense_up 叠满墙(设计语义——hero_defense 池触 clamp 下限 = 全额免伤) -# H2 Boss/敌攻击链缺陷(bug——fresh 池下攻击拍仍零兑现) -# 快照盲区说明:settlement_snapshot 取在衰减段之后,时效 1 的 defense_up 当轮授予当轮衰减—— -# 真人局 JSONL buffs[] 恒空 ≠ 池为空,本探针改从结算行(授予/续存 effect 行的 pool_value)重建战斗段时点池值。 -# 观测面:全部读战斗结算步骤行 / 表值 / 呈现快照(零核心穿透);池归属/墙深/池界全读表(数据配置 Rule 1——零数值字面量)。 -# 模式(-- 后参数):boss [seed](默认 42:Run A 零挖掘 fresh 池 + Run B 自动挖掘墙行为) +# 2026-09-16 改掷点口径(护甲等级掷点批):defense_up 墙已随 AC 换装退役—— +# H1 旧 defense_up 叠满墙失义(hero_defense 池现仅 off_guard 正向行,×0 墙结构面消失) +# H2 Boss/敌攻击链健康:fresh 池下非蓄力拍 d_tick = max(下限, 池乘区(掷点档位基线 + 伏击附加)) +# 快照盲区说明:settlement_snapshot 取在衰减段之后,时效 1 的 ac_up 当轮授予当轮衰减—— +# 真人局 JSONL buffs[] 恒空 ≠ 状态为空,本探针改从结算行(授予/续存 effect 行的 pool_value)重建战斗段时点池值。 +# 观测面:全部读战斗结算步骤行 / 表值 / 呈现快照(零核心穿透);池归属/档位/下限全读表(数据配置 Rule 1——零数值字面量)。 +# 模式(-- 后参数):boss [seed](默认 42:Run A 零挖掘 fresh 池 + Run B 自动挖掘授予行为) # level [seed](全关遭遇回放 + 多种子 spawn 扫描——报告 Bug#1 证据面) # 运行:Godot_console.exe --headless --path . --script res://smoke/damage_probe.gd -- boss 42 # Godot_console.exe --headless --path . --script res://smoke/damage_probe.gd -- level 42 @@ -53,31 +53,35 @@ func _initialize() -> void: func _probe_boss(db, seed_val: int) -> void: var math = load("res://core/damage_math.gd") - var def_row: Dictionary = db.status_effect_row("defense_up") - var wall_floor: int = int(def_row.get("magnitude", 0)) * int(def_row.get("max_stack", 1)) var clamp_lo: int = int(db.settlement.pool_clamp_lo) var clamp_hi: int = int(db.settlement.pool_clamp_hi) - print("表值面:defense_up magnitude=%d × max_stack=%d = 墙最深池 %d · pool_clamp [%d, %d]" % [ - int(def_row.get("magnitude", 0)), int(def_row.get("max_stack", 1)), wall_floor, clamp_lo, clamp_hi]) + var hero_def_negative := 0 + for eff_id in db.settlement.effect_pool_map.keys(): + if String(db.settlement.effect_pool_map[eff_id]) != "hero_defense": + continue + var row: Dictionary = db.status_effect_row(eff_id) + if not row.is_empty() and String(row.get("unit", "")) == "percent": + hero_def_negative = mini(hero_def_negative, int(row.get("magnitude", 0))) + print("表值面:hero_defense 池最负 percent 行 %d · pool_clamp [%d, %d] · damage_floor %d · AC 基准 %d(+ac_up×%d 层)" % [ + hero_def_negative, clamp_lo, clamp_hi, int(db.settlement.damage_floor), int(db.hero.armor_class), int(db.status_effect_row("ac_up").get("max_stack", 0))]) - print("--- Run A:fresh 池(零挖掘停手轮——无授予、无墙、攻击链裸验)---") + print("--- Run A:fresh 池(零挖掘停手轮——无授予,攻击链裸验)---") var run_a := _run_boss(db, seed_val, false) _print_rounds(db, math, run_a, true) var a_ok := _assert_rounds(db, math, run_a, true) - check(a_ok.fresh_and_full, "A1 攻击链健康(fresh 池):非蓄力拍 d_tick = atk.total 全额兑现(×%d 拍)——H2 攻击链缺陷否证" % a_ok.n_attack) - check(a_ok.arith_ok, "A2 池算术完整:全部非蓄力拍 d_tick == DamageMath.pool_damage(×%d 拍)" % a_ok.n_attack) + check(a_ok.fresh_and_full, "A1 攻击链健康(fresh 池·掷点口径):非蓄力拍 d_tick = max(下限, 掷点档位基线+附加)(×%d 拍)——H2 攻击链缺陷否证" % a_ok.n_attack) + check(a_ok.arith_ok, "A2 池算术完整:全部非蓄力拍 d_tick == 掷点口径期望(×%d 拍)" % a_ok.n_attack) check(String(run_a.result) == "defeat", "A3 攻击链致死可达:fresh 池 Boss 在 %d 轮内击倒英雄(result=%s——伤害确实落地)" % [run_a.snaps.size(), str(run_a.result)]) var run_a2 := _run_boss(db, seed_val, false) - check(JSON.stringify(run_a.rows) == JSON.stringify(run_a2.rows), "A4 同种子回放:Run A 两遍结算行逐行相同") + check(JSON.stringify(run_a.rows) == JSON.stringify(run_a2.rows), "A4 同种子回放:Run A 两遍结算行逐行相同(含 armor_roll 掷点流)") - print("--- Run B:自动挖掘(授予照常发生——当前数据墙行为实测)---") + print("--- Run B:自动挖掘(授予照常发生——当前数据承伤行为实测)---") var run_b := _run_boss(db, seed_val, true) _print_rounds(db, math, run_b, true) var b_ok := _assert_rounds(db, math, run_b, false) - check(b_ok.arith_ok, "B1 池算术完整(含墙轮):全部非蓄力拍 d_tick == pool_damage(raw, hero_def, enemy_out)(×%d 拍)" % b_ok.n_attack) - check(b_ok.wall_tick_positive, "B2 墙轮不归零(当前数据重证):hero_def<0 的轮 d_tick 恒 > 0(墙轮 ×%d,最深实测池 %d)" % [b_ok.n_wall, b_ok.deepest]) - var binding_floor: int = maxi(wall_floor, clamp_lo) # 墙深与池界取更浅者 = 实际可达的最深免伤池 - check(wall_floor > clamp_lo, "B3 墙归零不可达(表值判定):墙最深池 %d > pool_clamp_lo %d → 最低残余 ×%.2f——旧数据墙(旧 magnitude×2 = clamp_lo 触底 → ×0)为真人局零承伤根因,09-13 调参批已拆(H1 成立=设计语义,非攻击链 bug)" % [wall_floor, clamp_lo, float(100 + binding_floor) / 100.0]) + check(b_ok.arith_ok, "B1 池算术完整(含授予轮):全部非蓄力拍 d_tick == 掷点口径期望(×%d 拍)" % b_ok.n_attack) + check(b_ok.wall_tick_positive, "B2 负池轮不归零(伤害下限):hero_def<0 的轮 d_tick 恒 ≥ damage_floor(×%d 轮,最深实测池 %d)" % [b_ok.n_wall, b_ok.deepest]) + check(hero_def_negative * 16 > clamp_lo or hero_def_negative >= 0, "B3 墙归零不可达(表值判定):hero_defense 池最负行 %d——叠满不可触池底 %d(×0 免伤墙结构面已随 AC 换装消失)" % [hero_def_negative, clamp_lo]) func _run_boss(db, seed_val: int, dig: bool) -> Dictionary: @@ -200,12 +204,19 @@ func _analyze_round(db, math, rows: Array, beats: Dictionary) -> Dictionary: elif t == "damage" and String(row.get("source_class", "")) == "enemy_action": var baseline: int = int(row.get("baseline", 0)) var attach: int = int(row.get("attach", 0)) - var raw: int = baseline + attach + # 掷点口径重建(2026-09-16 护甲等级掷点批):基线按行留档档位打折(暴击 ×倍率),附加必中 + var tier: int = int(row.get("tier", 100)) + var crit: bool = bool(row.get("crit", false)) + var base_factor: float = float(tier) * (float(db.settlement.crit_multiplier) / 100.0 if crit else 1.0) + var rolled_base := int(floor(float(baseline) * base_factor / 100.0)) + var raw: int = rolled_base + attach var tick: int = int(row.get("value", 0)) - var expected: int = math.pool_damage(raw, pd, pe) + var expected: int = maxi(int(db.settlement.damage_floor), math.pool_damage(raw, pd, pe)) + var roll_txt := "掷%s" % str(int(row.get("roll", -1))) var zeroed := "✓" if tick == expected else "✗ 算术不符!" - lines.append("拍=%s 原始=%d+%d=%d hero_def=%d enemy_out=%d d_tick=%d 期望=%d %s" % [ - String(beats.get(int(row.get("round_index", 0)), "?")), baseline, attach, raw, pd, pe, tick, expected, zeroed]) + lines.append("拍=%s %s/档%d%s 基线%d→掷后%d +伏击%d hero_def=%d enemy_out=%d d_tick=%d 期望=%d %s" % [ + String(beats.get(int(row.get("round_index", 0)), "?")), roll_txt, tier, "暴击" if crit else "", + baseline, rolled_base, attach, 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(普通 %d + 穿刺 %d + 魔法 %d)→ %s" % [ diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/defense_scan.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/defense_scan.gd index 92c9914b..a8a565da 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/defense_scan.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/defense_scan.gd @@ -1,7 +1,8 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # Date: 2026-09-13(playtest-2026-09-12 Q1 复核——"安全格减伤是否过量导致满血打完 Boss") -# 逐结算轮统计 defense_up 授予层数(effect 行 value=-50)与该轮敌方实伤,分割"免伤墙轮" -# (≥2 层 = 池 clamp -100 = 100% 免伤)与非墙轮的实际承伤。 +# 2026-09-16 改 AC 口径(护甲等级掷点批):defense_up 百分比减伤行已整体退役换装 ac_up +# (+1 护甲等级/层 ×2 层),本探针改逐结算轮统计 ac_up 授予层数(AC10→11→12)与该轮敌方实伤, +# 分割"高 AC 轮"(叠满 = AC12,期望减免 28.75%)与基线轮(AC10,期望减免 21.25%)的承伤读数。 # 运行:Godot_console.exe --headless --path . --script res://smoke/defense_scan.gd -- [N] [seed0] extends SceneTree @@ -20,19 +21,19 @@ func _initialize() -> void: quit(1) return var autoplay = load("res://smoke/level_autoplay.gd") - print("=== 减伤叠层扫描:第一关 · N=%d · 种子 %d+%d ===" % [n, seed0, n]) - print("%-6s %-6s %-8s %-10s %-12s %-12s" % ["seed", "轮数", "战斗轮", "墙轮(≥2层)", "墙轮承伤", "非墙轮承伤"]) - var wall_rounds := 0 + print("=== 护甲强化叠层扫描(AC 口径):第一关 · N=%d · 种子 %d+%d ===" % [n, seed0, n]) + print("%-6s %-6s %-8s %-12s %-14s %-14s" % ["seed", "轮数", "战斗轮", "满层轮(AC12)", "满层轮承伤", "非满层轮承伤"]) + var full_rounds := 0 var open_rounds := 0 - var wall_dmg := 0 + var full_dmg := 0 var open_dmg := 0 var stack_total := 0 var combat_rounds := 0 for i in range(n): var st = autoplay.run_level(db, seed0 + i, 1, 0, 0, true) # 逐轮聚合(按战斗分段——round_index 跨战斗重置,直接用轮号会串桶): - # effect 行 value=-50 计减伤层;enemy_action damage 计承伤 - var def_by_round := {} + # effect 行 effect_id=ac_up 计护甲强化层;enemy_action damage 计承伤 + var ac_by_round := {} var dmg_by_round := {} var battle_seq := 0 var last_round := 0 @@ -42,43 +43,43 @@ func _initialize() -> void: battle_seq += 1 # 轮号回卷 = 新战斗 last_round = r var key := battle_seq * 1000 + r - if String(row.get("type", "")) == "effect" and String(row.get("display_name", "")) == "减伤" \ + if String(row.get("type", "")) == "effect" and String(row.get("effect_id", "")) == "ac_up" \ and String(row.get("source_class", "")) == "safe_cell": - def_by_round[key] = int(def_by_round.get(key, 0)) + 1 + ac_by_round[key] = int(ac_by_round.get(key, 0)) + 1 elif String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "enemy_action": dmg_by_round[key] = int(dmg_by_round.get(key, 0)) + int(row.get("value", 0)) - for k in def_by_round: + for k in ac_by_round: combat_rounds += 1 - stack_total += int(def_by_round[k]) + stack_total += int(ac_by_round[k]) var d: int = int(dmg_by_round.get(k, 0)) - if int(def_by_round[k]) >= 2: - wall_rounds += 1 - wall_dmg += d + if int(ac_by_round[k]) >= 2: + full_rounds += 1 + full_dmg += d else: open_rounds += 1 open_dmg += d - print("%-6d %-6d %-8d %-10d %-12s %-12s" % [seed0 + i, st.rounds, def_by_round.size(), - _wall_count(def_by_round), str(dmg_in(dmg_by_round, def_by_round, 2)), str(dmg_in(dmg_by_round, def_by_round, 1))]) + print("%-6d %-6d %-8d %-12d %-14s %-14s" % [seed0 + i, st.rounds, ac_by_round.size(), + _full_count(ac_by_round), str(dmg_in(dmg_by_round, ac_by_round, 2)), str(dmg_in(dmg_by_round, ac_by_round, 1))]) if combat_rounds > 0: - print("---- 战斗轮 %d:免伤墙轮 %d(%.0f%%,均值 %.2f 层/轮)· 墙轮承伤合计 %d(合法——-30 批墙帽 ×0.40)· 非墙轮承伤合计 %d(均 %.1f/轮)" % [ - combat_rounds, wall_rounds, 100.0 * float(wall_rounds) / float(combat_rounds), - float(stack_total) / float(combat_rounds), wall_dmg, open_dmg, float(open_dmg) / float(maxi(1, open_rounds))]) + print("---- 战斗轮 %d:满层轮 %d(%.0f%%,均值 %.2f 层/轮)· 满层轮承伤合计 %d(AC12——期望减免 28.75%%)· 非满层轮承伤合计 %d(均 %.1f/轮,AC10/11)" % [ + combat_rounds, full_rounds, 100.0 * float(full_rounds) / float(combat_rounds), + float(stack_total) / float(combat_rounds), full_dmg, open_dmg, float(open_dmg) / float(maxi(1, open_rounds))]) quit(0) -func _wall_count(def_by_round: Dictionary) -> int: +func _full_count(ac_by_round: Dictionary) -> int: var c := 0 - for r in def_by_round: - if int(def_by_round[r]) >= 2: + for r in ac_by_round: + if int(ac_by_round[r]) >= 2: c += 1 return c -func dmg_in(dmg_by_round: Dictionary, def_by_round: Dictionary, mode: int) -> int: - # mode=2 墙轮承伤 / mode=1 非墙轮承伤 +func dmg_in(dmg_by_round: Dictionary, ac_by_round: Dictionary, mode: int) -> int: + # mode=2 满层轮承伤 / mode=1 非满层轮承伤 var t := 0 - for r in def_by_round: - var walled := int(def_by_round[r]) >= 2 - if (mode == 2 and walled) or (mode == 1 and not walled): + for r in ac_by_round: + var full: bool = int(ac_by_round[r]) >= 2 + if (mode == 2 and full) or (mode == 1 and not full): t += int(dmg_by_round.get(r, 0)) return t diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/monte_carlo.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/monte_carlo.gd index e4872a7d..41c9eeef 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/monte_carlo.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/monte_carlo.gd @@ -25,7 +25,7 @@ func _run_encounter(db, L: int) -> void: var dmg_total: Array = [] var safe_opened: Array = [] var clears: Array = [] # 每战斗清图(换新板)次数 - var buff_grants: Array = [] # attack_up+defense_up 授予数 + var buff_grants: Array = [] # attack_up+ac_up 授予数 var losses := 0 var timeouts := 0 for i in range(N): @@ -61,7 +61,7 @@ func _run_battle(db, seed_val: int, L: int) -> Dictionary: settlement.step_row.connect(func(row): if row.type == "damage" and row.source_class == "enemy_action": out.hero_dmg += int(row.value) - elif row.type == "effect" and row.source_class == "safe_cell" and (row.effect_id == "attack_up" or row.effect_id == "defense_up"): + elif row.type == "effect" and row.source_class == "safe_cell" and (row.effect_id == "attack_up" or row.effect_id == "ac_up"): out.buffs += 1) settlement.minesweeper.cell_revealed.connect(func(evt): if evt.kind == "safe" and evt.channel != "initial": diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd index fb51a39e..1ff568a3 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd @@ -38,6 +38,7 @@ func _initialize() -> void: check(rng.derive_at("board_gen", 3) == 8849696722413293638, "R2 向量 board_gen m=42 scope=3") check(rng.derive_at("reward_roll", 0) == 7422916049646135537, "R2 向量 reward_roll m=42 scope=0") check(rng.derive_at("encounter", 0) == 1061841193803499209, "R2 向量 encounter m=42 scope=0(registry salt_encounter=0x8EBC6AF09C88C6E3 权威值)") + check(rng.derive_at("armor_roll", 0) == 4806095330042243845, "R2 向量 armor_roll m=42 scope=0(护甲等级掷点批 2026-09-16——registry salt_armor_roll=0xC2B2AE3D27D4EB2F 权威值)") check(rng.derive_at("board_gen", 0) == rng.derive_at("board_gen", 0), "derive_at 纯函数恒等") # scope 取号语义:首个 open_stream = scope 0;计数器单调 var s0 = rng.open_stream("board_gen") @@ -60,7 +61,8 @@ var _rng func _run_core_smoke(db, rng) -> void: _db = db _rng = rng - _smoke_wall_zero_guard() + _smoke_ac_swap_guard() + _smoke_armor_roll() _smoke_board_generator() _smoke_variation_framework() _smoke_full_battle(1, 0, "L1 · grub") @@ -196,18 +198,194 @@ func _smoke_level() -> void: check(int(_db.hero.atk) == 6 and int(_db.hero.max_hp) == 50, "模拟后 hero 表恢复基线(atk=%d hp=%d)" % [int(_db.hero.atk), int(_db.hero.max_hp)]) -# ---- 减伤墙归零护栏(2026-09-13 用户拍板:墙语义维持现状 + 护栏——BUG-0003 事故类永久免疫)---- -# 表级不变式:2×defense_up.magnitude > pool_clamp_lo。违反 = 叠满两层触池底 = F2 乘区 ×0 全额免伤 -# (playtest-2026-09-12 真人局 12/12 轮零承伤根因)。任何未来 magnitude/clamp 调参重新造出 ×0 墙在此红灯, -# 强制显式重审(含 clamp 收紧等结构变更——本护栏随之需要有意修订)。 +# ---- 护甲等级掷点批·换装护栏(2026-09-16 用户拍板:百分比减伤行整体退役换装 AC,无双轨)---- +# ① 换装在位:defense_up 状态行/池映射/奖励行三处全无残留,ac_up 三处就位(unit=ac ×2 层); +# ② 免伤墙归零护栏泛化保留(BUG-0003 事故类永久免疫):hero_defense 池任何 percent 行 +# magnitude × max_stack > pool_clamp_lo——单效果叠满不可触池底(现行池内仅 off_guard 正向行, +# 墙结构面已随 AC 换装消失,本断言防未来负向行回归时静默造墙); +# ③ 单位表 armor_class 字段:主角 10(暂定)/ 敌方行 0(暂定——正式值归怪物设定批); +# ④ 掷点参数域:档位边界升序/档位值降序无零档/骰面与暴击参数在域(加载校验双保险)。 -func _smoke_wall_zero_guard() -> void: - print("--- 增益状态·减伤墙归零护栏 ---") - var def_row: Dictionary = _db.status_effect_row("defense_up") - var wall_floor: int = int(def_row.get("magnitude", 0)) * int(def_row.get("max_stack", 1)) - var clamp_lo: int = int(_db.settlement.pool_clamp_lo) - check(wall_floor > clamp_lo, "墙最深池 %d > pool_clamp_lo %d → 叠满不可归零(最低残余 ×%.2f)" % [ - wall_floor, clamp_lo, float(100 + wall_floor) / 100.0]) +func _smoke_ac_swap_guard() -> void: + print("--- 护甲等级掷点批·换装护栏 ---") + # ① 换装在位(三处无双轨) + check(_db.status_effect_row("defense_up").is_empty(), "状态表 defense_up 行已退役(无双轨)") + var ac_row: Dictionary = _db.status_effect_row("ac_up") + check(not ac_row.is_empty() and String(ac_row.get("target_side", "")) == "hero" \ + and String(ac_row.get("unit", "")) == "ac" and int(ac_row.get("magnitude", 0)) == 1 \ + and int(ac_row.get("max_stack", 0)) == 2 and String(ac_row.get("stack_policy", "")) == "stack", + "ac_up 行在位(hero · +1 ac · ×2 层 · stack——AC 阈值轴新量纲)") + var ac_reward := {} + for row in _db.reward.rows: + if String(row.get("cell_type", "")) == "defense" and String(row.get("polarity", "")) == "positive": + ac_reward = row + check(String(ac_reward.get("effect_id", "")) == "ac_up" and String(ac_reward.get("display_name", "")) == "护甲强化", + "奖励表 defense 正向行 effect_id = ac_up(显示名暂定护甲强化,权重/tier 不变)") + check(not _db.settlement.effect_pool_map.has("defense_up") and not _db.settlement.effect_pool_map.has("ac_up"), + "effect_pool_map 无 defense_up/ac_up 键(减伤不入池——AC 不经乘区)") + # ② 免伤墙归零护栏(泛化——BUG-0003 免疫) + var wall_ok := true + var wall_detail := "" + for eff_id in _db.settlement.effect_pool_map.keys(): + if String(_db.settlement.effect_pool_map[eff_id]) != "hero_defense": + continue + var row: Dictionary = _db.status_effect_row(eff_id) + if row.is_empty() or String(row.get("unit", "")) != "percent": + continue + var floor_v: int = int(row.get("magnitude", 0)) * int(row.get("max_stack", 1)) + if floor_v <= int(_db.settlement.pool_clamp_lo): + wall_ok = false + wall_detail += " %s(%d)" % [eff_id, floor_v] + check(wall_ok, "hero_defense 池 percent 行叠满不可触池底(墙最深%s > clamp_lo %d——单效果归零结构性不可达)" % [ + wall_detail if wall_detail != "" else "(池内现无负向行)", int(_db.settlement.pool_clamp_lo)]) + # ③ 单位表 armor_class + check(int(_db.hero.armor_class) == 10, "主角 armor_class = 10(暂定基准——EV 期望减免 21.25%%)") + var ac_all := true + for row in _db.enemy.rows: + if not row.has("armor_class") or int(row.armor_class) != 0: + ac_all = false + check(ac_all, "敌方全行 armor_class = 0(首关暂定——正式行值归怪物设定批待办)") + # ④ 掷点参数域(加载校验已过——此处钉形状语义) + var bounds: Array = _db.settlement.roll_tier_bounds + var discounts: Array = _db.settlement.roll_tier_discounts + check(int(_db.settlement.roll_faces) == 20 and int(_db.settlement.crit_roll) == 20 \ + and int(_db.settlement.crit_multiplier) == 200 and int(_db.settlement.damage_floor) == 1 \ + and int(_db.settlement.attack_bonus) == 0 and String(_db.settlement.armor_roll_stream) == "armor_roll", + "掷点参数拍板值在位(d20 / nat20 暴击 ×200%% / 下限 1 / attack_bonus 0 / armor_roll 流)") + check(bounds == [3, 7] and discounts == [75, 50, 25], "档位表拍板值在位(缺口 1-3 → ×0.75 · 4-7 → ×0.5 · 8+ → ×0.25——无零档)") + + +# ---- 护甲等级掷点批·行为面(2026-09-16 拍板验收:折扣档分布 / nat20 暴击 / 伤害下限 / 棋盘来源必中)---- + +func _smoke_armor_roll() -> void: + print("--- 护甲等级掷点(查档纯函数 + 暴击 + 伤害下限 + AC12 分布 + 棋盘必中)---") + # ① 查档纯函数:全掷点域逐位对照(AC10 / AC12)+ EV 锚(AC10 期望减免 21.25% —— registry 参考数) + var expect_ac10 := {100: 11, 75: 3, 50: 4, 25: 2} # 掷 10-20 全额 / 7-9 / 3-6 / 1-2 + var expect_ac12 := {100: 9, 75: 3, 50: 4, 25: 4} # 掷 12-20 全额 / 9-11 / 5-8 / 1-4 + for cfg in [{"ac": 10, "expect": expect_ac10}, {"ac": 12, "expect": expect_ac12}]: + var dist := {100: 0, 75: 0, 50: 0, 25: 0} + var tier_ok := true + for roll in range(1, 21): + var d: int = DamageMath.roll_discount_percent(roll, cfg.ac, _db.settlement.roll_tier_bounds, _db.settlement.roll_tier_discounts) + if not dist.has(d): + tier_ok = false + continue + dist[d] += 1 + check(tier_ok and dist == cfg.expect, "查档 AC%d 全掷点域分布(100/75/50/25 = %s)" % [cfg.ac, str([dist[100], dist[75], dist[50], dist[25]])]) + var ev_sum := 0 + for roll in range(1, 21): + ev_sum += DamageMath.roll_discount_percent(roll, 10, _db.settlement.roll_tier_bounds, _db.settlement.roll_tier_discounts) + check(ev_sum == 1575, "AC10 EV 锚:期望系数合计 %d/2000 = 期望减免 21.25%%(nat20 恒全额含内)" % ev_sum) + # ② nat20 暴击(行为面):种子扫描定位 armor_roll scope0 首掷 = 20 的种子 → 停手轮主角先手必暴击 + var crit_seed := -1 + for s in range(1, 800): + var rg_probe = load("res://core/rng_service.gd").new() + rg_probe.set_master_seed(s) + if rg_probe.open_stream("armor_roll").randi_range(1, int(_db.settlement.roll_faces)) == int(_db.settlement.crit_roll): + crit_seed = s + break + check(crit_seed > 0, "nat20 种子可达(seed=%d——armor_roll scope0 首掷命中 %d)" % [crit_seed, int(_db.settlement.crit_roll)]) + var crit_rows := _first_hero_attack_rows(crit_seed) + check(int(crit_rows.get("roll", -1)) == int(_db.settlement.crit_roll) and bool(crit_rows.get("crit", false)), + "nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量)") + check(int(crit_rows.get("normal", -1)) == 2 * int(_db.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.hero.atk), + "nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" % [int(crit_rows.get("normal", -1)), int(_db.hero.atk)]) + # ③ 伤害下限(行为面):削弱 ×2 = 池触底 ×0 → 落地伤害 = damage_floor(零值行已退役) + var floor_rows := _weaken_zero_round_rows() + check(not floor_rows.is_empty() and int(floor_rows[0].get("value", -1)) == int(_db.settlement.damage_floor), + "池触底 ×0 → 落地伤害 = %d(全局 max(1,·)——×%d 拍采样)" % [int(_db.settlement.damage_floor), floor_rows.size()]) + var no_zero_label := true + for r in floor_rows: + if String(r.get("display_name", "")).contains("归零") or int(r.get("value", 0)) <= 0: + no_zero_label = false + check(no_zero_label, "零值伤害行语义退役:无\"被减伤归零\"行、无 v=0 伤害行(被压制/保底下限可见)") + # ④ AC12 折扣档分布(行为面):ac_up ×2 逐轮授给(时效 1)→ Boss 行攻击拍逐拍判定 + var tiers := _ac12_tier_samples() + var tier_distinct := {} + var tiers_valid := true + for t in tiers: + tier_distinct[int(t)] = true + if not [100, 75, 50, 25].has(int(t)): + tiers_valid = false + check(tiers_valid and tiers.size() >= 3, "AC12 攻击拍逐拍判定(×%d 拍全部落档域 {100,75,50,25})" % tiers.size()) + check(tier_distinct.size() >= 2, "折扣档实际流动(AC12 实测档位 %s——≥2 档可见)" % str(tier_distinct.keys())) + # ⑤ 棋盘来源必中 + 吃掷点行留档(全战斗流扫描) + var run = _run_battle(1, 0) + var board_ok := true + var attack_ok := true + var all_positive := true + for row in run.stream: + if String(row.get("type", "")) != "damage": + continue + if int(row.get("value", 0)) <= 0: + all_positive = false + var src := String(row.get("source_class", "")) + if src == "hero_attack" or src == "enemy_action": + if not (row.has("roll") and row.has("ac") and row.has("tier")): + attack_ok = false + elif row.has("roll"): + board_ok = false # 棋盘来源(爆裂/直伤/清障炸药)必中全额——不掷不留档 + check(all_positive, "全战斗流零 v=0 伤害行(伤害下限全局生效)") + check(attack_ok, "hero_attack/enemy_action 伤害行掷点读数+档位随行留档(P2 归因)") + check(board_ok, "棋盘来源伤害行零掷点字段(伏击附加/爆裂/直伤/清障炸药必中全额)") + + +# 定位种子下首场战斗第一行主角攻击伤害行(停手轮——零授予零充能,普通分量 = 攻击基线裸值) +func _first_hero_attack_rows(seed_val: int) -> Dictionary: + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(seed_val) + var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) + var rows: Array = [] + settlement.step_row.connect(func(row): rows.append(row)) + if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}): + return {} + settlement.minesweeper.settle_batch([{"action": "stop"}]) + for row in rows: + if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "hero_attack": + return row + return {} + + +# 削弱 ×2(池 enemy_output 触底 ×0)下的敌方攻击伤害行采样(D2 熊地精——首拍攻击) +func _weaken_zero_round_rows() -> Array: + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(20260916) + var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) + var rows: Array = [] + settlement.step_row.connect(func(row): rows.append(row)) + if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)}): + return rows + # 测试通道:直授削弱 ×2(来源格取真实坐标——行来源断言要求;时效 1 当轮结算内有效) + for i in range(2): + settlement._status_effects.apply_effect("weaken", {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": 1}) + settlement.minesweeper.settle_batch([{"action": "stop"}]) + var out: Array = [] + for row in rows: + if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "enemy_action": + out.append(row) + return out + + +# AC12 折扣档采样:每停手轮前直授 ac_up ×2(时效 1 逐轮衰减)——Boss 行攻击/重击拍逐拍判定留档 +func _ac12_tier_samples() -> Array: + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(20260917) + var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) + var rows: Array = [] + settlement.step_row.connect(func(row): rows.append(row)) + if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)}): + return [] + var guard := 12 + while settlement.battle_result == "" and guard > 0: + guard -= 1 + for i in range(2): + settlement._status_effects.apply_effect("ac_up", {"source_class": "safe_cell", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index}) + settlement.minesweeper.settle_batch([{"action": "stop"}]) + var tiers: Array = [] + for row in rows: + if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "enemy_action": + tiers.append(int(row.get("tier", -1))) + return tiers func _smoke_board_generator() -> void: @@ -768,17 +946,19 @@ func _level_battle_nodes(level_index: int) -> Array: # ---- 战斗段 fresh 池攻击链回归(playtest-2026-09-12 报告 Bug#2 沉淀—— # 「Boss 攻击拍零兑现」实为旧 defense_up 数据 ×2 叠满 = 池触底 ×0(设计语义);本断言钉死: -# 池为空时攻击链必须全额兑现——若未来出现 fresh 池零兑现(真攻击链缺陷)在此被抓)---- +# 池为空时攻击链必须按掷点档位足额兑现——2026-09-16 护甲等级掷点批改口径: +# d_tick = max(damage_floor, pool_damage(floor(基线×档位%), 0, 0)),档位读数随行重建可归因)---- func _smoke_attack_chain_fresh_pool() -> void: - print("--- 战斗结算·fresh 池攻击链回归(Bug#2)---") + print("--- 战斗结算·fresh 池攻击链回归(Bug#2·掷点口径)---") var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(20260913) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) - var ticks: Array = [] # {raw, tick}——敌行动伤害行重建 + var ticks: Array = [] # {baseline, attach, roll, tier, crit, tick}——敌行动伤害行重建 settlement.step_row.connect(func(row): if row.type == "damage" and row.source_class == "enemy_action": - ticks.append({"raw": int(row.baseline) + int(row.attach), "tick": int(row.value)})) + ticks.append({"baseline": int(row.baseline), "attach": int(row.attach), "roll": int(row.roll), + "tier": int(row.tier), "crit": bool(row.get("crit", false)), "tick": int(row.value)})) check(settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "fresh 池回归战斗启动(Boss 行)") # 停手轮:零挖掘零旗 → 零授予 → 池恒空;Boss pattern 前 4 拍 = attack/attack/charge/heavy for i in range(4): @@ -788,9 +968,12 @@ func _smoke_attack_chain_fresh_pool() -> void: check(ticks.size() >= 2, "非蓄力攻击拍采样充足(%d 拍——charge 拍零兑付为设计语义不计)" % ticks.size()) var all_full := true for t in ticks: - if int(t.tick) != int(t.raw) or int(t.raw) <= 0: + var base_factor: float = float(t.tier) * (float(_db.settlement.crit_multiplier) / 100.0 if t.crit else 1.0) + var rolled_base := int(floor(float(t.baseline) * base_factor / 100.0)) + var expected: int = maxi(int(_db.settlement.damage_floor), rolled_base + int(t.attach)) + if int(t.tick) != expected or int(t.tick) <= 0: all_full = false - check(all_full, "fresh 池非蓄力拍 d_tick == atk.total 全额兑现(Boss 攻击链健康——Bug#2 回归断言)") + check(all_full, "fresh 池非蓄力拍 d_tick = max(1, 掷点档位基线 + 伏击附加)(Boss 攻击链健康——Bug#2 回归断言)") func _encounter_replay(seed_val: int) -> Array: var out: Array = [] @@ -914,9 +1097,10 @@ func _smoke_refresh_provenance_guard() -> void: # ==================== 格面资源批新增覆盖(极性路由 / 负向字典 / 资源面 / 五段全序) ==================== # 极性路由断言(战斗结算 验收):挖安全格/标中旗只出正向、挖雷格/标错旗只出负向、兑付行指回来源格。 -# 正向字典效果集 = {attack_up, defense_up, weaken}(percent)+ spark_aoe/heal_minor(flat); +# 正向字典效果集 = {attack_up, ac_up, weaken}(ac_up = 2026-09-16 护甲等级掷点批换装,原 defense_up) +# + spark_aoe/heal_minor(flat); # 负向字典效果集 = {enrage, off_guard, sapped}(percent)+ 直伤(heal flat)/ ambush(blast flat)。 -const POSITIVE_EFFECTS := ["attack_up", "defense_up", "weaken"] +const POSITIVE_EFFECTS := ["attack_up", "ac_up", "weaken"] const NEGATIVE_EFFECTS := ["enrage", "off_guard", "sapped", "ambush"] diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/supply_scan.gd.uid b/prototypes/dungeon-sweeper-vertical-slice/smoke/supply_scan.gd.uid deleted file mode 100644 index 5d3bba21..00000000 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/supply_scan.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dw3rd4l55w70r diff --git a/prototypes/dungeon-sweeper-vertical-slice/tools/log_analysis.py b/prototypes/dungeon-sweeper-vertical-slice/tools/log_analysis.py new file mode 100644 index 00000000..e0a41df7 --- /dev/null +++ b/prototypes/dungeon-sweeper-vertical-slice/tools/log_analysis.py @@ -0,0 +1,612 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""试玩日志分析器(垂直切片 tools/ 专用,纯标准库,零第三方依赖)。 + +消费 presentation/playtest_logger.gd 产生的 playtest_*.jsonl, +产出 关卡 / 战斗 / 轮次 三级统计,并为护甲等级(AC)掷点批预留验收统计面。 + +已知测量盲区(本工具的核心补偿,勿绕过): + settlement 快照在衰减段之后发出、且现有状态寿命全部为 1 结算 + → 快照 buffs 字段恒为空。战斗期真实增益只能从 step 行(type=effect, + 文本 增伤/削弱/减伤/…)按结算轮窗口复原。本工具从不把快照 buffs + 当战斗期状态使用,只将其并列输出以持续暴露盲区。 + +AC 掷点批预留位: + 落批后伤害 step 行会新增掷点字段(读数/档位/暴击,字段名以落批实际为准)。 + 本工具按 FIELD_CANDIDATES 探测:字段存在 → 激活掷点统计面; + 不存在(AC 批前旧日志)→ 降级输出"不适用(AC 批前日志)"。 + 同时所有 step 行的未知键都会进"数据质量"观测面——落批后先看那里 + 拿到真实字段名,再在 FIELD_CANDIDATES 里补一行适配即可。 + +用法: + python log_analysis.py [--per-run] [--rounds] + 目录/通配 = 批量汇总;--per-run 附加逐局明细;--rounds 附加逐轮流水。 +""" + +import argparse +import glob +import json +import os +import sys +import time +from collections import Counter, defaultdict + +# ---------------------------------------------------------------- 常量与映射 + +# step 行已知键(schema 事实源 = playtest_logger.gd _on_step_row,2026-09-16 版) +STEP_KNOWN_KEYS = {"kind", "type", "source", "cell", "round", "text", "value", "highlight"} + +# AC 掷点批候选字段名(落批后按实际字段名在此登记——预留的一行适配点) +ROLL_FIELD_CANDIDATES = ("roll", "d20", "dice", "nat", "nat_roll", "roll_value") +TIER_FIELD_CANDIDATES = ("tier", "discount", "discount_tier", "damage_tier", "bracket") +CRIT_FIELD_CANDIDATES = ("crit", "critical", "is_crit", "nat20") +AC_FIELD_CANDIDATES = ("ac", "target_ac", "defense_ac") + +# effect 行显示文本 → 增益语义键(未登记文本原样作键,新增益自动浮现) +TEXT_TO_BUFF = {"增伤": "attack_up", "削弱": "weaken", "减伤": "defend_up"} + +# 档位语义:缺口 = AC − 掷点读数(读数达标 = 全额) +TIER_MULTIPLIER = {"全额": 1.0, "×0.75": 0.75, "×0.5": 0.5, "×0.25": 0.25} +THEORY_EV_REDUCTION_AC10 = 0.2125 # AC10 理论 EV 减免(任务口径) +THEORY_CRIT_RATE = 0.05 # nat20 暴击率理论值 +CRIT_TOLERANCE = 0.02 # 健康检查宽容度 ±2 个百分点 + +BAND_NORMAL = 0.20 # 承伤带:普通场 ≤20% max_hp +BAND_BOSS = 0.50 # 承伤带:Boss 场 ≤50% max_hp + +BATTLE_TYPES = ("battle", "boss") + + +def _first(row, candidates): + """返回 row 中第一个命中的候选字段值。""" + for c in candidates: + if c in row: + return row[c] + return None + + +# ---------------------------------------------------------------- 逐局状态机 + +class BattleAcc: + """一场战斗(battle/boss 节点)的累计器。""" + + def __init__(self, node_type, depth): + self.node_type = node_type + self.depth = depth + self.rounds = [] # 每结算一个 dict(见 RunAcc._finalize_round) + self.damage = 0 # Σ 敌方攻击落地伤害(enemy_action × type=damage 的 value) + self.heals = 0 # Σ 主角治疗(type=heal 的 value) + self.attack_rounds = 0 # 有敌方攻击落地的轮数 + self.max_hp = None + self.start_hp = None # 首结算快照 HP + 首轮承伤 − 首轮治疗 反推的进场 HP + self.last_hp = None + + +class RunAcc: + """一局(run 行切分;无 run 行的旧日志 = 整文件一局,种子取 session 行)。""" + + def __init__(self, src_file, session_seed): + self.src_file = src_file + self.session_seed = session_seed + self.seed = session_seed + self.policy = "" + self.level_index = None + self.result = None + self.end_hp = None + self.max_hp = None + self.round_steps = [] # 当前结算轮窗口内的 step 行缓冲 + self.battle = None + self.battles = [] + self.battle_fifo = [] # battle_end 延迟发出且不带 depth,按 FIFO 对账 + self.late_battle_ends = 0 # 未能配对的 battle_end(数据质量) + self.boss_node_seen = False + self.rounds_total = 0 + self.rounds_with_enemy = 0 + self.damage_total = 0 + self.heals_total = 0 + self.zero_narrow = 0 # 零承伤轮(窄口径,AC 验收主指标) + self.attack_landed_rounds = 0 + self.zero_loss_wide = 0 # 零掉血轮(宽口径,参考) + self.snapshot_buffs_nonempty = 0 # 盲区暴露面:快照 buffs 恒空计数 + self.snapshot_buffs_total = 0 + self.round_ledger = [] # --rounds 逐轮流水 + self.roll_rows = [] # 命中掷点候选字段的 step 行 + self._prev_hp = None # 上一结算快照的主角 HP(宽口径逐轮差分用) + self._last_enemy_present = False + + # -- 事件入口 --------------------------------------------------------- + + def on_run_row(self, e): + self.seed = e.get("seed", self.session_seed) + self.policy = "" if e.get("policy") is None else str(e.get("policy")) + self.level_index = e.get("level_index") + + def on_node(self, e): + t = e.get("type", "") + if t in BATTLE_TYPES: + # 先收掉上一场(战斗的 settlement 严格落在相邻 node 行之间) + self._close_battle() + self.battle = BattleAcc(t, e.get("depth")) + if t == "boss": + self.boss_node_seen = True + # rest / reward 节点不切战斗上下文(与 battle 同 index 出现) + + def on_step(self, e): + self.round_steps.append(e) + if any(c in e for c in ROLL_FIELD_CANDIDATES): + self.roll_rows.append(e) + + def on_settlement(self, e): + b = self.battle + self.rounds_total += 1 + hero = e.get("hero", {}) + hp = int(hero.get("hp", 0)) + if hero.get("max_hp"): + self.max_hp = int(hero.get("max_hp")) + enemy_present = bool(e.get("enemies")) + if enemy_present: + self.rounds_with_enemy += 1 + # 盲区暴露面:快照 buffs 只计数对照,从不当作战斗期状态 + self.snapshot_buffs_total += 1 + if e.get("buffs"): + self.snapshot_buffs_nonempty += 1 + self._finalize_round(e, b, hp, enemy_present) + + def on_battle_end(self, e): + # 延迟发出、不带 depth:只做 FIFO 对账(数据质量),核心统计不依赖它 + if self.battle_fifo: + self.battle_fifo.pop(0) + else: + self.late_battle_ends += 1 + + def on_level_end(self, e): + self._close_battle() + self.result = e.get("result", "") + self.end_hp = int(e.get("hp", 0)) + + # -- 轮窗口收口(核心口径都在这里)------------------------------------ + + def _finalize_round(self, snap, battle, hp, enemy_present): + steps, self.round_steps = self.round_steps, [] + dmg = sum(int(s.get("value", 0)) for s in steps + if s.get("type") == "damage" and s.get("source") == "enemy_action") + heal = sum(int(s.get("value", 0)) for s in steps if s.get("type") == "heal") + attacks = sum(1 for s in steps + if s.get("type") == "damage" and s.get("source") == "enemy_action") + charges = sum(1 for s in steps if s.get("type") == "charge") + # 蓄力拍(type=charge,含空拍/白喂)不算"攻击落地"——验收口径硬性排除。 + # 战斗期增益复原:本结算轮窗口内的 effect 行(快照 buffs 恒空,禁用) + buffs = Counter() + for s in steps: + if s.get("type") == "effect": + key = TEXT_TO_BUFF.get(s.get("text", ""), s.get("text", "")) + buffs[key or "<未登记effect文本>"] += 1 + + # 宽口径(整轮掉血 0):首轮用进场反推 HP 差分,之后用相邻快照差 + if self._prev_hp is None: + entry_hp = hp + dmg - heal + loss = entry_hp - hp + else: + loss = max(0, self._prev_hp - hp) + self._prev_hp = hp + + zero_narrow = attacks > 0 and dmg == 0 # 窄口径:有攻击落地且掉血 0 + zero_wide = loss == 0 # 宽口径:整轮 HP 不降(聚合时限敌战轮) + + self.damage_total += dmg + self.heals_total += heal + if attacks > 0: + self.attack_landed_rounds += 1 + if zero_narrow: + self.zero_narrow += 1 + if zero_wide: + self.zero_loss_wide += 1 + + rec = { + "type": battle.node_type if battle else "?", + "depth": snap.get("depth"), "round": snap.get("round"), + "dmg": dmg, "heal": heal, "hp": hp, + "attacks": attacks, "charges": charges, + "zero_narrow": zero_narrow, "zero_wide": zero_wide, + "buffs": dict(buffs), + "path": (snap.get("round_facts") or {}).get("path", ""), + "enemy_present": enemy_present, + } + self.round_ledger.append(rec) + if battle is not None: + battle.rounds.append(rec) + battle.damage += dmg + battle.heals += heal + battle.attack_rounds += 1 if attacks > 0 else 0 + battle.max_hp = self.max_hp + battle.last_hp = hp + if battle.start_hp is None: + battle.start_hp = hp + dmg - heal + + def _close_battle(self): + if self.battle is None: + return + b, self.battle = self.battle, None + if b.rounds: + # 有过敌人的战斗(任一轮出现过敌人或攻击落地)——宽口径分母用其全部轮数。 + # 不能按单轮快照 enemies 判定:敌人当轮被歼的轮快照为空,但轮仍属于敌战。 + b.enemy_flag = any(r["attacks"] > 0 or r["enemy_present"] for r in b.rounds) + for rec in b.rounds: + rec["enemy_battle"] = b.enemy_flag + self.battles.append(b) + self.battle_fifo.append(b) + + # -- 派生量 ----------------------------------------------------------- + + def boss_battle(self): + for b in self.battles: + if b.node_type == "boss": + return b + return None + + def boss_enter_hp(self): + """进 Boss HP = Boss 首结算快照 HP + 首轮承伤 − 首轮治疗(精确反推)。""" + b = self.boss_battle() + if b is None or not b.rounds: + return None + r0 = b.rounds[0] + return r0["hp"] + r0["dmg"] - r0["heal"] + + +# ---------------------------------------------------------------- 文件解析 + +class FileResult: + def __init__(self, path): + self.path = path + self.lines = 0 + self.bad_lines = 0 + self.parse_seconds = 0.0 + self.session_seeds = [] + self.runs = [] + self.unknown_step_keys = Counter() # 未知 step 键 → 次数(AC 落批观察面) + self.unknown_step_samples = {} # 未知 step 键 → 样例值 + self.dice_active = False + + +def parse_file(path): + res = FileResult(path) + t0 = time.perf_counter() + run = None + with open(path, encoding="utf-8", errors="replace") as f: + for line in f: + line = line.strip() + if not line: + continue + res.lines += 1 + try: + e = json.loads(line) + except json.JSONDecodeError: + res.bad_lines += 1 + continue + kind = e.get("kind", "") + if kind == "session": + res.session_seeds.append(e.get("seed")) + if run is None: + run = RunAcc(os.path.basename(path), e.get("seed")) + res.runs.append(run) + elif kind == "run": + if run is not None and (run.rounds_total or run.result or run.battles): + run = RunAcc(os.path.basename(path), res.session_seeds[-1] if res.session_seeds else None) + res.runs.append(run) + elif run is None: + run = RunAcc(os.path.basename(path), None) + res.runs.append(run) + run.on_run_row(e) + elif run is None: + # 无 session/run 头的残缺文件:兜底开一局,保证数据不丢 + run = RunAcc(os.path.basename(path), None) + res.runs.append(run) + _feed(run, res, kind, e) + else: + _feed(run, res, kind, e) + if run is not None: + run._close_battle() + res.parse_seconds = time.perf_counter() - t0 + res.dice_active = any(r.roll_rows for r in res.runs) + return res + + +def _feed(run, res, kind, e): + if kind == "node": + run.on_node(e) + elif kind == "step": + run.on_step(e) + for k in e: + if k not in STEP_KNOWN_KEYS: + res.unknown_step_keys[k] += 1 + res.unknown_step_samples.setdefault(k, repr(e[k])[:60]) + elif kind == "settlement": + run.on_settlement(e) + elif kind == "battle_end": + run.on_battle_end(e) + elif kind == "level_end": + run.on_level_end(e) + # action / ux / diagnosis 等其余 kind:本工具当前无消费面,静默跳过 + + +def resolve_inputs(arg): + """文件 / 目录 / 通配 → 排序后的 playtest_*.jsonl 列表。""" + if os.path.isdir(arg): + return sorted(glob.glob(os.path.join(arg, "playtest_*.jsonl"))) + if any(ch in arg for ch in "*?["): + return sorted(glob.glob(arg)) + return [arg] + + +# ---------------------------------------------------------------- 聚合与渲染 + +def fmt(x, nd=1): + return "—" if x is None else f"{x:.{nd}f}" + + +def aggregate_levels(runs): + """关卡级:按(关卡, 档位)聚合。""" + groups = defaultdict(list) + for r in runs: + groups[(r.level_index, r.policy)].append(r) + rows = [] + for (lvl, policy), rs in sorted(groups.items(), key=lambda kv: (str(kv[0][0]), str(kv[0][1]))): + n = len(rs) + wins = sum(1 for r in rs if r.result == "victory") + boss_rs = [r for r in rs if r.boss_enter_hp() is not None] + win_rs = [r for r in rs if r.result == "victory"] + rows.append({ + "level": lvl, "policy": policy or "—", "runs": n, + "clear_rate": wins / n if n else None, + "avg_rounds": sum(r.rounds_total for r in rs) / n if n else None, + "avg_boss_hp": (sum(r.boss_enter_hp() for r in boss_rs) / len(boss_rs)) if boss_rs else None, + "avg_end_hp": (sum(r.end_hp for r in win_rs) / len(win_rs)) if win_rs else None, + "avg_dmg": sum(r.damage_total for r in rs) / n if n else None, + }) + return rows + + +def aggregate_battles(runs): + """战斗级:按(关卡, 节点类型, 深度)分组,普通场与 Boss 场分开对照承伤带。""" + groups = defaultdict(list) + for r in runs: + for b in r.battles: + if b.max_hp: + groups[(r.level_index, b.node_type, b.depth)].append(b) + rows = [] + for (lvl, ntype, depth), bs in sorted( + groups.items(), key=lambda kv: (str(kv[0][0]), kv[0][1], str(kv[0][2]))): + n = len(bs) + avg_dmg = sum(b.damage for b in bs) / n + avg_ratio = sum(b.damage / b.max_hp for b in bs) / n + band = BAND_BOSS if ntype == "boss" else BAND_NORMAL + rows.append({ + "level": lvl, "type": ntype, "depth": depth, "count": n, + "avg_dmg": avg_dmg, "avg_ratio": avg_ratio, + "band": band, "pass": avg_ratio <= band, + "avg_rounds": sum(len(b.rounds) for b in bs) / n, + "avg_attack_rounds": sum(b.attack_rounds for b in bs) / n, + }) + return rows + + +def aggregate_rounds(runs): + t = sum(r.rounds_total for r in runs) + e = sum(r.rounds_with_enemy for r in runs) + a = sum(r.attack_landed_rounds for r in runs) + zn = sum(r.zero_narrow for r in runs) + # 宽口径分母 = 有过敌人的战斗的全部轮数(含敌人当轮被歼的收尾轮) + enemy_battle_rounds = 0 + zw = 0 + for r in runs: + for b in r.battles: + if getattr(b, "enemy_flag", False): + enemy_battle_rounds += len(b.rounds) + zw += sum(1 for rec in b.rounds if rec["zero_wide"]) + return {"rounds": t, "with_enemy": e, "attack_landed": a, + "zero_narrow": zn, "zero_loss_wide": zw, + "enemy_battle_rounds": enemy_battle_rounds} + + +def derive_tier(roll, ac): + """缺口 = AC − 掷点读数 → 档位(1-3/4-7/8+ → 75/50/25%,达标=全额)。""" + gap = ac - roll + if gap <= 0: + return "全额" + if gap <= 3: + return "×0.75" + if gap <= 7: + return "×0.5" + return "×0.25" + + +def aggregate_dice(res_files): + """掷点面(AC 批落地后激活)。字段名以 FIELD_CANDIDATES 命中为准。""" + rolls = [] # (direction, roll, tier, crit) + for res in res_files: + for run in res.runs: + for row in run.roll_rows: + roll = _first(row, ROLL_FIELD_CANDIDATES) + if not isinstance(roll, int): + continue + direction = "主角→敌" if row.get("source") == "hero_attack" else "敌→主角" + ac = _first(row, AC_FIELD_CANDIDATES) + ac = int(ac) if isinstance(ac, int) else 10 # 基线 AC10 + tier_v = _first(row, TIER_FIELD_CANDIDATES) + tier = tier_v if tier_v in TIER_MULTIPLIER else derive_tier(roll, ac) + crit_v = _first(row, CRIT_FIELD_CANDIDATES) + crit = bool(crit_v) if crit_v is not None else (roll == 20) + rolls.append((direction, roll, tier, crit)) + if not rolls: + return None + hist = Counter(r for _, r, _, _ in rolls) + tiers = Counter(t for _, _, t, _ in rolls) + crits = sum(1 for *_, c in rolls if c) + ev_red = 1 - sum(TIER_MULTIPLIER[t] for _, _, t, _ in rolls) / len(rolls) + by_dir = defaultdict(list) + for d, r, t, c in rolls: + by_dir[d].append((r, t, c)) + return {"hist": hist, "tiers": tiers, "n": len(rolls), + "crit_rate": crits / len(rolls), "ev_reduction": ev_red, "by_dir": by_dir} + + +def render_dice(dice, out): + print("== 掷点面(AC 批验收统计)==", file=out) + if dice is None: + print(" 不适用(AC 批前日志):本日志无掷点字段。", file=out) + print(" 其余指标照常输出;落批后重跑本工具,先看【数据质量】的未知字段观测面。", file=out) + return + print(f" 样本 {dice['n']} 次掷点", file=out) + print(" 掷点直方图(d20):", file=out) + for v in range(1, 21): + c = dice["hist"].get(v, 0) + if c: + print(f" {v:2d}: {'#' * min(c, 60)}{c if c > 60 else ''}", file=out) + n = dice["n"] + print(" 档位占比:", file=out) + for t in ("全额", "×0.75", "×0.5", "×0.25"): + c = dice["tiers"].get(t, 0) + print(f" {t:>6}: {c:6d} ({c / n * 100:5.1f}%)", file=out) + cr = dice["crit_rate"] + ok = abs(cr - THEORY_CRIT_RATE) <= CRIT_TOLERANCE + print(f" nat20 暴击率: {cr * 100:.2f}%(理论 {THEORY_CRIT_RATE * 100:.0f}% ±{CRIT_TOLERANCE * 100:.0f}pp)" + f" → {'健康' if ok else '超界,需排查'}", file=out) + print(f" 观察 EV 减免: {dice['ev_reduction'] * 100:.2f}%" + f"(理论 AC10 = {THEORY_EV_REDUCTION_AC10 * 100:.2f}%)", file=out) + print(" 按攻击方向:", file=out) + for d, items in sorted(dice["by_dir"].items()): + rr = [r for r, _, _ in items] + cc = sum(1 for *_, c in items if c) + red = 1 - sum(TIER_MULTIPLIER[t] for _, t, _ in items) / len(items) + print(f" {d}: n={len(items)} 均值掷点={sum(rr) / len(rr):.2f} " + f"暴击率={cc / len(items) * 100:.2f}% EV减免={red * 100:.2f}%", file=out) + + +def render(res_files, args, out): + total_lines = sum(r.lines for r in res_files) + total_secs = sum(r.parse_seconds for r in res_files) + all_runs = [run for res in res_files for run in res.runs] + + print("== 输入概况 ==", file=out) + print(f" 文件 {len(res_files)} 个,共 {total_lines} 行,解析 {total_secs:.2f}s" + f"(大文件单文件耗时见下)", file=out) + for res in res_files: + dice_tag = "掷点面:激活" if res.dice_active else "掷点面:本日志无掷点字段(AC 批前日志,已降级)" + print(f" {os.path.basename(res.path)}: {res.lines} 行 / {res.parse_seconds:.2f}s / " + f"{len(res.runs)} 局 / {dice_tag}", file=out) + print(file=out) + + render_dice(aggregate_dice(res_files), out) + print(file=out) + + print("== 关卡级统计 ==", file=out) + print(" 口径:局=run 行切分(无 run 行的旧日志整文件一局);通关=result=victory;", file=out) + print(" 进BossHP=Boss首结算快照HP+首轮承伤−首轮治疗(精确反推);平均轮数=每局settlement数。", file=out) + hdr = f" {'关卡':>6} {'档位':>8} {'局数':>5} {'通关率':>7} {'平均轮数':>8} {'进BossHP均':>10} {'通关剩余HP均':>12} {'整局承伤均':>10}" + print(hdr, file=out) + for row in aggregate_levels(all_runs): + print(f" {str(row['level']):>6} {row['policy']:>8} {row['runs']:>5} " + f"{fmt(row['clear_rate'] * 100 if row['clear_rate'] is not None else None):>6}% " + f"{fmt(row['avg_rounds']):>8} {fmt(row['avg_boss_hp']):>10} " + f"{fmt(row['avg_end_hp']):>12} {fmt(row['avg_dmg']):>10}", file=out) + print(file=out) + + print("== 战斗级统计(按 关卡/节点类型/深度 分组;对照承伤带 普通≤20%、Boss≤50% max_hp)==", file=out) + print(f" {'关卡':>6} {'类型':>6} {'深度':>4} {'场数':>5} {'场均轮数':>8} {'场均承伤':>8} {'承伤/max_hp':>11} {'承伤带':>7} {'判定':>4}", file=out) + for row in aggregate_battles(all_runs): + print(f" {str(row['level']):>6} {row['type']:>6} {str(row['depth']):>4} {row['count']:>5} " + f"{fmt(row['avg_rounds']):>8} {fmt(row['avg_dmg']):>8} {fmt(row['avg_ratio'] * 100):>10}% " + f"{'≤' + format(row['band'] * 100, '.0f') + '%':>7} {'✓' if row['pass'] else '✗':>4}", file=out) + print(file=out) + + rr = aggregate_rounds(all_runs) + print("== 轮级统计 ==", file=out) + print(f" 总轮数 {rr['rounds']}|敌战轮(有过敌人的战斗全部轮数){rr['enemy_battle_rounds']}|有攻击落地轮 {rr['attack_landed']}", file=out) + zn_pct = rr['zero_narrow'] / rr['attack_landed'] * 100 if rr['attack_landed'] else 0.0 + ebr = rr['enemy_battle_rounds'] + zw_pct = rr['zero_loss_wide'] / ebr * 100 if ebr else 0.0 + print(f" 零承伤轮(窄口径,AC 验收主指标): {rr['zero_narrow']} / 有攻击落地轮 {rr['attack_landed']}" + f"({zn_pct:.1f}%)——AC 批落地含'伤害下限 1'后该值应趋零", file=out) + print(f" 零掉血轮(宽口径,参考): {rr['zero_loss_wide']} / 敌战轮 {ebr}" + f"({zw_pct:.1f}%)", file=out) + print(file=out) + + # 增益战斗期复原 + buff_totals = Counter() + buff_settlements = 0 + for run in all_runs: + seen_in_run = False + for rec in run.round_ledger: + for k, v in rec["buffs"].items(): + buff_totals[k] += v + seen_in_run = True + if seen_in_run: + buff_settlements += sum(1 for rec in run.round_ledger if rec["buffs"]) + snap_total = sum(r.snapshot_buffs_total for r in all_runs) + snap_nonempty = sum(r.snapshot_buffs_nonempty for r in all_runs) + print("== 增益战斗期复原(来自 step 行,按结算轮窗口聚合)==", file=out) + print(f" 各增益出现总层数: {dict(buff_totals) if buff_totals else '(无 effect 行)'}", file=out) + print(f" 有增益激活的结算数: {buff_settlements}(每结算层数明细见 --rounds 流水)", file=out) + print(f" 快照 buffs 非空: {snap_nonempty}/{snap_total}" + f"(已知盲区:快照在衰减段后发出且状态寿命=1 结算 → 恒空;此行仅作盲区持续暴露)", file=out) + print(file=out) + + if args.per_run: + print("== 逐局明细(--per-run)==", file=out) + print(f" {'文件':<28} {'种子':>6} {'档位':>8} {'结果':>8} {'轮数':>4} {'承伤':>5} {'零承伤轮':>8} {'进BossHP':>8} {'终局HP':>6}", file=out) + for run in all_runs: + print(f" {run.src_file:<28} {str(run.seed):>6} {run.policy or '—':>8} " + f"{run.result or '—':>8} {run.rounds_total:>4} {run.damage_total:>5} " + f"{run.zero_narrow:>8} {fmt(run.boss_enter_hp(), 0):>8} {str(run.end_hp):>6}", file=out) + print(file=out) + + if args.rounds: + print("== 逐轮流水(--rounds)==", file=out) + print(" 列: 种子 节点 深度 轮 | 攻击落地 蓄力 承伤 治疗 结算HP | 零承伤 零掉血 | 增益复原 | 出路", file=out) + for run in all_runs: + for rec in run.round_ledger: + buffs = ",".join(f"{k}×{v}" for k, v in sorted(rec["buffs"].items())) or "—" + print(f" {run.seed} {rec['type']} {rec['depth']} r{rec['round']} | " + f"{rec['attacks']} {rec['charges']} {rec['dmg']} {rec['heal']} {rec['hp']:>3} | " + f"{'Y' if rec['zero_narrow'] else '-'} {'Y' if rec['zero_wide'] else '-'} | " + f"{buffs} | {rec['path']}", file=out) + print(file=out) + + print("== 数据质量 ==", file=out) + bad = sum(r.bad_lines for r in res_files) + late = sum(run.late_battle_ends for res in res_files for run in res.runs) + print(f" 解析失败行: {bad}|未配对 battle_end(延迟发出越局): {late}", file=out) + if res_files and any(res.unknown_step_keys for res in res_files): + print(" step 行未知字段观测面(AC 批落批后的适配入口——新掷点字段会先出现在这里):", file=out) + for res in res_files: + for k, c in res.unknown_step_keys.most_common(): + print(f" {os.path.basename(res.path)}: {k} ×{c} 样例 {res.unknown_step_samples[k]}", file=out) + else: + print(" step 行未知字段: 无(当前日志 = 2026-09-16 schema,无掷点/分量字段)", file=out) + print(" AC 批落批后:新字段先出现在上面观测面 → 在 FIELD_CANDIDATES 登记真实字段名 → 掷点面自动激活。", file=out) + + +# ---------------------------------------------------------------- 入口 + +def main(argv=None): + ap = argparse.ArgumentParser(description="试玩 JSONL 日志三级统计分析器(关卡/战斗/轮次 + AC 掷点预留面)") + ap.add_argument("input", help="jsonl 文件 | 目录 | 通配(目录按 playtest_*.jsonl 收取)") + ap.add_argument("--per-run", action="store_true", help="附加逐局明细") + ap.add_argument("--rounds", action="store_true", help="附加逐轮流水") + args = ap.parse_args(argv) + + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + + files = resolve_inputs(args.input) + files = [f for f in files if os.path.isfile(f)] + if not files: + print(f"未找到输入:{args.input}", file=sys.stderr) + return 2 + res_files = [parse_file(f) for f in files] + render(res_files, args, sys.stdout) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/prototypes/dungeon-sweeper-vertical-slice/tools/指标口径说明.md b/prototypes/dungeon-sweeper-vertical-slice/tools/指标口径说明.md new file mode 100644 index 00000000..bc9bd47e --- /dev/null +++ b/prototypes/dungeon-sweeper-vertical-slice/tools/指标口径说明.md @@ -0,0 +1,99 @@ +# log_analysis.py 指标口径说明 + +试玩日志分析器(`tools/log_analysis.py`,Python 3 标准库单文件)的指标定义、 +自验对账记录与 AC 掷点批适配点。schema 事实源 = `presentation/playtest_logger.gd`。 + +## 用法 + +``` +python log_analysis.py [--per-run] [--rounds] +``` + +- 目录 = 收取其下全部 `playtest_*.jsonl`;通配/目录 = 批量汇总(跨文件聚合)。 +- `--per-run` 附加逐局明细表;`--rounds` 附加逐轮流水(含每轮增益复原)。 + +## 局的切分 + +- 批量日志一文件多局:按 `run` 行切局(run 行携带该局种子/档位)。 +- 旧日志无 `run` 行:整文件一局,种子取 `session` 行。 +- 无 `session`/`run` 头的残缺文件:兜底开局,数据不丢。 + +## 关卡级指标 + +| 指标 | 定义 | +| --- | --- | +| 局数 | run 行切出的局数(旧日志 = 文件数)。 | +| 通关率 | `level_end.result == "victory"` 的局占比。 | +| 平均轮数/局 | 每局 `settlement` 快照数(=该局回合总数,快照每结算必发)。 | +| 进 Boss HP 均 | **快照反推法**:Boss 首结算快照 HP + 首轮承伤 − 首轮治疗。快照是回合末状态,用首轮回合内净额反推进场 HP,精确且自包含;仅统计到达 Boss 的局。 | +| 通关剩余 HP 均 | 通关局 `level_end.hp` 均值。 | +| 整局承伤均 | 每局全部敌方落地伤害之和的均值(见战斗级"承伤")。 | + +## 战斗级指标 + +- 分组键 =(关卡,节点类型,节点深度);`battle` 与 `boss` 分开输出。 +- **承伤** = 该场全部 `step` 行中 `source=enemy_action` 且 `type=damage` 的 `value` 合计 + (即敌方攻击实际落地伤害;含"被减伤归零"的 0 值行)。 +- **占生效 max_hp 比** = 承伤 ÷ 该场快照 `hero.max_hp`("生效"指取快照实时值,非写死 50)。 +- 承伤带判定:普通场 ≤20%、Boss 场 ≤50%,均值超带在"判定"列标 ✗。 +- `battle_end` 行由核心延迟发出(落在下一 node 行之后)且不带深度,本工具仅按 FIFO + 对账其配对数(数据质量面),核心统计不依赖它——战斗上下文以 node 行开、相邻 node/局末收。 + +## 轮级指标(AC 批验收硬指标所在) + +- **零承伤轮(窄口径,验收主指标)**:本轮**有敌方攻击落地**(≥1 行 `enemy_action`×`damage`) + 且主角掉血为 0(该轮敌方落地伤害合计 = 0)。分母 = 有攻击落地的轮数。 + - 蓄力拍(`type=charge`,含空拍/踩雷白喂)**不计**为"有攻击落地"——口径硬性排除。 + - AC 批落地含"伤害下限 1"后,"被减伤归零"型轮将变为 1 点实伤,该值应趋零。 +- **零掉血轮(宽口径,参考)**:本轮主角 HP 不降(首回合按进场反推 HP 差分, + 其后按相邻快照差)。分母 = **敌战轮**——"有过敌人的战斗"(任一轮出现敌人或攻击落地) + 的全部轮数。不能按单轮快照 enemies 判定:敌人当轮被歼的收尾轮快照 enemies 为空, + 但该轮仍属敌战(5678 局按单轮判定会得 4/7,按敌战轮才是 8/11)。 + +## 战斗期增益复原(盲区补偿面) + +- **事实**:`settlement` 快照在衰减段之后发出、且现有状态寿命全部为 1 结算 + → 快照 `buffs` 字段恒为空(本次自验 7,473 个快照 0 非空)。**禁止把快照 buffs + 当战斗期状态使用**。 +- **复原口径**:每结算的"轮窗口"= 上一 `settlement` 之后至本 `settlement` 之间的全部 + `step` 行;窗口内 `type=effect` 的行按显示文本聚为增益键(增伤→attack_up、 + 削弱→weaken、减伤→defend_up;未登记文本原样作键,如 4200 批浮现的 失衡/暴走/乏力/敌方伏击+2 层), + 行数即层数。逐层数明细看 `--rounds` 流水的"增益复原"列。 +- 快照 buffs 非空计数仍照常输出(当前恒 0),作为盲区持续暴露面:将来衰减时序改动 + 若使快照变非空,这里会第一个显示出来。 + +## 掷点面(AC 批预留,当前降级) + +- **激活条件**:step 行出现候选掷点字段(`ROLL/TIER/CRIT/AC_FIELD_CANDIDATES`)。 +- 字段存在 → 输出:d20 直方图、档位占比(缺口=AC−读数:达标=全额,1-3→×0.75, + 4-7→×0.5,8+→×0.25;优先读落批直供的档位字段)、nat20 暴击率(理论 5% ±2pp 健康检查)、 + 观察 EV 减免 vs 理论值(AC10 → 21.25%)、按攻击方向(主角→敌 / 敌→主角)分组。 +- 字段不存在(AC 批前日志)→ 显示"不适用(AC 批前日志)",其余指标照常。 +- **落批适配入口(一行)**:【数据质量】节的"step 行未知字段观测面"会自动列出全部新键及样例; + 把真实字段名补进 `FIELD_CANDIDATES` 常量即可,其余零改动。 + +## 已登记差异(等落批后核对) + +1. 任务背景称 step 行含分量字段 `baseline/attach/normal/pierce/magic`—— + **现行日志(2026-09-16 schema)没有这些字段**:`_on_step_row` 只落 + `kind/type/source/cell/round/text/value/highlight` 八键。本工具的未知键观测面 + 会在它们出现时自动捕获;届时按落批语义接入,不动 AC 批范围内文件。 +2. 掷点字段名以落批实际为准(候选名单只作探测,不作断言)。 + +## 自验对账记录(2026-09-16,真实日志) + +| 局 | 人工对账 | 结果 | +| --- | --- | --- | +| 5678(perfect 通关) | 整关掉血合计 = 深度2 场 1 + 深度3 场 1 + Boss 场 4 = **6** | 工具输出 6 ✓ | +| 5678 | 零掉血轮宽口径:敌战 11 轮 − 掉血 3 轮 = **8/11** | 工具输出 8/11 ✓ | +| 5678 | 零承伤轮窄口径:被减伤归零型 3 轮(深度1 r1 / Boss r1 / Boss r4) | 工具输出 3 ✓ | +| 1234(perfect 通关) | 独立脚本计数:敌方伤害 12、治疗 8、11 轮(HP 守恒 50−12+8=50) | 工具输出 12/11/终局 50 ✓ | +| 4200 批(233,254 行) | run 行 600、level_end 600(500 胜 100 败) | 切 600 局,normal 66.7% + perfect 100% = 500 胜 ✓ | + +- 大文件全量解析耗时:**233,254 行 / 1.35–1.53s**(python 3.13,单遍流式,含 json 解析与全部聚合)。 +- `battle_end` FIFO 对账:两种子局 + 600 局批均 0 未配对。 + +## 边界 + +- 本工具只读日志,不写任何游戏文件;与 `playtest_logger.gd` 的字段集对齐以 2026-09-16 版为准。 +- 快照 buffs、`battle_end`、`action`/`ux` 行族的更多消费面留待需要时扩展。 diff --git a/prototypes/dungeon-sweeper-vertical-slice/tools/样例输出_4200批600局.txt b/prototypes/dungeon-sweeper-vertical-slice/tools/样例输出_4200批600局.txt new file mode 100644 index 00000000..1dd6f6f8 --- /dev/null +++ b/prototypes/dungeon-sweeper-vertical-slice/tools/样例输出_4200批600局.txt @@ -0,0 +1,37 @@ +== 输入概况 == + 文件 1 个,共 233254 行,解析 1.35s(大文件单文件耗时见下) + playtest_4200_20260916T003953.jsonl: 233254 行 / 1.35s / 600 局 / 掷点面:本日志无掷点字段(AC 批前日志,已降级) + +== 掷点面(AC 批验收统计)== + 不适用(AC 批前日志):本日志无掷点字段。 + 其余指标照常输出;落批后重跑本工具,先看【数据质量】的未知字段观测面。 + +== 关卡级统计 == + 口径:局=run 行切分(无 run 行的旧日志整文件一局);通关=result=victory; + 进BossHP=Boss首结算快照HP+首轮承伤−首轮治疗(精确反推);平均轮数=每局settlement数。 + 关卡 档位 局数 通关率 平均轮数 进BossHP均 通关剩余HP均 整局承伤均 + 1 normal 300 66.7% 13.0 67.7 45.7 65.1 + 1 perfect 300 100.0% 11.8 75.0 73.7 8.5 + +== 战斗级统计(按 关卡/节点类型/深度 分组;对照承伤带 普通≤20%、Boss≤50% max_hp)== + 关卡 类型 深度 场数 场均轮数 场均承伤 承伤/max_hp 承伤带 判定 + 1 battle 0 600 1.8 5.1 7.7% ≤20% ✓ + 1 battle 1 598 2.2 5.8 8.7% ≤20% ✓ + 1 battle 2 589 1.7 2.2 3.2% ≤20% ✓ + 1 battle 3 584 2.5 4.4 6.5% ≤20% ✓ + 1 boss 4 562 4.7 20.8 29.3% ≤50% ✓ + +== 轮级统计 == + 总轮数 7449|敌战轮(有过敌人的战斗全部轮数)6988|有攻击落地轮 3749 + 零承伤轮(窄口径,AC 验收主指标): 1334 / 有攻击落地轮 3749(35.6%)——AC 批落地含'伤害下限 1'后该值应趋零 + 零掉血轮(宽口径,参考): 4259 / 敌战轮 6988(60.9%) + +== 增益战斗期复原(来自 step 行,按结算轮窗口聚合)== + 各增益出现总层数: {'失衡': 2821, 'defend_up': 8180, 'weaken': 8103, '暴走': 3287, 'attack_up': 9893, '乏力': 2075, '敌方伏击 +2 层': 2459} + 有增益激活的结算数: 7396(每结算层数明细见 --rounds 流水) + 快照 buffs 非空: 0/7449(已知盲区:快照在衰减段后发出且状态寿命=1 结算 → 恒空;此行仅作盲区持续暴露) + +== 数据质量 == + 解析失败行: 0|未配对 battle_end(延迟发出越局): 0 + step 行未知字段: 无(当前日志 = 2026-09-16 schema,无掷点/分量字段) + AC 批落批后:新字段先出现在上面观测面 → 在 FIELD_CANDIDATES 登记真实字段名 → 掷点面自动激活。 diff --git a/prototypes/dungeon-sweeper-vertical-slice/tools/样例输出_种子1234局.txt b/prototypes/dungeon-sweeper-vertical-slice/tools/样例输出_种子1234局.txt new file mode 100644 index 00000000..30bb574e --- /dev/null +++ b/prototypes/dungeon-sweeper-vertical-slice/tools/样例输出_种子1234局.txt @@ -0,0 +1,54 @@ +== 输入概况 == + 文件 1 个,共 340 行,解析 0.00s(大文件单文件耗时见下) + playtest_1234_20260916T004132.jsonl: 340 行 / 0.00s / 1 局 / 掷点面:本日志无掷点字段(AC 批前日志,已降级) + +== 掷点面(AC 批验收统计)== + 不适用(AC 批前日志):本日志无掷点字段。 + 其余指标照常输出;落批后重跑本工具,先看【数据质量】的未知字段观测面。 + +== 关卡级统计 == + 口径:局=run 行切分(无 run 行的旧日志整文件一局);通关=result=victory; + 进BossHP=Boss首结算快照HP+首轮承伤−首轮治疗(精确反推);平均轮数=每局settlement数。 + 关卡 档位 局数 通关率 平均轮数 进BossHP均 通关剩余HP均 整局承伤均 + 1 perfect 1 100.0% 11.0 50.0 50.0 12.0 + +== 战斗级统计(按 关卡/节点类型/深度 分组;对照承伤带 普通≤20%、Boss≤50% max_hp)== + 关卡 类型 深度 场数 场均轮数 场均承伤 承伤/max_hp 承伤带 判定 + 1 battle 0 1 2.0 0.0 0.0% ≤20% ✓ + 1 battle 1 1 2.0 0.0 0.0% ≤20% ✓ + 1 battle 2 1 2.0 0.0 0.0% ≤20% ✓ + 1 battle 3 1 2.0 8.0 16.0% ≤20% ✓ + 1 boss 4 1 3.0 4.0 8.0% ≤50% ✓ + +== 轮级统计 == + 总轮数 11|敌战轮(有过敌人的战斗全部轮数)11|有攻击落地轮 6 + 零承伤轮(窄口径,AC 验收主指标): 4 / 有攻击落地轮 6(66.7%)——AC 批落地含'伤害下限 1'后该值应趋零 + 零掉血轮(宽口径,参考): 9 / 敌战轮 11(81.8%) + +== 增益战斗期复原(来自 step 行,按结算轮窗口聚合)== + 各增益出现总层数: {'attack_up': 18, 'defend_up': 15, 'weaken': 14} + 有增益激活的结算数: 11(每结算层数明细见 --rounds 流水) + 快照 buffs 非空: 0/11(已知盲区:快照在衰减段后发出且状态寿命=1 结算 → 恒空;此行仅作盲区持续暴露) + +== 逐局明细(--per-run)== + 文件 种子 档位 结果 轮数 承伤 零承伤轮 进BossHP 终局HP + playtest_1234_20260916T004132.jsonl 1234 perfect victory 11 12 4 50 50 + +== 逐轮流水(--rounds)== + 列: 种子 节点 深度 轮 | 攻击落地 蓄力 承伤 治疗 结算HP | 零承伤 零掉血 | 增益复原 | 出路 + 1234 battle 0 r1 | 1 0 0 0 50 | Y Y | attack_up×2,defend_up×2,weaken×2 | ap_exhausted + 1234 battle 0 r2 | 0 0 0 0 50 | - Y | attack_up×1,defend_up×2,weaken×1 | ap_exhausted + 1234 battle 1 r1 | 2 0 0 0 50 | Y Y | attack_up×2,weaken×1 | ap_exhausted + 1234 battle 1 r2 | 0 0 0 0 50 | - Y | attack_up×2,defend_up×2 | ap_exhausted + 1234 battle 2 r1 | 1 0 0 0 50 | Y Y | attack_up×2,defend_up×1,weaken×2 | ap_exhausted + 1234 battle 2 r2 | 0 0 0 0 50 | - Y | attack_up×1,defend_up×1,weaken×3 | ap_exhausted + 1234 battle 3 r1 | 1 0 8 0 42 | - - | attack_up×2 | ap_exhausted + 1234 battle 3 r2 | 0 0 0 4 46 | - Y | attack_up×2,defend_up×2,weaken×2 | ap_exhausted + 1234 boss 4 r1 | 1 0 0 0 50 | Y Y | attack_up×1,defend_up×2,weaken×1 | ap_exhausted + 1234 boss 4 r2 | 1 0 4 0 46 | - - | attack_up×2,defend_up×1 | ap_exhausted + 1234 boss 4 r3 | 0 0 0 4 50 | - Y | attack_up×1,defend_up×2,weaken×2 | cleared + +== 数据质量 == + 解析失败行: 0|未配对 battle_end(延迟发出越局): 0 + step 行未知字段: 无(当前日志 = 2026-09-16 schema,无掷点/分量字段) + AC 批落批后:新字段先出现在上面观测面 → 在 FIELD_CANDIDATES 登记真实字段名 → 掷点面自动激活。 diff --git a/prototypes/dungeon-sweeper-vertical-slice/tools/样例输出_种子5678局.txt b/prototypes/dungeon-sweeper-vertical-slice/tools/样例输出_种子5678局.txt new file mode 100644 index 00000000..80c4f635 --- /dev/null +++ b/prototypes/dungeon-sweeper-vertical-slice/tools/样例输出_种子5678局.txt @@ -0,0 +1,55 @@ +== 输入概况 == + 文件 1 个,共 380 行,解析 0.00s(大文件单文件耗时见下) + playtest_5678_20260916T004134.jsonl: 380 行 / 0.00s / 1 局 / 掷点面:本日志无掷点字段(AC 批前日志,已降级) + +== 掷点面(AC 批验收统计)== + 不适用(AC 批前日志):本日志无掷点字段。 + 其余指标照常输出;落批后重跑本工具,先看【数据质量】的未知字段观测面。 + +== 关卡级统计 == + 口径:局=run 行切分(无 run 行的旧日志整文件一局);通关=result=victory; + 进BossHP=Boss首结算快照HP+首轮承伤−首轮治疗(精确反推);平均轮数=每局settlement数。 + 关卡 档位 局数 通关率 平均轮数 进BossHP均 通关剩余HP均 整局承伤均 + 1 perfect 1 100.0% 12.0 50.0 50.0 6.0 + +== 战斗级统计(按 关卡/节点类型/深度 分组;对照承伤带 普通≤20%、Boss≤50% max_hp)== + 关卡 类型 深度 场数 场均轮数 场均承伤 承伤/max_hp 承伤带 判定 + 1 battle 0 1 1.0 0.0 0.0% ≤20% ✓ + 1 battle 1 1 2.0 0.0 0.0% ≤20% ✓ + 1 battle 2 1 2.0 1.0 2.0% ≤20% ✓ + 1 battle 3 1 2.0 1.0 2.0% ≤20% ✓ + 1 boss 4 1 5.0 4.0 8.0% ≤50% ✓ + +== 轮级统计 == + 总轮数 12|敌战轮(有过敌人的战斗全部轮数)11|有攻击落地轮 6 + 零承伤轮(窄口径,AC 验收主指标): 3 / 有攻击落地轮 6(50.0%)——AC 批落地含'伤害下限 1'后该值应趋零 + 零掉血轮(宽口径,参考): 8 / 敌战轮 11(72.7%) + +== 增益战斗期复原(来自 step 行,按结算轮窗口聚合)== + 各增益出现总层数: {'weaken': 18, 'defend_up': 12, 'attack_up': 21} + 有增益激活的结算数: 12(每结算层数明细见 --rounds 流水) + 快照 buffs 非空: 0/12(已知盲区:快照在衰减段后发出且状态寿命=1 结算 → 恒空;此行仅作盲区持续暴露) + +== 逐局明细(--per-run)== + 文件 种子 档位 结果 轮数 承伤 零承伤轮 进BossHP 终局HP + playtest_5678_20260916T004134.jsonl 5678 perfect victory 12 6 3 50 50 + +== 逐轮流水(--rounds)== + 列: 种子 节点 深度 轮 | 攻击落地 蓄力 承伤 治疗 结算HP | 零承伤 零掉血 | 增益复原 | 出路 + 5678 battle 0 r1 | 0 0 0 0 50 | - Y | attack_up×1,defend_up×2,weaken×1 | ap_exhausted + 5678 battle 1 r1 | 1 0 0 0 50 | Y Y | attack_up×2,defend_up×1,weaken×3 | ap_exhausted + 5678 battle 1 r2 | 0 0 0 0 50 | - Y | attack_up×1,defend_up×1,weaken×1 | ap_exhausted + 5678 battle 2 r1 | 1 0 1 0 49 | - - | attack_up×2,defend_up×2,weaken×1 | ap_exhausted + 5678 battle 2 r2 | 0 0 0 1 50 | - Y | attack_up×2,defend_up×1,weaken×3 | ap_exhausted + 5678 battle 3 r1 | 1 0 1 0 49 | - - | attack_up×2,weaken×1 | ap_exhausted + 5678 battle 3 r2 | 0 0 0 1 50 | - Y | attack_up×1,defend_up×2,weaken×3 | ap_exhausted + 5678 boss 4 r1 | 1 0 0 0 50 | Y Y | attack_up×2,weaken×2 | ap_exhausted + 5678 boss 4 r2 | 1 0 4 0 46 | - - | attack_up×2,defend_up×2 | ap_exhausted + 5678 boss 4 r3 | 0 1 0 4 50 | - Y | attack_up×2,weaken×1 | ap_exhausted + 5678 boss 4 r4 | 1 0 0 0 50 | Y Y | attack_up×2,defend_up×1,weaken×2 | ap_exhausted + 5678 boss 4 r5 | 0 0 0 0 50 | - Y | attack_up×2 | ap_exhausted + +== 数据质量 == + 解析失败行: 0|未配对 battle_end(延迟发出越局): 0 + step 行未知字段: 无(当前日志 = 2026-09-16 schema,无掷点/分量字段) + AC 批落批后:新字段先出现在上面观测面 → 在 FIELD_CANDIDATES 登记真实字段名 → 掷点面自动激活。