diff --git a/design/engineering/test-conventions.md b/design/engineering/test-conventions.md index 32f46d5e..4b9247e9 100644 --- a/design/engineering/test-conventions.md +++ b/design/engineering/test-conventions.md @@ -79,7 +79,7 @@ ### 8.2 四层与真实入口 -- **单元**:不依赖场景树和真实宿主的纯逻辑;测试从领域类的对外方法进出,断言钉对外可观测面(返回值/信号/查询接口/落盘内容)。内部字段直查=受控例外:仅当该行为没有公开可观测面可钉时允许,对照表该行注明理由;存量不回头清扫,被批次触碰时顺带换代。 +- **单元**:不依赖场景树和真实宿主的纯逻辑;测试从领域类的对外方法进出,断言钉对外可观测面(返回值/信号/查询接口/落盘内容)。内部字段直查=受控例外:仅当该行为没有公开可观测面可钉时允许,对照表该行注明理由;存量不回头清扫,被批次触碰时顺带换代。探针直读视口承载子件结构(画布取视口首子件、渐隐带两片与平移提示子件按名取)属此类——断言对象即承载结构本身,无公开可观测面可钉,理由随对照表行申报。 - **接口**:模块边界的公开契约——数据表加载、存档与断点、事件载荷、命令接口;测试从公开入口进出。接口级断言随单元套件落(`unit/contracts/`),不另设承载目录。 - **集成**:跨宿主的接线、持久、生命周期——例如 LevelRunner 把 level_counters 传给 BattleOrchestrator、两场战斗共享同一个桶、断点续玩;测试必须实例化真实宿主链,禁止用裸 EffectEngine 代替。 - **探针/golden**:玩家可达性与观感;真实实例化场景、驱动真实输入。 diff --git a/design/gdd/00-游戏概念.md b/design/gdd/00-游戏概念.md index f695d80b..9b89a970 100644 --- a/design/gdd/00-游戏概念.md +++ b/design/gdd/00-游戏概念.md @@ -47,12 +47,6 @@ | 专名 | 是什么 | 出处条目 | |---|---|---| -| 显影尘 | 信息类秘宝 | aux_items | -| 工兵令 | 加行动点的秘宝 | aux_items | -| 探宝罗盘 | 预读奖励身份的秘宝 | aux_items | -| 探巢摆钟 | 探知巢穴位置的秘宝 | aux_items | -| 地穴全图卷 | 全图级稀有秘宝 | aux_items | -| 爆破工兵包 | 行动点 + 探巢的稀有秘宝 | aux_items | | 地精 / 熊地精 / 食人魔酋长 | 首发三敌种(末者为 Boss) | enemy | | 第一层 · 地精洞窟 | 首关层名 | level_table | | 封印 | 正式动作名(全库统一,非比喻、非呈现层别名)——对未开格实施封印,印记=封印印记 | terminology | diff --git a/design/gdd/03-内容规模.md b/design/gdd/03-内容规模.md index 1318120a..800a778d 100644 --- a/design/gdd/03-内容规模.md +++ b/design/gdd/03-内容规模.md @@ -38,7 +38,7 @@ MVP 不加新敌种,也不做同深度多敌种混搭;敌种扩容走 `enemy 目录(重设计 v1)已定稿 **12 件**(普通 7 / 优秀 3 / 稀有 2——代号制,正式命名归用户;数值全 provisional 归调参批),现行事实源=[秘宝](systems/跨局循环/秘宝.md)「目录(重设计 v1)」节。数据行随实装批落表(同调件数据表 = `data/tables/treasure_items.tres`),MVP 不新增。 -旧目录(旧 aux_items 六行:显影尘/工兵令/探宝罗盘/探巢摆钟/地穴全图卷/爆破工兵包,及过渡形态诸件)已全数退役、只作素材库(design/content-archive/道具退役素材库.md);旧行删除归实装批。 +旧目录(旧 aux_items 六行:显影尘/工兵令/探宝罗盘/探巢摆钟/地穴全图卷/爆破工兵包,及过渡形态诸件)已全数退役、留底 design/content-archive/道具退役素材库.md(数据表旧行已删净——treasure_items.tres 旧六名扫描零命中)。 ## 增益与状态 diff --git a/design/gdd/systems/战斗循环/地图战斗资源.md b/design/gdd/systems/战斗循环/地图战斗资源.md index 8d383c0d..556376d7 100644 --- a/design/gdd/systems/战斗循环/地图战斗资源.md +++ b/design/gdd/systems/战斗循环/地图战斗资源.md @@ -1,7 +1,7 @@ # 地图战斗资源 > 一句话:格面资源的产出、极性字典与清图奖励封装 -> 状态:已定稿;现行为 v1.5 +> 状态:已定稿;现行为 v1.6 ## 设计目的 @@ -96,7 +96,7 @@ - **档由两个参数定义:概率位 + 保底上限**。「永不保底」不单立规则——T4 的保底上限 = 0 即其统一表达;T1/T2 上限同为 0。 - **保底数量挂在型上、不挂在档上**:每个型各自登记「每板至少出几个」。档内登记保底的型数 ≤ 档上限 → 全部兑现;> 上限 → 每板从登记型中随机选定「上限」个兑现,其余型该板按普通概率出现、不保底。 -- **补签纪律**:执行序 = 装板摇号(含偏运硬币投影)→ 按上限定本期保底型集合 → 校验补签 → 板上全程只读。补签只改类型身份:危险格与安全格不动(补签格可为雷可为安全格,危险格与安全格同池不破)、不消费权重(补签不是一次取号)、清图判定不动、不泄露元信息(补签格与摇号格不可区分)。粒度 = 板,换板重新校验。 +- **补签纪律**:执行序 = 装板摇号(含订阅加权投影)→ 按上限定本期保底型集合 → 校验补签 → 板上全程只读。补签只改类型身份:危险格与安全格不动(补签格可为雷可为安全格,危险格与安全格同池不破)、不消费权重(补签不是一次取号)、清图判定不动、不泄露元信息(补签格与摇号格不可区分)。粒度 = 板,换板重新校验。 - **GP 数值锚**:以战斗结束 GP 奖励为锚——小 GP 袋 ≈ 其 1/10、大宝箱 ≈ 其 1 倍(具体数值归调参批)。 - 大宝箱为枚举新增(九型→十型走本文修订流程,不随手加行);微光归通用增益组(五色相不变,组内不细分正好容下)。 - **对外稀有度映射**(现行事实源=[核心循环](../../01-核心循环.md)「档位与获取规则」节):T1=普通 / T2=优秀 / T3=稀有 / T4=传说;任何概率提升禁止改变本档保底承诺(保底=承诺非概率)。 diff --git a/design/gdd/systems/战斗循环/战斗结算.md b/design/gdd/systems/战斗循环/战斗结算.md index 297f167a..3e0e9d6f 100644 --- a/design/gdd/systems/战斗循环/战斗结算.md +++ b/design/gdd/systems/战斗循环/战斗结算.md @@ -1,7 +1,7 @@ # 战斗结算 > 一句话:轮末结算的编排与逐行兑现 -> 状态:已定稿;现行为 v1.7 +> 状态:已定稿;现行为 v1.8 ## 设计目的 @@ -84,7 +84,7 @@ | 判定行 | 胜 / 负 / 继续各一行;终局判定行同时是胜负画面的数据锚 | | 高光行 | 完美封印成立时一行,只记实际有效兑付 | -每行还携带公共字段:来源类别、来源格、轮次、显示名、数值,以及可空的高光标记(用于强调封印标中、清图奖励这类值得高亮的结果)。奖励直伤行的数值字段沿用 `effectiveness` 键名、值为整数点数——键名与量纲是数据导出与分析工具的兼容契约,改动须连动分析工具面。没生效也要留痕:两类"未生效"行如实记录合法例外,呈现时聚合计数,不逐行刷屏。 +每行还携带公共字段:来源类别、来源格、轮次、显示名、数值,以及可空的高光标记(用于强调封印标中、清图奖励这类值得高亮的结果)。**显示名等玩家可见文案是数据面字段:文案值登记在数据表行,代码按来源类别与效果身份读表拼行,不在代码里写死文案字串**(2026-11-06 用户裁——文案归属数据表承载;承载表与字段形态归表结构设计段定,设计闭合后由实装批落表)。奖励直伤行的数值字段沿用 `effectiveness` 键名、值为整数点数——键名与量纲是数据导出与分析工具的兼容契约,改动须连动分析工具面。没生效也要留痕:两类"未生效"行如实记录合法例外,呈现时聚合计数,不逐行刷屏。 任何一行找不到来源即断言失败——宁可返回错误,也不让"合并到看不出来源"的呈现混过数据层。但有些行天然没有棋盘来源,断言为此留了两层豁免,豁免集合是封闭清单,改动同样走本文修订。 diff --git a/design/gdd/术语表.md b/design/gdd/术语表.md index fb63daa8..ffd8907d 100644 --- a/design/gdd/术语表.md +++ b/design/gdd/术语表.md @@ -1,7 +1,7 @@ # 术语表 > 全库专用术语的唯一对照表(2026-10-11 用户令建立)。规矩:新术语先登记本表、再在文档使用;退役词见文末对照表,文档正文一律使用现行词。文档中遇到看不懂的词,查本表。用词纪律(禁用词、替换值、豁免范围)见「用语规范」节。 -> 状态:**现行 v1.26**——2026-10-11 建立;历次升版明细见 git 提交史。 +> 状态:**现行 v1.27**——2026-10-11 建立;历次升版明细见 git 提交史。 ## 格与棋盘 @@ -95,7 +95,7 @@ | 术语 | 定义 | |---|---| | 效果端口件 | 战斗业务类(伤害管线/行动经济)唯一的效果发射出口:收事件与载荷、交效果引擎结算、取回命中批——业务类零引擎直连(机制归属见架构文档「战斗编排域拆件定稿」节轴戊;实现类 EffectPort) | -| 掷格件 | 装配期格面资源摇号的承载件:池行装配、区间行 roll、权重合成(偏运硬币/board_skew 两路投影);受理账面留守地图战斗资源件(机制归属同上;实现类 RewardRoller) | +| 掷格件 | 装配期格面资源摇号的承载件:池行装配、区间行 roll、权重合成(订阅加权/board_skew 两路投影);受理账面留守地图战斗资源件(机制归属同上;实现类 RewardRoller) | | `seal_missed` | 封印落空事件(旧名已换代):封印动作落在安全格上时发出,订阅面据此做限次与豁免判定 | | `forgive_seal_miss` | 本场首次封印落空豁免——专长「完美保险」的效果类型,`domain=battle` 首例;有效误标=误标总数 − 豁免数,豁免不改写误标账 | diff --git a/game/core/action_economy.gd b/game/core/action_economy.gd index 679302ef..6becbdfc 100644 --- a/game/core/action_economy.gd +++ b/game/core/action_economy.gd @@ -79,7 +79,7 @@ func try_spend(action_kind: String) -> bool: # 排雷玩法 挖掘专用:chain_size = 连锁附加格数(不含首格);clears_map = 本次动作后全图安全格开完。 -# 深挖钻头付费开掘(dig_paid counter_spend 订阅施加面):按序消耗族——AP 富余(≥基础+对价)时本动作付全价并积本轮付费计数(效果引擎 +# 付费开掘(dig_paid counter_spend 订阅施加面):按序消耗族——AP 富余(≥基础+对价)时本动作付全价并积本轮付费计数(效果引擎 # 计数器=授予段追加产出配额取数面);AP 不足全价时 fallback 降级只付基础 1 照常开格、不积计数 # ——持有钻头永不锁死普通挖格(散件保底 B-2)。 func request_dig(chain_size: int, clears_map: bool) -> bool: diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 0aba641a..d30b320e 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -255,7 +255,7 @@ func begin_battle(enc: Dictionary) -> bool: _begin_battle_fx_state(enc) _begin_battle_attune_params(enc) _begin_battle_preview_economy(enc) - # 工兵晨令=round_start grant_ap(mode:queue) 订阅行承载(when round_index ≤ effective_rounds + # 晨间 AP=round_start grant_ap(mode:queue) 订阅行承载(when round_index ≤ effective_rounds # value_from 定影);round_start 发射先于行动经济轮切换=树规则 queue 型当轮入队当轮兑现 # (先手压制=首轮 AP+1 语义锚);计数器 round_start 重置点同点生效 round_index = 1 @@ -535,7 +535,7 @@ func _exit_distribution() -> void: if not _install_and_reset(): _fatal("换图编排失败") return - # 工兵晨令续队直调拆除(内容数据驱动扩展批 T4——round_start 订阅行承载,见 begin_battle 注) + # 晨间 AP 续队直调拆除(内容数据驱动扩展批 T4——round_start 订阅行承载,见 begin_battle 注) action_economy.notify_settlement_done(mode) # round_start 发射先于行动经济轮切换(queue 型 grant_ap 当轮入队当轮兑现——与首轮同口径) round_index += 1 diff --git a/game/core/damage_controller.gd b/game/core/damage_controller.gd index 4223ef42..2b32f298 100644 --- a/game/core/damage_controller.gd +++ b/game/core/damage_controller.gd @@ -106,7 +106,7 @@ func _per_value(name: String, payload: Dictionary) -> int: return 0 -# 巢穴控制数(fact nest_count 取数面——敌方名下 weaken 实例总数,猎巢人订阅面; +# 巢穴控制数(fact nest_count 取数面——敌方名下 weaken 实例总数,nest_count 订阅消费面; # 个体化后削弱为累加行、每体各持一条——总数 = 被削弱个体数) func _nest_count() -> int: var n := 0 @@ -135,7 +135,7 @@ func submit(t: Dictionary) -> int: # attack_commit 发射点(我方伤害单据开立入口; # 命中挂单据 context 随行——施加面在 _resolve_hero_attack 管线算术前逐族施加,开单时点=主角攻击 # 单恒为段内首单(结账现场与开单现场一致)。载荷 atk/target/matchup + 挖掘 streak 事实 -# (执拗订阅面)+巢穴控制数〔猎巢人〕) +# (开格 streak 订阅面)+巢穴控制数〔nest_count 订阅面〕) # spec 束必载键(缺载 assert——fail-fast):channel/base_value/target_rule/ri; # 可选键缺省=原形缺省:origin_cell=(-1,-1)/attack_type=""/rider=0/highlight=""/context={}。 # 返回单据字典九栏目+requires/source_unit 逐字不变(契约 8)。 @@ -247,9 +247,9 @@ func _resolve(t: Dictionary) -> void: # 主角先手攻击:掷点 → 折扣/暴击 → 克制点数 → hero_offense×enemy_defense 池乘区 → 下限 → 交付 # (attack_commit/clamp_check/armor_roll 订阅施加面:命中=开单入口随单 -# context 携带,按固定算序逐族施加——命中序=数据表声明序,确定性)。 +# context 携带,按固定算序逐族施加——命中序=数据表声明序,确定性;掷点折扣读数→克制点数两拍)。 # 四步分段:命中面(_hero_attack_ctx,目标/护甲/ -# 掷点/按序改写族四拍/克制点数) → 基值装配(_hero_attack_base_value,加法族/计数器施加/倍乘) → +# 掷点/掷点折扣读数/克制点数) → 基值装配(_hero_attack_base_value,加法族/计数器施加) → # 掷点兑现(_hero_attack_redeem,rolled/池乘/下限/rider/落地/击杀发射) → 交付(_hero_attack_deliver, # 状态施加/标签/明细行)。步骤间经 ctx/redemption 字典穿线——算序/取值固定(🔴 逐位一致契约)。 func _resolve_hero_attack(t: Dictionary) -> void: @@ -261,7 +261,7 @@ func _resolve_hero_attack(t: Dictionary) -> void: _hero_attack_deliver(t, ctx, redemption) -# 命中面(目标/护甲克制 matchup/armor_roll 发射→按序改写族四拍→克制点数读值+订阅改写; +# 命中面(目标/护甲克制 matchup/armor_roll 发射→掷点折扣读数→克制点数读值+订阅改写; # 后两步各由 _ctx_rewrites/_ctx_clamp_delta 承载)。返回穿线 ctx(空=目标不可达)。 # clamp_check 只在 _ctx_rewrites 内发射一次——_ctx_clamp_delta 消费同批命中(禁重复发射)。 func _hero_attack_ctx(t: Dictionary) -> Dictionary: @@ -278,54 +278,25 @@ func _hero_attack_ctx(t: Dictionary) -> Dictionary: return {"target": target, "armor": armor, "atk_type": atk_type, "ac_hits": (t.context.get("fx_attack_hits", []) as Array), "ac_payload": ac_payload, "roll_read": rewrites["roll_read"], "threshold": rewrites["threshold"], - "crit": rewrites["crit"], "discount": rewrites["discount"], - "spike_ready": rewrites["spike_ready"], "cdelta": cdelta} + "crit": rewrites["crit"], "discount": rewrites["discount"], "cdelta": cdelta} -# 按序改写族四拍+掷点快照(armor_roll 发射点在此):第一拍矿工重锤命中交换(正负双修原生形态: -# 命中判定减成 trade_hit 点〔更易命中〕换攻击增伤 trade_atk%——高 AC 负期望/低 AC 正期望; -# 改写位先于掷点折扣读数)→ 第二拍专注免掷骰恒定折扣(方差买断成期望——稳定器人格;roll 记 0 归因) -# → 第三拍满层必中(spike ready 时 discount 恒全额、暴击面走倍乘不加掷)→ 第四拍猎弱本能对轻甲敌折扣档改善 value 档 -# (对冲 pierce 列 L1 −1 开局罚;改善 = 折扣表向全额方向移档,nat 最大面/已全额不重复)。 -# clamp_check 同在此发射(唯一发射点——命中随返回值穿给 _ctx_clamp_delta)。 +# 掷点折扣读数+克制点数命中批(armor_roll 发射点在此):一拍护甲掷骰折扣读数(掷点对照 +# 防御阈值定折扣档——阈值=防御 AC+攻击加值,nat 面沿折扣表)→ 二拍 clamp_check 发射 +# (唯一发射点——命中随返回值穿给 _ctx_clamp_delta)。 func _ctx_rewrites(t: Dictionary, target: Dictionary, armor: String, matchup: String) -> Dictionary: - var ac_hits: Array[Variant] = (t.context.get("fx_attack_hits", []) as Array) var roll_read: Dictionary[Variant, Variant] = _armor_roll() var threshold: int = defender_ac(target) + int(db.combat.settlement.damage_roll.attack_bonus) - # armor_roll 发射点(敌护甲掷骰判定;免掷恒折扣订阅面) - var ar_hits := _fx_hits("armor_roll", {"target_ac": threshold, "roll": int(roll_read.roll)}) - var trade_hit := 0 - for h in ac_hits: - if String(h.get("kind", "")) == "modify_ticket" and int(h.get("trade_hit", 0)) > 0: - trade_hit += int(h.trade_hit) - if trade_hit > 0: - threshold = maxi(1, threshold - trade_hit) + # armor_roll 发射点(敌护甲掷骰判定——事件在册、现役零订阅行,发射面保留) + _fx_hits("armor_roll", {"target_ac": threshold, "roll": int(roll_read.roll)}) var crit: bool = bool(roll_read.crit) var discount: int = _roll_discount(int(roll_read.roll), threshold) - for h in ar_hits: - if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "armor_rewrite": - discount = int(h.get("value", 0)) - crit = false - roll_read["roll"] = 0 - var spike_ready := false - for h in ac_hits: - if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "spike": - spike_ready = true - discount = 100 var clamp_hits := _fx_hits("clamp_check", {"matchup": matchup}) - var shift := 0 - for h in clamp_hits: - if String(h.get("kind", "")) != "modify_ticket": - continue - if String(h.get("mode", "")) == "discount_shift": - shift += int(h.get("value", 0)) - if shift > 0 and armor == "light" and discount < 100: - discount = _discount_shift_up(discount, shift) return {"roll_read": roll_read, "threshold": threshold, "crit": crit, - "discount": discount, "spike_ready": spike_ready, "clamp_hits": clamp_hits} + "discount": discount, "clamp_hits": clamp_hits} -# 克制点数读值+订阅改写(魔网织者/湛蓝誓约/猎手刺剑 clamp_check modify delta——克制矩阵查表值 +# 克制点数读值+订阅改写(clamp_check 带模式行 modify delta——克制矩阵查表值 # 后的直调;hero_attack 与 board_typed 两消费面同享)。命中批由 _ctx_rewrites 单次发射后传入。 # 淬毒刃纹(行 triggers clamp_check # modify(mode:clamp_bonus) 订阅命中并入本处 delta 合计;mode 路由=主角攻击面专享, @@ -338,8 +309,8 @@ func _ctx_clamp_delta(atk_type: String, armor: String, clamp_hits: Array) -> int return cdelta -# 基值装配(基值+单位标记加伤+矿工重锤增伤面+加法族(attack_bonus/ -# value_counter 层值)+计数器施加(attack_commit 用后清零行——命中序=声明序)+spike 倍乘。 +# 基值装配(基值+单位标记加伤+加法族(attack_bonus/ +# value_counter 层值)+计数器施加(attack_commit 用后清零行——命中序=声明序)。 func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float: var ac_hits: Array[Variant] = ctx["ac_hits"] var ac_payload: Dictionary[Variant, Variant] = ctx["ac_payload"] @@ -347,10 +318,6 @@ func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float: # 单位标记加伤(处刑台 target_attack_bonus 挂单位字典 fx_atk_bonus 键; # 阈值沿发射点已写入,本处通用消费——对该敌伤害加算) base_value += float(int((ctx["target"] as Dictionary).get("fx_atk_bonus", 0))) - # 按序改写族第五拍:矿工重锤增伤面(交换的正向侧) - for h in ac_hits: - if String(h.get("kind", "")) == "modify_ticket" and int(h.get("trade_atk", 0)) > 0: - base_value *= 1.0 + float(int(h.trade_atk)) / 100.0 # 加法族:attack_bonus per 计数器层值(层数×value 加算)。 # value_counter 扩展〔疼痛记忆/弃图兑换/陷阱本能——rider 值=计数器账面, # 值读于施加时点=命中序先行 rider 后清零行〕 @@ -365,11 +332,6 @@ func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float: if String(h.get("kind", "")) == "counter_add": var set_v = h.get("set", null) _port.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) - # 任意真族:modify_ticket mode=spike 满层倍乘(强制必中已在折扣拍承载) - if ctx["spike_ready"]: - for h in ac_hits: - if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "spike": - base_value *= float(int(h.get("value", 1))) return base_value @@ -391,7 +353,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_offense", StatusContainer.OWNER_HERO), clamped_pool("enemy_defense", StatusContainer.owner_key_for(target))) var landed: int = maxi(int(db.combat.settlement.damage_roll.damage_floor), pooled) # 加法族:英雄侧 rider(enemy_skill rider 对称位补齐口径)——后置必中附加,独立于掷点与乘区 - # (奥术余烬「普攻命中后恒定结算」语义);猎巢人巢控光环(per nest_count)/矿工的执拗开格加成 + # (普攻命中后恒定结算语义);巢控光环(per nest_count)/开格 streak 加成 # (per dig_streak、cap 封顶)同通道合计;档一质变 rider+1 / 档二(crit 时)rider 乘暴击倍率。 # rider 不改变下限语义(landed 含 rider 后不再过 floor)。 var hero_rider := int(_fact_or("rider_tier_bonus") or 0) # or 0=未绑缺省与 int(null) 缺省同值(int() 构造拒 null——端口缺省 null 面的空值合并) @@ -421,7 +383,7 @@ func _kill_effects(t: Dictionary, target: Dictionary) -> void: _port.reveal_effect() # 斩首学者——击杀揭示 1 格(端口路由宿主入队,授予段末兑付) -# 交付:即时执行族(碎甲震击打重甲附加「护甲裂解」——复用 off_guard,零新状态行; +# 交付:即时执行族(重甲附加「护甲裂解」——复用 off_guard,零新状态行; # apply_effect 域闭/重复授予由状态容器自行守卫)/标签装配/明细行发射/兑现登记。 func _hero_attack_deliver(t: Dictionary, ctx: Dictionary, redemption: Dictionary) -> void: var payload: Dictionary[Variant, Variant] = redemption["payload"] @@ -524,11 +486,11 @@ func _resolve_board_typed(t: Dictionary) -> void: var armor_b: String = String(target.get("armor_type", "light")) var atk_b: String = String(t.attack_type) # clamp_check 发射+施加(克制判定修正点棋盘面;与 hero_attack 同表同点: - # modify delta 订阅改写合计进克制点数;discount_shift 档位面属主角攻击专享,本面不施加) + # modify delta 订阅改写合计进克制点数;带模式行只施放于主角攻击面,本面不施加) var cdelta: int = _counter_delta(atk_b, armor_b) if atk_b != "": for h in _fx_hits("clamp_check", {"matchup": atk_b + "_vs_" + armor_b}): - # 无模式 delta 才入棋盘窗(mode 路由:clamp_bonus=主角攻击面专享、discount_shift=折扣档面) + # 无模式 delta 才入棋盘窗(mode 路由:带模式行=主角攻击面专享,棋盘窗只吃无模式 delta) if String(h.get("kind", "")) == "modify_ticket" and h.has("delta") and String(h.get("mode", "")) == "": cdelta += int(h.delta) var dealt: int = DamageMath.counter_component(int(t.base_value), cdelta, int(db.combat.settlement.damage_roll.damage_floor)) @@ -619,15 +581,6 @@ func _counter_delta(attack_type: String, armor: String) -> int: return int(row.get(armor, 0)) -# 折扣档改善(猎弱本能——折扣表向全额方向移 value 档;查不到当前档 = 不动防御口径) -func _discount_shift_up(discount: int, shift: int) -> int: - var tbl: Array[Variant] = db.combat.settlement.damage_roll.roll_tier_discounts - var idx := tbl.find(discount) - if idx < 0: - return discount - return int(tbl[mini(tbl.size() - 1, idx + shift)]) - - # 池内加算 → clamp [-100,+100](池间乘算归 DamageMath.pool_damage 调用侧); # 池按个体读(增益状态 v1.5):主角侧池传 OWNER_HERO,敌方侧池传被打者/施放者个体键—— # 乘区只合计该个体名下实例(B 敌的削弱不进 A 敌的账) diff --git a/game/core/effect_engine.gd b/game/core/effect_engine.gd index 33d3a725..56b9e75f 100644 --- a/game/core/effect_engine.gd +++ b/game/core/effect_engine.gd @@ -27,7 +27,7 @@ const MINE_EVENTS := ["dig_reveal", "dig_paid", "seal_hit", "mine_hit", "seal_se ## bias_rewards/preview。需求卡片标题计 23 系笔误,落码按行内容全收——commit 留痕) const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open_ticket", "attack_bonus", "yield_mult", "spawn_spell", "grant_material", - "grant_ap", "grant_gp", "bias_wallet", "shield", "spike", "apply_status", "counter_add", + "grant_ap", "grant_gp", "bias_wallet", "shield", "apply_status", "counter_add", "counter_spend", "reveal_tiles", "remove_mines", "refresh_pick", "bias_rewards", "preview", # 六型效果(奇物.md 基础件表 27 件声明清单): @@ -61,8 +61,8 @@ const FACTS := ["matchup", "is_first_dig_of_battle", "terrain_kind", "target_arm "ap_exhausted_last", "hp_ratio", "amount", "ap_left", "pct", "battle_index"] # 关卡域战斗序(1 基,enc 携带;开局姿态组「每关首轮窗」条件面) -## when 算子词汇(封闭——fact 查询面落法:mod_zero=计数器取模判满〔掘进蓄能〕、 -## advantageous=克制矩阵正面判定〔淬毒刃纹〕、gte_ref/lt_ref=与另一计数器比较〔深挖钻头配额〕、 +## when 算子词汇(封闭——fact 查询面落法:mod_zero=计数器取模判满、 +## advantageous=克制矩阵正面判定〔淬毒刃纹〕、gte_ref/lt_ref=与另一计数器比较(计数器配额面)、 ## ends_with=后缀匹配〔对某甲族的泛条件〕、mod_eq=取模余数判定〔双管镐每第 2 次——value=模数 ## ref=余数;dispatch 预评估口径〔when 于命中前读计数器现值〕下「每第 N 次」系用 ref=N-1 表达〕。 ## 均系映射表现状条件装进 fact 的查询面落法,白名单覆盖) @@ -79,7 +79,7 @@ const WHEN_KEYS := ["fact", "op", "value", "id", "ref", "value_from"] ## axis=bonus_delta 加算区轴向) const DO_KEYS := ["kind", "value", "value_from", "delta", "delta_from", "mult", "mult_from", "cap", "cap_from", "cost_mult", "per", "divisor", "dtype", "mode", "channel", "counter_id", - "set", "status_id", "label", "fallback", "target_kinds", "trade_atk", "trade_hit", + "set", "status_id", "label", "fallback", "target_kinds", "value_event", "value_counter", "axis"] ## 主动件分发通道(不经事件订阅——刷新/显影/移除是玩家主动动作;use_treasure 按 kind 分发) @@ -154,7 +154,7 @@ func _push_tree_rule(rk: String, params: Dictionary[Variant, Variant]) -> void: "do": {"kind": "shield", "value": int(params["shield"])}}, {}) "sense_sharp": # 锋=踩中危险格伤害减半·每关限次——mine_hit 挂点现成(damage_mult 收集→halve_pct, - # 与惊巢反制同路径;伤害在结算战斗段末兑付,探索阶段零改动) + # 与挖掘惩罚减半同路径;伤害在结算战斗段末兑付,探索阶段零改动) _push_trigger("tree", rk, {"on": "mine_hit", "when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": int(params["cap_per_level"])}], "do": {"kind": "damage_mult", "value": int(params["halve_pct"])}}, {}) @@ -449,7 +449,7 @@ static func _vtr_from_refs(rid: String, tr: Dictionary, row: Dictionary, errs: A ## 加法族(数值相加,可交换):add_ticket_rider、attack_bonus、grant_material、grant_ap、 ## counter_add、shield——发射点逐条累加收集器。 ## 乘法族(收集后一次施加,正负分桶):yield_mult、damage_mult。 -## 任意真族(任一命中即生效):spawn_spell、spike。 +## 任意真族(任一命中即生效):spawn_spell。 ## 按序改写族(收集→单点按声明序施加):modify_ticket 及一切改写窗口效果。 ## 按序消耗族(按订阅序消耗共享资源,耗尽即止;可选 fallback):counter_spend。 ## 即时执行族(每条命中各自执行一次,执行序=声明序):open_ticket、 diff --git a/game/core/reward_grant.gd b/game/core/reward_grant.gd index db957961..5fb230d6 100644 --- a/game/core/reward_grant.gd +++ b/game/core/reward_grant.gd @@ -1,5 +1,5 @@ ## 奖励授予件(自 battle_orchestrator 收编 grant 族):极性字典路由兑付(grant_routed——含封印溢价 -## 与惊巢反制减半)、三种行应用(flat 四分支/percent/gp)、伤害单据提交面(submit——案C 暂存闸承载)、 +## 与挖掘惩罚减半)、三种行应用(flat 四分支/percent/gp)、伤害单据提交面(submit——案C 暂存闸承载)、 ## 清图奖励与身份授予(完美封印第二份)、战斗内 GP 缓冲 escrow、秘宝使用四步编排(use_treasure—— ## 相位闸→校验→受理闸→逐 kind 应用→消耗)、揭示奖励兑付(reveal_redeem——fx_host flush 的兑付回调面)。 ## 生命周期=随编排器(编排器 _init 同建同灭,每战新建)。 @@ -127,7 +127,7 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary: return {"ok": true, "item_id": v.item_id, "resolved": v.resolved, "record": rec} -# 受理闸(treasure_used 发射+施加:counter_spend 订阅=掘进蓄能积点账本,积点不足拒绝零消耗。 +# 受理闸(treasure_used 发射+施加:counter_spend 订阅=付费积点账本,积点不足拒绝零消耗。 # 零 item_id 特判;拒绝文案读装配集积点进度——宿主 attune 读投影) func _charge_gate(v: Dictionary, target: Vector2i) -> Dictionary: var tu_hits := fx_host.fx_emit("treasure_used", {"item_id": String(v.item_id), "target": target}) @@ -180,13 +180,13 @@ func _safe_dig_open(v: Dictionary) -> Dictionary: return {} -# 爆破工兵包兑付(中立移除危险格:未揭示/无封印/是雷过滤归排雷玩法; +# 中立移除兑付(未揭示/无封印/是雷过滤归排雷玩法; # 邻接数字冻结零重算、清图分母零涉(危险格不入分母);空集拒绝零消耗—— # consume 之前 return(拒绝面注记扩「③内零消耗分支」),拒绝 reason 可呈现) func _cell_removal_open(v: Dictionary) -> Dictionary: var blasted: Array[Variant] = fx_host.minesweeper.remove_mines_in(v.resolved) if blasted.is_empty(): - return {"ok": false, "reason": "爆破工兵包:3×3 区域内无可移除危险格(须未揭示且未封印)"} + return {"ok": false, "reason": "中立移除:3×3 区域内无可移除危险格(须未揭示且未封印)"} return {} @@ -218,12 +218,12 @@ func grant_routed(snap: Dictionary, polarity: String, att: Dictionary) -> bool: row = row.duplicate() row["power"] = int(round(int(row.power) * prem_pct / 100.0)) row["premium_pct"] = prem_pct - # 惊巢反制(减半系数=mine_hit damage_mult 订阅命中〔行 value 50=减 50%,取整 ceil〕; + # 挖掘惩罚减半(减半系数=mine_hit damage_mult 订阅命中〔行 value 50=减 50%,取整 ceil〕; # duplicate 隔离——减半不回写极性字典) if halve_pct > 0 and polarity == "negative" and source_class == "mine_hit": row = row.duplicate() var half: int = int(ceil(float(int(row.power)) * float(100 - halve_pct) / 100.0)) - emit_cb.call({"type": "effect", "source_class": source_class, "origin_cell": cell}, ri, "惊巢反制——危险格挖掘惩罚减半(%d→%d)" % [int(row.power), half], 0, {"effect_id": "nest_counter"}) + emit_cb.call({"type": "effect", "source_class": source_class, "origin_cell": cell}, ri, "挖掘惩罚减半(%d→%d)" % [int(row.power), half], 0, {"effect_id": "nest_halve"}) row.power = half return _dispatch_row(row, att) diff --git a/game/core/reward_roller.gd b/game/core/reward_roller.gd index 57010bac..ac1f9e3b 100644 --- a/game/core/reward_roller.gd +++ b/game/core/reward_roller.gd @@ -1,4 +1,4 @@ -## 掷格件:装配期格面资源摇号——池行装配、区间行 roll、权重合成(偏运硬币/board_skew 两路投影)。 +## 掷格件:装配期格面资源摇号——池行装配、区间行 roll、权重合成(订阅加权/board_skew 两路投影)。 ## 自 RewardEconomy 迁出掷格面(architecture v1.29 轴戊②);受理账面(grant/身份查询/快照/retire/ ## polarity_row)留守 RewardEconomy——衔接面=格→奖励身份映射批束(本件产出、受理件接收)。 ## 取号流声明:roll 每批恰开一 reward_roll 流(scope=每次 install 批不变),体序=normalize(行主序 @@ -38,12 +38,12 @@ func _init(bundle: Dictionary) -> void: func roll(eligible: Array) -> Dictionary[Variant, Variant]: var list: Array[Dictionary] = _normalize_eligible(eligible) var stream: RandomNumberGenerator = rng.open_stream("reward_roll") # 恰一 scope 批(格批 + 清图批尾 + 区间行追加取号) - # reward_roll 发射归 BattleFxHost(偏运硬币 bias_rewards 订阅命中合计经注入查询口取回); + # reward_roll 发射归 BattleFxHost(bias_rewards 订阅命中合计经注入查询口取回); # 零命中/未接线=0=原行集零扰动 var luck_bias := 0 if luck_bias_query.is_valid(): luck_bias = luck_bias_query.call(list.size()) - var cell_rows: Array[Variant] = _biased_cell_rows(luck_bias) # cell 池正向行 weight>0(偏运硬币装配时稀有正向 kinds 权重 +N——池结构零触碰) + var cell_rows: Array[Variant] = _biased_cell_rows(luck_bias) # cell 池正向行 weight>0(订阅件装配时稀有正向 kinds 权重 +N——池结构零触碰) var clear_rows: Array[Variant] = _pool_rows("clear_default") # ① 身份批:每非封锁格恰一类型身份(雷安同池——同一行集同一权重分布) var draft := {} @@ -83,7 +83,7 @@ func _normalize_eligible(eligible: Array) -> Array[Dictionary]: return list -# 偏运硬币权重投影(底稿 §3:只动池内权重、雷安仍同池同分布;luck_bias=0 逐位返回原行集——off 态零扰动) +# 订阅加权权重投影(底稿 §3:只动池内权重、雷安仍同池同分布;luck_bias=0 逐位返回原行集——off 态零扰动) # 合成序:先经图级偏斜投影再叠件级加法——合成序锚=图级先、件级后 #(环境事实先定型、玩家构筑叠其上;两投影均为池内权重面,雷安同池与取号次数零变化) func _biased_cell_rows(bias: int) -> Array: diff --git a/game/core/settlement/grant_segment.gd b/game/core/settlement/grant_segment.gd index 1eee08d0..eb472a60 100644 --- a/game/core/settlement/grant_segment.gd +++ b/game/core/settlement/grant_segment.gd @@ -124,8 +124,8 @@ func _reveal_safe(evt: Dictionary, snap: Dictionary, ri: int) -> void: # dig_reveal 施加面(序=计数器〔连击计数——乘区收集后执行=本格兑付用旧值〕→ 揭示入队〔地质学家/ -# 跳格采掘 reveal_tiles 订阅面,授予段末统一掷格兑付〕→ 追加产出〔深挖钻头 v2 双份制:grant_material -# 命中=配额内追加第二份〕→ 奥术矢〔魔能共鸣——每战首次开安全格追加魔法矢;任意真族单次生效〕) +# 跳格采掘 reveal_tiles 订阅面,授予段末统一掷格兑付〕→ 追加产出〔双份制:grant_material +# 命中=配额内追加第二份〕→ 奥术矢(每战首次开安全格追加魔法矢;任意真族单次生效)) func _reveal_safe_apply(dr_hits: Array, evt: Dictionary, ri: int) -> void: for h in dr_hits: if String(h.get("kind", "")) == "counter_add": @@ -163,7 +163,7 @@ func _reveal_mine(evt: Dictionary, snap: Dictionary, ri: int) -> void: reward_grant.grant_routed(reward_grant.feat_yield_adjust(snap, false, 0, 0), "negative", {"source_class": "mine_hit", "cell": evt.cell, "ri": ri, "halve_pct": int(applied["halve_pct"])}) for h in mh_hits: if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "reflect": - var nct := damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_counter", "惊巢反制·反弹", + var nct := damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_halve", "反弹伤害", {"channel": "board", "base_value": int(h.get("value", 0)), "target_rule": DamageController.TARGET_ALL_ALIVE, "ri": ri, "origin_cell": evt.cell}) reward_grant.submit(nct, false) @@ -229,7 +229,7 @@ func _terrain_batch(ri: int) -> void: # 封印判定批+战封印军规开单段(封印流:seal_judged 批后统一开单:轮末每面封印命中对最前存活敌 # 结算 board_typed 必中伤害〔复用零新配方;「封印所在巢穴敌」简化为 default_target 最前存活敌—— -# 台账披露待追认〕;完美布防=本轮封印批全中(hit>0 且 miss==0)时封印伤 ×mult——全对推理奖励)。 +# 台账披露待追认〕;封印全中倍率=本轮 hit>0 且 miss==0 时封印伤 ×mult——全对推理奖励)。 # 伤害单据案C 暂存战斗段末兑付。 func _seal_batch(ri: int) -> void: for evt in facts.seals: @@ -237,7 +237,7 @@ func _seal_batch(ri: int) -> void: _seal_strike_batch(ri) -# seal_settle 发射+施加(战封印 open_ticket + 完美布防 modify_ticket 改写窗口:封印伤基值×完美倍率后 +# seal_settle 发射+施加(战封印 open_ticket + 封印全中倍率 modify_ticket 改写窗口:封印伤基值×完美倍率后 # 逐封印开单) func _seal_strike_batch(ri: int) -> void: var fs_hits := 0 @@ -262,7 +262,7 @@ func _seal_strike_batch(ri: int) -> void: # 战封印军规基值决议(attack 通道 open_ticket 首命中取基值;seal_mult modify_ticket 连乘; -# 完美布防=miss==0 且 hit>0 时基值×倍率。空旗集=空字典〔不开单合法态〕) +# 封印全中倍率=miss==0 且 hit>0 时基值×倍率。空旗集=空字典〔不开单合法态〕) func _seal_strike_value(fs_fx: Array, fs_hits: int, fs_misses: int) -> Dictionary: var banner_hits: Array[Variant] = [] for h in fs_fx: @@ -276,11 +276,11 @@ func _seal_strike_value(fs_fx: Array, fs_hits: int, fs_misses: int) -> Dictionar if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "seal_mult": seal_mult *= maxi(1, int(h.get("value", 1))) if fs_misses == 0 and fs_hits > 0 and seal_mult > 1: - strike_value *= seal_mult # 完美布防:同轮全中封印伤倍增(全对推理奖励) + strike_value *= seal_mult # 封印全中:同轮全中封印伤倍增(全对推理奖励) return {"value": strike_value} -# round_end 发射+施加(每轮结算收口:熔火之躯反打单开立〔open_ticket 即时执行〕+积层清零〔counter_add +# round_end 发射+施加(每轮结算收口:轮末反打单开立〔open_ticket 即时执行〕+积层清零〔counter_add # set〕;停手侧 AP 已在 request_stop 记账,finish_round_stop 按转存率转存 RAP)+施加面:grant_gp〔爆破会计/收盘——escrow 战斗域 # 缓冲,随 gp_dropped 上抛〕/grant_ap mode:queue〔封印回响——返 1 AP 下轮,queue_ap_delta 通道与轮始 # 加成同面〕+段界收口(转存率入参、清图奖励、揭示兑付、段界结账) @@ -348,7 +348,7 @@ func _judge_seal(evt: Dictionary, ri: int) -> void: # 封印命中(资格以危险格实例为键去重——排雷玩法 诚实重复上报的对侧;seal_hit 发射序: -# 殉爆票据 → 施加面 → 雷区图谱额外兑付 → 基线正向兑付) +# 标中雷反打票据 → 施加面 → 标中雷额外兑付 → 基线正向兑付) func _judge_seal_hit(evt: Dictionary, ri: int) -> void: var cell: Vector2i = evt.cell if facts.seal_consumed.has(cell): @@ -359,7 +359,7 @@ func _judge_seal_hit(evt: Dictionary, ri: int) -> void: "terrain_kind": minesweeper.harmful_adjacent(cell)}) _seal_burst_tickets(fh_hits, cell, ri) _seal_hit_effects(fh_hits, cell, ri) - # 雷区图谱(封印流——标中雷额外获得一份格面资源:封印开始产出对标开格; + # 标中雷追加(封印流——标中雷额外获得一份格面资源:封印开始产出对标开格; # 复用 grant 单点,额外兑付走同一 grant_routed 正向路由) for h in fh_hits: if String(h.get("kind", "")) == "grant_material": @@ -370,7 +370,7 @@ func _judge_seal_hit(evt: Dictionary, ri: int) -> void: reward_grant.grant_routed(snap, "positive", {"source_class": "seal_hit", "cell": cell, "ri": ri, "highlight": "seal_hit"}) -# 标中殉爆票据(行 triggers open_ticket channel blast value_from magnitude——每标中一面封印全体存活敌 +# 标中雷反打票据(行 triggers open_ticket channel blast value_from magnitude——每标中一面封印全体存活敌 # 吃 N 群伤,无类型走 blast 通道;封印爆裂〔专长〕同面消费) func _seal_burst_tickets(fh_hits: Array, cell: Vector2i, ri: int) -> void: for h in DamageController.hits_of(fh_hits, "open_ticket"): diff --git a/game/data/schemas/level_gen_params.gd b/game/data/schemas/level_gen_params.gd index 40563cc5..6bb36305 100644 --- a/game/data/schemas/level_gen_params.gd +++ b/game/data/schemas/level_gen_params.gd @@ -1,6 +1,7 @@ ## 关卡节点图参数档 schema(节点图生成器核心批 卡① T1):五旋钮+战斗保底+岔口去路数的行式数据表。 ## 键 = level_index(关卡行集单点),消费面经 ConfigDB.run.level_gen_params.row_by_level_index() 读取;生成器只吃参数档、不认关卡号(契约 6)。 -## 数值全 provisional(定值批落终值);域校验沿关卡节点图生成.md v1.1 调参表已锁条款——层深 L1≥6 递增增幅变大、 +## 数值全 provisional(定值批落终值);取值域与字段契约单点=本文件域校验(概念与设计意图指针= +## 关卡节点图生成.md v1.5 参数契约节)——层深 L1≥6 递增增幅变大、 ## 配比四类合计 1、出口 L1 恒 1/L2-L3 1-3、岔口去路数恒 2/3、战斗保底≥0。 class_name LevelGenParams extends Resource @@ -66,7 +67,7 @@ func _validate_row_shape(row: Dictionary, L: int, key: String, layers_by_level: var bd: float = float(row.get("branch_density", -1.0)) if bd < 0.0 or bd > 1.0: errs.append({"category": "domain", "message": "%s 分支密度 %f 越域 [0,1]" % [key, bd]}) - # 域并单点(关卡节点图生成.md v1.4 调参表——生成器侧域校验退役,唯此一处) + # 取值域单点=本文件域校验(概念与设计意图指针=关卡节点图生成.md v1.5 参数契约节) var ftw: float = float(row.get("fork_third_way_pct", -1.0)) if ftw < 0.0 or ftw > 100.0: errs.append({"category": "domain", "message": "%s fork_third_way_pct %f 越域 [0,100]" % [key, ftw]}) diff --git a/game/data/schemas/treasure_item_table.gd b/game/data/schemas/treasure_item_table.gd index ceaeccf0..d4891c9f 100644 --- a/game/data/schemas/treasure_item_table.gd +++ b/game/data/schemas/treasure_item_table.gd @@ -17,7 +17,7 @@ const UNLOCK_COUNTER_KEYS := ["adventure_cleared", "boss_kill_count", "vision_us const DIMENSIONS := ["info", "risk", "action", "plan", "counter"] # enum 九 kind 全部条件触发/机制修正类;mine_reveal/ap_grant 语义不变。 -# cell_removal = 爆破工兵包中立移除格;mine_hit_halve 系固定 +# cell_removal = 中立移除危险格通道;mine_hit_halve 系固定 # 「减半」语义不消费 magnitude(反弹 N 由 reflect_damage 行 magnitude 承载)。 # 主动件 kind 词族(reveal_tiles/preview/grant_ap/remove_mines/ # refresh_pick——效果引擎 ACTIVE_KINDS 同名闭集);被动件不走 effect_kinds 词族 @@ -119,7 +119,7 @@ func _validate_row_head(row: Dictionary, iid: String) -> Array[Variant]: ## 订阅形态域:triggers 事件订阅行判据与行级触发词表校验(被动件形态;与 effect_kinds 互斥校验 ## 面=passive 行(单形态——效果行为只走订阅);active_charged 行两形合法并存 -## (effect_kinds=主动分发通道 + triggers=受理闸订阅,掘进蓄能先例)) +## (effect_kinds=主动分发通道 + triggers=受理闸订阅,积点解锁先例)) func _validate_row_triggers(row: Dictionary, iid: String) -> Array[Variant]: var errs: Array[Variant] = [] if row.has("triggers"): diff --git a/game/presentation/views/level_map_screen.gd b/game/presentation/views/level_map_screen.gd index fd0af5e7..c7ad7bf6 100644 --- a/game/presentation/views/level_map_screen.gd +++ b/game/presentation/views/level_map_screen.gd @@ -31,6 +31,8 @@ const SLOT_STEP_Y := 58 # 同层槽位纵向步距 const HIT_PAD := 6.0 # 图上命中外扩(指宽容差) const CONFIRM_WINDOW_S := 3.0 # 放弃确认态窗口(秒——超时回落) const COL_HP_BAR_BACK := UiKit.HP_BAR_BACK # 迷你血条深底(对暗面板可辨;值单点收 UiKit) +const DRAG_THRESHOLD := 8.0 # 拖动距离阈值(布局决策 5 provisional——超阈值进拖拽态,拖拽不触发选路) +const WHEEL_STEP := 60.0 # 滚轮横滚单档步进(provisional 实现期定值;方向映射=代拍板:下/右=向右前进向) ## 内部状态件(u2 子域拆件——契约 9 职责轴):_MapState=选路屏运行态与图绘制态(单一变化原因= ## 选路呈现)。layer_decor 域字典承载换层装饰三件(图标贴图/石纹面/tint 层号——装饰域归并形态)。 @@ -39,6 +41,7 @@ class _MapState extends RefCounted: var runner = null var title: Label var canvas: Control + var viewport: LevelMapViewport # 卷轴视口承载件(布局决策 5——画布宿主/裁切/clamp/跟随/指引) var level_label: Label var gp_value: Label var hp_value: Label @@ -54,6 +57,8 @@ class _MapState extends RefCounted: var _cand_ids: Array[int] = [] # 本停点候选 id 集(图上命中合法集;探针直读冻结——住宿主) var _content_size := Vector2(620, 240) # 图内容包络(open_choices 按图拟合——画布 min 尺寸随它;探针直读冻结) var _hover_id := -1 # 悬停预承诺候选(-1 = 无;探针直读冻结) +var _drag_active := false # 拖拽态(超阈值至抬杆——拖拽不触发选路,布局决策 5) +var _drag_press_pos := Vector2(-1, -1) # 拖距判定基准(按下点画布局部坐标;-1 = 无按下跟踪) var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开;探针直读冻结) var status_badge: Button # 状态面板入口角标(顶栏 GP 徽右侧;连接归组合根;探针直读冻结) var _layout_cache := {"nodes": [], "canvas_size": Vector2.ZERO, "pos": {}} # 布局缓存(_layout 纯函数于 (nodes, canvas)——键=nodes 内容等价+画布尺寸,图态换代或画布尺寸变即自然失效;探针直读冻结) @@ -74,15 +79,15 @@ func _init() -> void: base.add_child(tex) _map.layer_decor.noise_rect = tex # 世界侧 tint 承载面(换层色板渐变挂点——§2.7 切片档) base.add_child(UiKit.vignette_rect()) - var center := CenterContainer.new() - center.set_anchors_preset(Control.PRESET_FULL_RECT) - base.add_child(center) - var box := VBoxContainer.new() - box.add_theme_constant_override("separation", 10) - center.add_child(box) - _build_top_bar(box) - _build_canvas(box) - _build_status_bar(box) + # 三段纵排页(布局决策 5 卷轴视口:顶栏/标题在上、图区视口扩张充盈、底栏在下—— + # 视口为窗口屏为框,画布随图生长可超屏) + var page := VBoxContainer.new() + page.set_anchors_preset(Control.PRESET_FULL_RECT) + page.add_theme_constant_override("separation", 10) + base.add_child(page) + _build_top_bar(page) + _build_canvas(page) + _build_status_bar(page) visible = false @@ -122,14 +127,21 @@ func _build_top_bar(box: VBoxContainer) -> void: box.add_child(_map.title) -## 节点图画布域(图上点击 = 唯一提交通道——布局决策 2;mouse_filter STOP 接悬停/点击) +## 节点图区(卷轴视口承载——布局决策 5):视口件为窗口(裁切/offset/clamp/跟随/指引), +## 画布为其子件;图上点击 = 唯一提交通道(布局决策 2),拖拽/滚轮判定留在画布 _gui_input +## (事件流同源——悬停/选路同源,视口件不做父子事件路由) func _build_canvas(box: VBoxContainer) -> void: _map.canvas = Control.new() _map.canvas.custom_minimum_size = Vector2(620, 240) _map.canvas.mouse_filter = Control.MOUSE_FILTER_STOP _map.canvas.draw.connect(_draw_graph.bind(_map.canvas)) _map.canvas.gui_input.connect(_on_canvas_input) - box.add_child(_map.canvas) + _map.canvas.mouse_exited.connect(_on_canvas_mouse_exited) + _map.viewport = LevelMapViewport.new() + _map.viewport.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _map.viewport.size_flags_vertical = Control.SIZE_EXPAND_FILL + box.add_child(_map.viewport) + _map.viewport.setup(_map.canvas, _viewport_anchor, Vector2(1280, 800)) # 兜底尺寸=设计基座(实况窗口以布局为准) ## 底部状态栏域(合并单条:亮米面板一站读齐=头像+生命+属性+本关加成/流派件——demo ④; @@ -297,6 +309,9 @@ func open_choices(cands: Array, runner: LevelRunner) -> void: _fit_canvas(_map.runner.nodes()) _apply_level_tint(int(_map.runner.level_row.get("level_index", 0))) visible = true + # 视口锚定(布局决策 5):入口停点(尚无当前节点)=首锚即达;其余停点/兑付回图=平滑跟随 + # (停点切换与兑付确认回图——减动效开由视口件跳终态) + _map.viewport.reanchor(_map.runner.current_node_id() >= 0) if not was_visible: # 独立屏淡入(spec Transitions:约 200ms;alpha 淡入非明度变化——art-bible §2 明度恒定) modulate.a = 0.0 @@ -323,6 +338,7 @@ func _fit_canvas(nodes: Array) -> void: SLOT_BASE_Y + maxslot * SLOT_STEP_Y + NODE_HALF.y + 24.0) # +24 = 类型文字标签行 _content_size = _content_size.max(Vector2(620, 256)) # 下限(抬高容纳标签行) _map.canvas.custom_minimum_size = _content_size + _map.canvas.size = _content_size # 同帧收敛(视口锚点/包络数学与 _layout 同源读画布尺寸——容器延迟收敛不等锚点) # 换层色板渐变挂点(art-bible §2.7 下探过渡切片档——关卡结算确认→下一关→独立屏淡入链路上的 @@ -356,20 +372,70 @@ func apply_status(payload: Dictionary) -> void: # ---- 图上命中(布局决策 2——唯一提交通道;探针 press_candidate 同路径) ---- +# 拖拽与点击以拖动距离阈值区分(布局决策 5):按下起跟踪拖距,超阈值进拖拽态逐帧平移视口, +# 抬杆不提交;未超阈值的完整点击(按下→抬杆)=提交沿——命中候选即提交,语义零改动。 func _on_canvas_input(ev: InputEvent) -> void: if _map.runner == null or not visible: return - if ev is InputEventMouseMotion: - var nid := _candidate_at(ev.position) # gui_input 事件坐标 = 接收控件局部坐标(push_input 同成立) - if nid != _hover_id: - _hover_id = nid - _map.canvas.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if nid >= 0 else Control.CURSOR_ARROW + if ev is InputEventMouseButton and ev.pressed and _wheel_pan_step(ev.button_index) != 0.0: + _map.viewport.pan_by(Vector2(_wheel_pan_step(ev.button_index), 0)) + elif ev is InputEventMouseMotion: + _on_canvas_motion(ev) + elif ev is InputEventMouseButton and ev.button_index == MOUSE_BUTTON_LEFT: + _on_canvas_click(ev) + + +## 悬停预承诺与拖拽态推进沿(画布局部坐标——gui_input 事件流同源) +func _on_canvas_motion(ev: InputEventMouseMotion) -> void: + if _drag_active: + _map.viewport.pan_by(ev.relative) # 拖拽态逐帧平移(视口件执行 clamp——本屏只判定) + return + if _drag_press_pos.x >= 0.0 and ev.position.distance_to(_drag_press_pos) >= DRAG_THRESHOLD: + _drag_active = true + _drag_press_pos = Vector2(-1, -1) + if _hover_id != -1: # 进拖拽态清悬停预承诺(拖拽非选路意图) + _hover_id = -1 + _map.canvas.mouse_default_cursor_shape = Control.CURSOR_ARROW _map.canvas.queue_redraw() - elif ev is InputEventMouseButton and ev.button_index == MOUSE_BUTTON_LEFT and ev.pressed: - var nid2 := _candidate_at(ev.position) - if nid2 >= 0: - _submit_choice(nid2) + _map.viewport.pan_by(ev.relative) # 跨阈值帧位移计入平移(拖距全长=平移全长) + return + var nid := _candidate_at(ev.position) # gui_input 事件坐标 = 接收控件局部坐标(push_input 同成立) + if nid != _hover_id: + _hover_id = nid + _map.canvas.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if nid >= 0 else Control.CURSOR_ARROW + _map.canvas.queue_redraw() + + +## 点击沿与拖拽抬杆(未超阈值的完整点击=提交沿——布局决策 2 语义零改动) +func _on_canvas_click(ev: InputEventMouseButton) -> void: + if ev.pressed: + _drag_press_pos = ev.position + _drag_active = false + return + _drag_press_pos = Vector2(-1, -1) + if _drag_active: + _drag_active = false # 拖拽抬杆=平移结束,不触发选路提交(布局决策 5) + return + var nid2 := _candidate_at(ev.position) + if nid2 >= 0: + _submit_choice(nid2) + + +## 滚轮横滚步进(代拍板:下/右=向右前进向——视线移向深层,画布左移;上/左反向;非滚轮=0) +func _wheel_pan_step(button_index: int) -> float: + if button_index == MOUSE_BUTTON_WHEEL_DOWN or button_index == MOUSE_BUTTON_WHEEL_RIGHT: + return -WHEEL_STEP + if button_index == MOUSE_BUTTON_WHEEL_UP or button_index == MOUSE_BUTTON_WHEEL_LEFT: + return WHEEL_STEP + return 0.0 + + +## 拖距跟踪器生命周期清理(按下即武装——鼠标离画布且未入拖拽态时解除, +## 防丢失 release 沿后的残留 armed 态把后续 motion 误判成拖拽激活) +func _on_canvas_mouse_exited() -> void: + if not _drag_active: + _drag_press_pos = Vector2(-1, -1) # 候选命中:位置 = 布局公式(与 _draw_graph 同源),矩形 = 节点方块 + 外扩容差;非候选格不响应 @@ -413,6 +479,42 @@ func press_candidate(index: int) -> void: _submit_choice(nid) +# ---- 视口锚点(布局决策 5:当前节点居中;入口停点尚无当前节点=锚入口组包围盒居中) ---- + +## 锚点画布坐标提供方(构造注入视口件——本屏供数,视口件只消费) +func _viewport_anchor() -> Vector2: + if _map.runner == null: + return _content_size / 2.0 + var nodes: Array[Variant] = _map.runner.nodes() + var pos := _layout(nodes, _map.canvas) + var cur_id: int = _map.runner.current_node_id() + if cur_id < 0: + return _entry_group_center(nodes, pos) # 入口停点(尚未走出第一步) + var cur := _node_by_id(cur_id, nodes) + if cur.is_empty(): + return _content_size / 2.0 + return pos[cur] + + +func _entry_group_center(nodes: Array, pos: Dictionary) -> Vector2: + # 入口组 = 全部无入边节点(与 _assign_layers 入口判据同源);多入口取包围盒居中 + var inbound := {} + for node in nodes: + for tid in _out_ids(node, nodes): + inbound[tid] = true + var lo := Vector2(INF, INF) + var hi := Vector2(-INF, -INF) + for node in nodes: + if inbound.has(_id_of(node)): + continue + var p: Vector2 = pos[node] + lo = lo.min(p) + hi = hi.max(p) + if lo.x > hi.x: + return _content_size / 2.0 # 空图兜底(无入口——不达路径,防御位) + return (lo + hi) / 2.0 + + func _id_of(node: Dictionary) -> int: # 编号显示经行进器读面(编号空间解析单一出口——单链 = 下标 / 图 = node_depth) return _map.runner.node_id_of(node) diff --git a/game/presentation/views/level_map_viewport.gd b/game/presentation/views/level_map_viewport.gd new file mode 100644 index 00000000..70c5642e --- /dev/null +++ b/game/presentation/views/level_map_viewport.gd @@ -0,0 +1,190 @@ +## 卷轴视口承载件(level-map.md 布局决策 5——关卡画面选路屏):画布宿主与裁切、平移状态 +## 与 clamp、锚点跟随与平滑滚动、指引件(屏缘渐隐带+平移提示 chip)。 +## 纯呈现态:平移位置零核心写、不进存档(重进本屏按锚点规则重新定位);跟随只随停点切换/ +## 兑付回图发生(零自动推进)。拖拽/滚轮判定留画布 _gui_input(事件流同源——悬停/选路同源), +## 本件只管裁切/offset/clamp/跟随/指引;画布包络=画布实际尺寸(open_choices 拟合后同帧收敛)。 +class_name LevelMapViewport +extends Control + +const SCROLL_DUR := 0.3 # 平滑滚动时长(布局决策 5 provisional 0.3s 单段缓动) +const FADE_BAND_W := 70.0 # 屏缘渐隐带宽(布局决策 5/像素级判据②) +const CHIP_SIZE := Vector2(240, 30) # 平移提示 chip(像素级判据③——图区底部居中) +const CHIP_CORNER := 6 # chip 圆角(布局决策 5) +const CHIP_BOTTOM_GAP := 24.0 # chip 距视口底缘(效果图档同族取值) +const CHIP_TEXT := "◄ 拖拽画布 / 滚轮平移 ►" # 14px(FS_HINT 档——布局决策 5) +const EDGE_EPS := 0.5 # 包络边界判定容差(浮点平移累积) + +var _canvas: Control +var _anchor_provider: Callable # () -> Vector2 锚点画布坐标(screen 提供——当前节点/入口组) +var _viewport_size := Vector2.ZERO # 窗口尺寸兜底(setup 注入设计基座——实况窗口以控件 size 为准,布局晚于构造) +var _offset := Vector2.ZERO: # 画布左上角在视口件坐标系的位置(纯呈现态——直驱画布 position) + set(value): + _offset = value + if _canvas != null: + _canvas.position = _offset +var _view_tween: Tween +var _guided_visible := false # 指引件可见态(画布超窗才暗示——浅图无平移余量即隐藏) + + +## 一次交齐注入(code-conventions §1.6 拆分交接:画布宿主、锚点来源、窗口兜底尺寸) +func setup(canvas: Control, anchor_provider: Callable, viewport_size: Vector2) -> void: + _canvas = canvas + _anchor_provider = anchor_provider + _viewport_size = viewport_size + clip_contents = true + add_child(canvas) + _build_guidance() + + +func _notification(what: int) -> void: + # 窗口实况尺寸落定/变化即重锚(首开布局晚于 open_choices 的锚定——隐藏子树容器排序 + # 首个可见帧才传播,4.7 行为;实况窗口以本件 size 为准,落定即收敛锚点) + if what == NOTIFICATION_RESIZED and _canvas != null and _anchor_provider.is_valid(): + reanchor(false) + + +## 锚点跟随(布局决策 5:锚点居中;smooth=停点切换/兑付回图平滑滚动,减动效开=跳终态; +## 重入先杀在飞 tween 再建新——快速连续停点切换防叠加) +func reanchor(smooth: bool) -> void: + if _canvas == null or not _anchor_provider.is_valid(): + return + var target := _clamped_center_offset(_anchor_provider.call()) + _kill_scroll_tween() + if not smooth or ArmedTimer.motion_still(): + _offset = target + _sync_guidance() + return + _view_tween = create_tween() + _view_tween.tween_property(self, "_offset", target, UiKit.motion(SCROLL_DUR)) \ + .set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT) + _sync_guidance() + + +## 手动平移(画布 _gui_input 判定后调用——拖拽沿逐帧/滚轮步进;纯呈现零核心写) +func pan_by(delta: Vector2) -> void: + if _canvas == null: + return + _kill_scroll_tween() + var win := _window_size() + _offset = Vector2( + _axis_pan_offset(_offset.x, delta.x, _canvas.size.x, win.x), + _axis_pan_offset(_offset.y, delta.y, _canvas.size.y, win.y)) + _sync_guidance() + + +## 视口中线的画布坐标(锚点居中判据投影面——浅图=画布中心,深图=夹紧域内当前注视点) +func viewport_center_canvas_pos() -> Vector2: + return _window_size() / 2.0 - _offset + + +## 视口是否钉在画布包络边界(任一超窗轴 offset 顶边——clamp 判据面) +func is_clamped_at_edge() -> bool: + var win := _window_size() + if _canvas == null: + return false + return _axis_at_edge(_offset.x, _canvas.size.x, win.x) \ + or _axis_at_edge(_offset.y, _canvas.size.y, win.y) + + +func _kill_scroll_tween() -> void: + if _view_tween != null and _view_tween.is_valid(): + _view_tween.kill() + _view_tween = null + + +## 单轴锚定 offset:画布不超窗=居中分支(夹紧域退化——布局决策 5); +## 超窗=锚点夹紧在画布包络内,锚点落视口中线(offset = 半窗 − 锚点) +func _axis_center_offset(anchor: float, canvas_len: float, half_win: float) -> float: + if canvas_len <= half_win * 2.0: + return half_win - canvas_len / 2.0 + return half_win - clampf(anchor, half_win, canvas_len - half_win) + + +func _clamped_center_offset(anchor: Vector2) -> Vector2: + var half := _window_size() / 2.0 + return Vector2( + _axis_center_offset(anchor.x, _canvas.size.x, half.x), + _axis_center_offset(anchor.y, _canvas.size.y, half.y)) + + +## 单轴平移收敛:居中轴零平移余量不动;超窗轴 offset 收敛在 [半窗−画布, 0](不出露屏底空档) +func _axis_pan_offset(cur: float, delta: float, canvas_len: float, win_len: float) -> float: + if canvas_len <= win_len: + return cur + return clampf(cur + delta, win_len - canvas_len, 0.0) + + +func _axis_at_edge(offset: float, canvas_len: float, win_len: float) -> bool: + if canvas_len <= win_len: + return false + return offset >= -EDGE_EPS or offset <= win_len - canvas_len + EDGE_EPS + + +## 实况窗口优先(容器布局晚于构造——注入值只兜构造帧数学) +func _window_size() -> Vector2: + if size.x > 0.0 and size.y > 0.0: + return size + return _viewport_size + + +## 指引件(布局决策 5/像素级判据②③):屏缘渐隐带盖在画布之上+图区底部居中提示 chip; +## 可见性=画布超窗(_sync_guidance 随锚定/平移刷新) +func _build_guidance() -> void: + var band_l := TextureRect.new() + band_l.name = "FadeL" + band_l.texture = _fade_gradient(false) + band_l.set_anchors_preset(Control.PRESET_LEFT_WIDE) + band_l.offset_right = FADE_BAND_W + band_l.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + band_l.mouse_filter = Control.MOUSE_FILTER_IGNORE + add_child(band_l) + var band_r := TextureRect.new() + band_r.name = "FadeR" + band_r.texture = _fade_gradient(true) + band_r.set_anchors_preset(Control.PRESET_RIGHT_WIDE) + band_r.offset_left = -FADE_BAND_W + band_r.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + band_r.mouse_filter = Control.MOUSE_FILTER_IGNORE + add_child(band_r) + var chip := PanelContainer.new() + chip.name = "PanHint" + chip.add_theme_stylebox_override("panel", + UiKit.panel_flat(UiKit.HP_BAR_BACK, UiKit.STONE, CHIP_CORNER, 1, Vector4(12, 5, 12, 5))) + chip.set_anchors_preset(Control.PRESET_CENTER_BOTTOM) + chip.offset_left = -CHIP_SIZE.x / 2.0 + chip.offset_right = CHIP_SIZE.x / 2.0 + chip.offset_top = -CHIP_SIZE.y - CHIP_BOTTOM_GAP + chip.offset_bottom = -CHIP_BOTTOM_GAP + chip.custom_minimum_size = CHIP_SIZE + chip.mouse_filter = Control.MOUSE_FILTER_IGNORE + var chip_label := UiKit.label(CHIP_TEXT, UiKit.FS_HINT, false, UiKit.WARM) + chip_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + chip_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + chip.add_child(chip_label) + add_child(chip) + _sync_guidance() + + +## 屏缘渐隐渐变(色取屏底同族 BG_ROOT——代拍板:不透明端 0.9 单段衰减) +func _fade_gradient(transparent_at_left: bool) -> GradientTexture2D: + var grad := Gradient.new() + grad.offsets = PackedFloat32Array([0.0, 1.0]) + grad.colors = PackedColorArray([Color(UiKit.BG_ROOT, 0.9), Color(UiKit.BG_ROOT, 0.0)]) + var tex := GradientTexture2D.new() + tex.gradient = grad + tex.fill_from = Vector2(1, 0.5) if transparent_at_left else Vector2(0, 0.5) + tex.fill_to = Vector2(0, 0.5) if transparent_at_left else Vector2(1, 0.5) + tex.width = int(FADE_BAND_W) + return tex + + +func _sync_guidance() -> void: + var win := _window_size() + var over := _canvas != null and (_canvas.size.x > win.x or _canvas.size.y > win.y) + if over == _guided_visible: + return + _guided_visible = over + for nm in ["FadeL", "FadeR", "PanHint"]: + var node := get_node_or_null(NodePath(nm)) as Control + if node != null: + node.visible = over diff --git a/game/presentation/views/level_map_viewport.gd.uid b/game/presentation/views/level_map_viewport.gd.uid new file mode 100644 index 00000000..0e5acf00 --- /dev/null +++ b/game/presentation/views/level_map_viewport.gd.uid @@ -0,0 +1 @@ +uid://dgin6f6214cbg diff --git a/game/probe/level_map_shot_probe.gd b/game/probe/level_map_shot_probe.gd new file mode 100644 index 00000000..0f920158 --- /dev/null +++ b/game/probe/level_map_shot_probe.gd @@ -0,0 +1,91 @@ +# level_map_shot_probe.gd — 关卡画面视口 golden 采集探针(卷轴视口批 golden 扩项) +# 两 Shot 面(2026-11-05 用户裁:只对拍关卡最开始与关卡 Boss 节点,中间过程不对拍): +# lmap_entry.png = 入口停点(关卡最开始——锚点=入口组包围盒中心夹紧) +# lmap_boss.png = 中段停点锚定末列(Boss 紫赤描边列入屏——viewport_center 夹紧右缘) +# 采集确定性 = 固定种子单关直构(LevelRunner.start_level_external defer_entry / resume_state, +# 零 RNG 消耗路径)+ 探针隔离档(无断点、出厂设置 1280×800);运行=窗口化(--headless 禁—— +# 渲染闸机器面吃真实渲染帧)。截图经 user args 目录落盘,基线换代走 golden_baseline_refresh.sh --lmap。 +# 运行:Godot_console.exe --path game res://probe/level_map_shot_probe.tscn -- +extends Node + +const SEED_L3 := 910212 # 深图固定种子(与 level_map_viewport_probe 同源——图确定性承载) + +func _ready() -> void: + var args := OS.get_cmdline_user_args() + var out_dir := args[0] if args.size() > 0 else "user://" + await get_tree().process_frame + var host := Control.new() + add_child(host) + host.size = get_viewport().get_visible_rect().size + var screen = load("res://presentation/views/level_map_screen.gd").new() + screen.set_anchors_preset(Control.PRESET_FULL_RECT) + host.add_child(screen) + for i in range(6): + await get_tree().process_frame # 布局收敛(视口实况尺寸落定→resize 重锚) + # Shot①:入口停点(关卡最开始) + var runner = _make_runner(true, -1) + screen.open_choices(_cand_nodes(runner), runner) + await _settle() + _shot(out_dir, "lmap_entry.png") + # Shot②:中段停点锚定末列(布局 x 最深的有出边节点——id 不按深度排,锚定后 Boss 列入屏) + var boss_stop := _deepest_with_outs(screen, runner) + runner = _make_runner(false, boss_stop) + screen.open_choices(_cand_nodes(runner), runner) + await _settle() + _shot(out_dir, "lmap_boss.png") + print("[level_map_shot_probe] PASS — 两 Shot 落盘(entry+boss → %s)" % out_dir) + get_tree().quit(0) + + +func _settle() -> void: + for i in range(8): + await get_tree().process_frame + await get_tree().create_timer(0.5).timeout # 平滑滚动终态余量(reanchor tween 0.3s) + + +func _make_runner(entry: bool, node_id: int): + var rng = load("res://core/rng_service.gd").new() + rng.new_adventure(SEED_L3) + var runner = load("res://core/level_runner.gd").new(get_node("/root/ConfigDB"), rng) + var ok: bool + if entry: + ok = runner.start_level_external(3, Callable(runner, "rest_heal_amount"), true) + else: + ok = runner.start_level_external(3, Callable(runner, "rest_heal_amount"), + false, {"node_id": node_id, "visited_ids": [node_id]}) + if not ok: + push_error("[level_map_shot_probe] 状态注入失败(entry=%s node=%d)" % [str(entry), node_id]) + get_tree().quit(1) + return runner + + +func _cand_nodes(runner) -> Array: + var ids: Array = runner.next_candidate_ids() + var out: Array = [] + for node in runner.nodes(): + if ids.has(runner.node_id_of(node)): + out.append(node) + return out + + +func _deepest_with_outs(screen, runner) -> int: + var pos: Dictionary = screen.layout_probe(runner.nodes()) + var best := -1 + var best_x := -INF + for node in runner.nodes(): + if runner.node_out_ids(node).is_empty(): + continue + var x: float = pos[node].x + if x > best_x: + best_x = x + best = runner.node_id_of(node) + return best + + +func _shot(out_dir: String, fname: String) -> void: + DirAccess.make_dir_recursive_absolute(out_dir) # 落盘目录自建(save_png 对缺目录静默失败——刷新链不预建) + var img := get_viewport().get_texture().get_image() + var err := img.save_png(out_dir.path_join(fname)) + if err != OK: + push_error("[level_map_shot_probe] 截图落盘失败 %s(err=%d)" % [fname, err]) + get_tree().quit(1) diff --git a/game/probe/level_map_shot_probe.gd.uid b/game/probe/level_map_shot_probe.gd.uid new file mode 100644 index 00000000..8e1aa67e --- /dev/null +++ b/game/probe/level_map_shot_probe.gd.uid @@ -0,0 +1 @@ +uid://j4ax48si4nq5 diff --git a/game/probe/level_map_shot_probe.tscn b/game/probe/level_map_shot_probe.tscn new file mode 100644 index 00000000..d34d0546 --- /dev/null +++ b/game/probe/level_map_shot_probe.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://probe/level_map_shot_probe.gd" id="1"] + +[node name="LevelMapShotProbe" type="Node"] +script = ExtResource("1") diff --git a/game/probe/level_map_viewport_probe.gd b/game/probe/level_map_viewport_probe.gd new file mode 100644 index 00000000..6c1ede3a --- /dev/null +++ b/game/probe/level_map_viewport_probe.gd @@ -0,0 +1,389 @@ +# level_map_viewport_probe.gd — 卷轴视口行为探针(卷轴视口批 R1–R8 验收载体) +# 判的是 level-map.md 布局决策 5 的视口行为四条:浅图整幅居中无裁切(R1)、停点切换/兑付回图 +# 当前节点居中锚定——平滑滚动与减动效瞬切(R2/R7)、拖拽/滚轮手动平移与 clamp 包络—— +# 首末列可达(R3/R4/R5)、拖拽不触发选路提交(R6)、指引件可见性与几何(R8,像素级判据②③)。 +# 真实入口 = 合成拖拽沿/滚轮沿/点击沿经 push_input 进引擎输入管线打在真实画布 _gui_input 上 +# (probe_ui_util.push_drag_at/push_wheel_at/push_click_at);状态注入 = 直调核心层写口构造 +# 单关停点(LevelRunner.start_level_external——defer_entry=入口停点 / resume_state=断点续玩 +# 重建到任意停点,零 RNG 消耗;test-conventions §5.4 自动化测试直构口径),零 main 冒险流程 +# 依赖:真实屏 = LevelMapScreen 实例 + 真实单关 runner。 +# 运行:Godot_console.exe --headless --path . res://probe/level_map_viewport_probe.tscn +extends Node + +const ProbeUiUtil := preload("res://probe/probe_ui_util.gd") +const ViewportScript := preload("res://presentation/views/level_map_viewport.gd") + +const SEED_L1 := 910211 # 浅图停点种子(L1 单链——画布 846.5×256 全幅入窗) +const SEED_L3 := 910212 # 深图停点种子(L3 图——画布超 1280 窗宽,滚动视口生效面) + +var _problems: Array[String] = [] +var _checks := 0 +var _runner # 当前停点的单关 runner(open_choices 驱动随记——查证面共用) + + +func _check(desc: String, ok: bool) -> void: + _checks += 1 + if not ok: + _problems.append(desc) + + +func _ready() -> void: + await get_tree().process_frame + await get_tree().process_frame + var host := Control.new() + add_child(host) + host.size = get_viewport().get_visible_rect().size # 显式置底座(父非 Control 锚点不解析) + var screen = load("res://presentation/views/level_map_screen.gd").new() + screen.set_anchors_preset(Control.PRESET_FULL_RECT) + host.add_child(screen) + await get_tree().process_frame + await get_tree().process_frame # 布局收敛先行(视口实况窗口尺寸就位后再开停点——锚点数学以实况窗口为准) + + var vp = _find_viewport(screen) + if vp == null: + _problems.append("卷轴视口承载件未挂入关卡屏(LevelMapViewport 实例缺位)") + _finish() + return + + await _probe_shallow_no_crop(screen, vp) + await _probe_deep_entry_clamp(screen, vp) + await _probe_wheel_pan(screen, vp) + await _probe_drag_pan(screen, vp) + await _probe_clamp_bounds_and_columns(screen, vp) + await _probe_drag_no_submit(screen, vp) + await _probe_follow_anchor(screen, vp) + await _probe_smooth_scroll_reduced_motion(screen, vp) + await _probe_guidance_chrome(screen, vp) + await _probe_shallow_guidance_hidden(screen, vp) + _finish() + + +## R1 浅图整幅居中无裁切+画布入视口件承载(画布=视口首子件——setup 注入序) +func _probe_shallow_no_crop(screen, vp) -> void: + _runner = _make_runner_entry(1, SEED_L1) + screen.open_choices(_cand_nodes(_runner), _runner) + await get_tree().process_frame + await get_tree().process_frame + var canvas: Control = vp.get_child(0) as Control + if canvas == null: + _problems.append("R1 画布实例缺位(视口首子件非 Control)") + return + _check("R1 浅图画布全幅入窗(canvas=%s window=%s)〔卷轴批 V1〕" % [canvas.size, vp.size], + canvas.size.x <= vp.size.x + 0.5 and canvas.size.y <= vp.size.y + 0.5) + var want: Vector2 = (vp.size - canvas.size) / 2.0 + _check("R1 居中分支 offset 定位(want=%s got=%s)〔卷轴批 V1〕" % [want, canvas.position], + canvas.position.distance_to(want) <= 0.5) + _check("R1 画布矩形在窗内无裁切〔卷轴批 V1〕", vp.get_global_rect().encloses(canvas.get_global_rect())) + + +## R5 前置+深图入口停点:锚点=入口组包围盒中心夹紧(spec :101——多入口并列取包围盒居中) +func _probe_deep_entry_clamp(screen, vp) -> void: + _runner = _make_runner_entry(3, SEED_L3) + screen.open_choices(_cand_nodes(_runner), _runner) + await get_tree().process_frame + await get_tree().process_frame + var center: Vector2 = vp.viewport_center_canvas_pos() + var half: float = vp.size.x / 2.0 + var canvas_len: float = _canvas_len(screen) + _check("R5 入口锚定后视口中线在夹紧域内(center.x=%.1f ∈ [%.1f, %.1f])〔卷轴批 V5〕" % + [center.x, half, canvas_len - half], + center.x >= half - 0.5 and center.x <= canvas_len - half + 0.5) + # 入口停点候选 = 入口节点——包围盒中心夹紧即锚点(本档图入口分布跨图,中心落夹紧域) + var nodes: Array = _runner.nodes() + var pos: Dictionary = screen.layout_probe(nodes) + var ids: Array = _runner.next_candidate_ids() + var lo := INF + var hi := -INF + for node in nodes: + if ids.has(_runner.node_id_of(node)): + lo = minf(lo, pos[node].x) + hi = maxf(hi, pos[node].x) + var want: float = clampf((lo + hi) / 2.0, half, canvas_len - half) + _check("R5 入口组包围盒中心=锚点(bbox=%.1f center=%.1f want=%.1f)〔卷轴批 V5〕" % + [(lo + hi) / 2.0, center.x, want], absf(center.x - want) <= 0.5) + + +## R4 滚轮横滚移动视口(下=向右前进向——代拍板映射;窗内平移不钉边界,与 R5 两极 true 构成翻转) +func _probe_wheel_pan(screen, vp) -> void: + var before: float = vp.viewport_center_canvas_pos().x + var pos: Vector2 = vp.get_global_rect().get_center() + ProbeUiUtil.push_wheel_at(self, pos, true) + await get_tree().process_frame + await get_tree().process_frame + var after: float = vp.viewport_center_canvas_pos().x + _check("R4 滚轮下单档平移 +60(before=%.1f after=%.1f)〔卷轴批 V4〕" % [before, after], + absf(after - before - 60.0) <= 0.5) + _check("R4 窗内平移不钉边界(is_clamped_at_edge=false)〔卷轴批 V4〕", not vp.is_clamped_at_edge()) + ProbeUiUtil.push_wheel_at(self, pos, false) + await get_tree().process_frame + await get_tree().process_frame + _check("R4 滚轮上回移 −60〔卷轴批 V4〕", + absf(vp.viewport_center_canvas_pos().x - before) <= 0.5) + + +## R3 拖拽平移移动视口(窗内位移量=拖拽位移,无 clamp 干扰) +func _probe_drag_pan(screen, vp) -> void: + # 先滚入窗内区(两档 +120——避开左包络边界的 clamp 吸收),再拖拽核位移 + var pos: Vector2 = vp.get_global_rect().get_center() + ProbeUiUtil.push_wheel_at(self, pos, true) + ProbeUiUtil.push_wheel_at(self, pos, true) + await get_tree().process_frame + await get_tree().process_frame + var before: float = vp.viewport_center_canvas_pos().x + ProbeUiUtil.push_drag_at(self, pos, Vector2(-40, 0)) + await get_tree().process_frame + await get_tree().process_frame + var after: float = vp.viewport_center_canvas_pos().x + _check("R3 拖拽左移 −40 平移量逐位一致(画布随手——视线 +40,before=%.1f after=%.1f)〔卷轴批 V3〕" % [before, after], + absf(after - before - 40.0) <= 0.5) + # 复位到左包络边界,给后续探针稳定起点 + ProbeUiUtil.push_drag_at(self, pos, Vector2(2000, 0)) + await get_tree().process_frame + await get_tree().process_frame + + +## R5 clamp 边界与首末列可达(拖拽两极) +func _probe_clamp_bounds_and_columns(screen, vp) -> void: + var nodes: Array = _runner.nodes() + var pos: Dictionary = screen.layout_probe(nodes) + var first_node := _extreme_node(pos, nodes, true) + var last_node := _extreme_node(pos, nodes, false) + var vp_rect: Rect2 = vp.get_global_rect() + # 右极:拖拽超包络 → 视口钉左缘,首列(深度 0)入屏 + ProbeUiUtil.push_drag_at(self, vp_rect.get_center(), Vector2(2000, 0)) + await get_tree().process_frame + await get_tree().process_frame + var center: float = vp.viewport_center_canvas_pos().x + var half: float = vp.size.x / 2.0 + _check("R5 拖拽超包络收敛夹紧域(center.x=%.1f)〔卷轴批 V5〕" % center, + center >= half - 0.5 and center <= _canvas_len(screen) - half + 0.5) + _check("R5 右极视口钉边界(is_clamped_at_edge=true)〔卷轴批 V5〕", vp.is_clamped_at_edge()) + _check("R5 首列(深度 0)中心入屏〔卷轴批 V5〕", + vp_rect.has_point(_node_screen_pos(screen, pos, first_node))) + # 左极:反向超包络 → 视口钉右缘,末列(含 Boss 紫赤描边列)入屏 + ProbeUiUtil.push_drag_at(self, vp_rect.get_center(), Vector2(-2000, 0)) + await get_tree().process_frame + await get_tree().process_frame + _check("R5 左极视口钉边界(is_clamped_at_edge=true)〔卷轴批 V5〕", vp.is_clamped_at_edge()) + _check("R5 末列中心入屏(拖拽两极可达——布局决策 5)〔卷轴批 V5〕", + vp_rect.has_point(_node_screen_pos(screen, pos, last_node))) + + +## R6 拖拽不触发选路提交;阈值另一侧完整点击仍提交(提交语义零改动) +func _probe_drag_no_submit(screen, vp) -> void: + # 重建到中段可居中停点(候选=停点出边——候选节点居屏中可拖可达) + var stops := _centerable_stops(screen, vp) + if stops.size() < 3: + _problems.append("R6 可居中停点不足(=%d)" % stops.size()) + return + _runner = _make_runner_at_stop(3, SEED_L3, _runner.node_id_of(stops[0])) + screen.open_choices(_cand_nodes(_runner), _runner) + await get_tree().process_frame + await get_tree().process_frame + var nodes: Array = _runner.nodes() + var cands: Array = _runner.next_candidate_ids() + var target := {} + for node in nodes: + if cands.has(_runner.node_id_of(node)): + target = node + break + if target.is_empty(): + _problems.append("R6 候选节点缺位(候选=%s)" % str(cands)) + return + var gpos: Vector2 = _node_screen_pos(screen, screen.layout_probe(nodes), target) + var submitted: Array = [] + var on_route := func(nid: int) -> void: submitted.append(nid) + screen.route_chosen.connect(on_route) + ProbeUiUtil.push_drag_at(self, gpos, Vector2(120, 24)) + await get_tree().process_frame + await get_tree().process_frame + _check("R6 拖拽沿起止候选上方零提交(submitted=%s)〔卷轴批 V6〕" % str(submitted), submitted.is_empty()) + _check("R6 拖拽后本屏未关〔卷轴批 V6〕", screen.visible) + var gpos2: Vector2 = _node_screen_pos(screen, screen.layout_probe(nodes), target) # 拖拽平移后重算投影(画布随视口移动) + ProbeUiUtil.push_click_at(self, gpos2) + await get_tree().process_frame + await get_tree().process_frame + _check("R6 完整点击(未超阈值)仍提交选路——阈值区分另一侧〔卷轴批 V6〕", submitted.size() == 1) + screen.route_chosen.disconnect(on_route) + + +## R2 停点切换后当前节点居中(视口中线投影 x=640±2——视口满屏宽即屏中线) +func _probe_follow_anchor(screen, vp) -> void: + var stops := _centerable_stops(screen, vp) + if stops.size() < 3: + _problems.append("R2 可居中停点不足(=%d)" % stops.size()) + return + _runner = _make_runner_at_stop(3, SEED_L3, _runner.node_id_of(stops[1])) + screen.open_choices(_cand_nodes(_runner), _runner) + await get_tree().create_timer(0.6).timeout # 平滑滚动终态(0.3s 单段缓动+余量) + var sx: float = _node_screen_pos(screen, screen.layout_probe(_runner.nodes()), stops[1]).x + var want: float = vp.get_global_rect().get_center().x + _check("R2 停点切换后当前节点投影=视口中线(got=%.1f want=%.1f±2)〔卷轴批 V2〕" % [sx, want], + absf(sx - want) <= 2.0) + + +## R7 平滑滚动有 tween 中间帧;减动效开=位置即达(画布坐标域判定) +func _probe_smooth_scroll_reduced_motion(screen, vp) -> void: + var saved_scale: float = UiKit.motion_scale + UiKit.motion_scale = 1.0 + var stops := _centerable_stops(screen, vp) + if stops.size() < 3: + _problems.append("R7 可居中停点不足(=%d)" % stops.size()) + return + # 起点:锚在停点 A(画面已停稳) + _runner = _make_runner_at_stop(3, SEED_L3, _runner.node_id_of(stops[0])) + screen.open_choices(_cand_nodes(_runner), _runner) + await get_tree().create_timer(0.6).timeout + var start: float = vp.viewport_center_canvas_pos().x + # 停点切换 A→B(屏幕保持可见=兑付回图同路径)——平滑滚动 + var target_b: float = screen.layout_probe(_runner.nodes())[stops[1]].x + _runner = _make_runner_at_stop(3, SEED_L3, _runner.node_id_of(stops[1])) + screen.open_choices(_cand_nodes(_runner), _runner) + await get_tree().create_timer(0.05).timeout # 中间帧采样(300ms 单段缓动早段) + var mid: float = vp.viewport_center_canvas_pos().x + var total := absf(target_b - start) + _check("R7 平滑滚动中间帧在途中(start=%.1f mid=%.1f target=%.1f)〔卷轴批 V7〕" % [start, mid, target_b], + total > 4.0 and absf(mid - start) > 0.5 and absf(mid - target_b) > 0.5 + and (mid - start) * (target_b - start) > 0.0) # 同向在途 + await get_tree().create_timer(0.5).timeout + _check("R7 平滑滚动终态到位〔卷轴批 V7〕", + absf(vp.viewport_center_canvas_pos().x - target_b) <= 2.0) + # 减动效置位(UiKit.motion 通道单点——设置面开关写同一值):停点切换 B→C 位置即达 + UiKit.motion_scale = 0.5 + var target_c: float = screen.layout_probe(_runner.nodes())[stops[2]].x + _runner = _make_runner_at_stop(3, SEED_L3, _runner.node_id_of(stops[2])) + screen.open_choices(_cand_nodes(_runner), _runner) + await get_tree().process_frame + await get_tree().process_frame + _check("R7 减动效开=位置即达(got=%.1f target=%.1f)〔卷轴批 V7〕" % + [vp.viewport_center_canvas_pos().x, target_c], + absf(vp.viewport_center_canvas_pos().x - target_c) <= 2.0) + UiKit.motion_scale = saved_scale + + +## R8 指引件可见性与几何(深图超窗——像素级判据②③) +func _probe_guidance_chrome(screen, vp) -> void: + var band_l: Control = vp.get_node_or_null(NodePath("FadeL")) + var band_r: Control = vp.get_node_or_null(NodePath("FadeR")) + var chip: Control = vp.get_node_or_null(NodePath("PanHint")) + if band_l == null or band_r == null or chip == null: + _problems.append("R8 指引件缺位(FadeL/FadeR/PanHint)〔卷轴批 V8〕") + return + _check("R8 深图渐隐带可见〔卷轴批 V8〕", band_l.visible and band_r.visible) + _check("R8 渐隐带宽 70px(L=%.1f R=%.1f)〔卷轴批 V8〕" % [band_l.size.x, band_r.size.x], + absf(band_l.size.x - 70.0) <= 0.5 and absf(band_r.size.x - 70.0) <= 0.5) + _check("R8 平移提示 chip 可见且 240×30(size=%s)〔卷轴批 V8〕" % chip.size, + chip.visible and absf(chip.size.x - 240.0) <= 0.5 and absf(chip.size.y - 30.0) <= 0.5) + _check("R8 chip 图区底部居中(水平中线对窗中线±2)〔卷轴批 V8〕", + absf(chip.get_global_rect().get_center().x - vp.get_global_rect().get_center().x) <= 2.0) + + +## R8 浅图指引件隐藏(无暗示必要) +func _probe_shallow_guidance_hidden(screen, vp) -> void: + _runner = _make_runner_entry(1, SEED_L1) + screen.open_choices(_cand_nodes(_runner), _runner) + await get_tree().process_frame + await get_tree().process_frame + var band_l: Control = vp.get_node_or_null(NodePath("FadeL")) + var chip: Control = vp.get_node_or_null(NodePath("PanHint")) + _check("R8 浅图画布不超窗指引件隐藏〔卷轴批 V8〕", + band_l != null and not band_l.visible and chip != null and not chip.visible) + + +# ---- 驱动与查证工具(状态注入面——直调核心层写口) ---- + +## 入口停点 runner(defer_entry——选路态挂起、候选=入口节点) +func _make_runner_entry(level_index: int, seed_val: int): + var rng = load("res://core/rng_service.gd").new() + rng.new_adventure(seed_val) + var runner = load("res://core/level_runner.gd").new(get_node("/root/ConfigDB"), rng) + if not runner.start_level_external(level_index, Callable(runner, "rest_heal_amount"), true): + _problems.append("状态注入失败:start_level_external(%d, %d) 未受理" % [level_index, seed_val]) + return runner + + +## 中段停点 runner(断点续玩重建——图由种子+参数档重生成同图,零 RNG 消耗) +func _make_runner_at_stop(level_index: int, seed_val: int, node_id: int): + var rng = load("res://core/rng_service.gd").new() + rng.new_adventure(seed_val) + var runner = load("res://core/level_runner.gd").new(get_node("/root/ConfigDB"), rng) + if not runner.start_level_external(level_index, Callable(runner, "rest_heal_amount"), + false, {"node_id": node_id, "visited_ids": [node_id]}): + _problems.append("状态注入失败:断点停点 %d 未受理" % node_id) + return runner + + +func _cand_nodes(runner) -> Array: + var ids: Array = runner.next_candidate_ids() + var out: Array = [] + for node in runner.nodes(): + if ids.has(runner.node_id_of(node)): + out.append(node) + return out + + +## 深图可居中停点选取(有出边且画布 x 落在夹紧域内的节点——锚定不贴边、当前节点投影可居中; +## 按 x 降序去重取第 idx 个——同层同 x 只计一个,层距 122 保证停点间滚动位移充分) +func _centerable_stops(screen, vp) -> Array: + var nodes: Array = _runner.nodes() + var pos: Dictionary = screen.layout_probe(nodes) + var half: float = vp.size.x / 2.0 + var canvas_len: float = _canvas_len(screen) + var by_x_desc: Array = nodes.duplicate() + by_x_desc.sort_custom(func(a, b) -> bool: return pos[a].x > pos[b].x) + var picked: Array = [] + var seen_x := {} + for node in by_x_desc: + if _runner.node_out_ids(node).is_empty(): + continue + var x: float = pos[node].x + if x < half + 20.0 or x > canvas_len - half - 20.0 or seen_x.has(x): + continue + seen_x[x] = true + picked.append(node) + return picked + + +func _find_viewport(screen) -> Control: + return _find_by_script(screen, ViewportScript) + + +func _find_by_script(root: Node, script: Script) -> Control: + if root.get_script() == script: + return root as Control + for child in root.get_children(): + var found := _find_by_script(child, script) + if found != null: + return found + return null + + +func _canvas_len(screen) -> float: + return screen.canvas_probe()["min_size"].x # 画布包络宽(open_choices 拟合值——画布尺寸即它) + + +func _node_screen_pos(screen, pos: Dictionary, node: Dictionary) -> Vector2: + return screen.canvas_probe()["origin"] + pos[node] + + +func _extreme_node(pos: Dictionary, nodes: Array, first: bool) -> Dictionary: + var best := {} + var best_x := INF if first else -INF + for node in nodes: + var x: float = pos[node].x + if (first and x < best_x) or (not first and x > best_x): + best_x = x + best = node + return best + + +func _finish() -> void: + if _checks == 0: + _problems.append("零断言执行(断言数下限闸——三硬闸③)") + if _problems.is_empty(): + print("[level_map_viewport_probe] PASS — 卷轴视口行为断言全绿(%d 项)" % _checks) + get_tree().quit(0) + else: + for p in _problems: + printerr("[level_map_viewport_probe] FAIL — " + p) + get_tree().quit(1) diff --git a/game/probe/level_map_viewport_probe.gd.uid b/game/probe/level_map_viewport_probe.gd.uid new file mode 100644 index 00000000..06779e47 --- /dev/null +++ b/game/probe/level_map_viewport_probe.gd.uid @@ -0,0 +1 @@ +uid://cprbakccv06re diff --git a/game/probe/level_map_viewport_probe.tscn b/game/probe/level_map_viewport_probe.tscn new file mode 100644 index 00000000..5a3ee798 --- /dev/null +++ b/game/probe/level_map_viewport_probe.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://probe/level_map_viewport_probe.gd" id="1"] + +[node name="LevelMapViewportProbe" type="Node"] +script = ExtResource("1") diff --git a/game/probe/probe_ui_util.gd b/game/probe/probe_ui_util.gd index 3f68514c..4e0a5e75 100644 --- a/game/probe/probe_ui_util.gd +++ b/game/probe/probe_ui_util.gd @@ -213,6 +213,48 @@ static func scroll_button_into_view(main, view: Control, btn: Control) -> void: await main.get_tree().process_frame +# 真输入拖拽沿(press→motion×steps→release 成对经 push_input 进引擎输入管线——卷轴视口批 +# R3/R5/R6 驱动沿;沿 push_click_at 同式。relative 按步显式置值——合成事件的位移量由事件 +# 自身承载,被测面按 relative 逐帧平移) +static func push_drag_at(main, pos: Vector2, delta: Vector2, steps: int = 4) -> void: + var down := InputEventMouseButton.new() + down.position = pos + down.global_position = pos + down.button_index = MOUSE_BUTTON_LEFT + down.pressed = true + main.get_viewport().push_input(down, true) + for i in range(1, steps + 1): + var cur := pos + delta * float(i) / float(steps) + var mv := InputEventMouseMotion.new() + mv.position = cur + mv.global_position = cur + mv.relative = delta / float(steps) + main.get_viewport().push_input(mv, true) + var up := InputEventMouseButton.new() + up.position = pos + delta + up.global_position = pos + delta + up.button_index = MOUSE_BUTTON_LEFT + up.pressed = false + main.get_viewport().push_input(up, true) + + +# 真输入滚轮沿(单档滚轮事件经 push_input——卷轴视口批 R4 驱动沿;方向映射在被测面, +# 本工具只发原始滚轮档)。press+release 成对(同 push_wheel 注——只发 press 钉死鼠标焦点)。 +static func push_wheel_at(main, pos: Vector2, wheel_down: bool = true) -> void: + var ev := InputEventMouseButton.new() + ev.position = pos + ev.global_position = pos + ev.button_index = MOUSE_BUTTON_WHEEL_DOWN if wheel_down else MOUSE_BUTTON_WHEEL_UP + ev.pressed = true + main.get_viewport().push_input(ev, true) + var ev2 := InputEventMouseButton.new() + ev2.position = pos + ev2.global_position = pos + ev2.button_index = MOUSE_BUTTON_WHEEL_DOWN if wheel_down else MOUSE_BUTTON_WHEEL_UP + ev2.pressed = false + main.get_viewport().push_input(ev2, true) + + # 营地经进档开层(卡② mn9 新路径——campbtn 已撤;槽模板注入隔离命名空间,防碰真实槽 # user://save_slot_N.json——真实档红线)。创建=激活=暂行开营地(卡③ Q3 乙落点); # 驱动后写盘目标=隔离槽文件(持久化断言读 user://_slot_1.json)。返回 false=新档钮缺失(驱动中断)。 diff --git a/game/probe/ui_input_probe.gd b/game/probe/ui_input_probe.gd index 65f89a51..e3ecde7c 100644 --- a/game/probe/ui_input_probe.gd +++ b/game/probe/ui_input_probe.gd @@ -87,6 +87,14 @@ func _ready() -> void: evf.position = fog_pos evf.global_position = fog_pos main.get_viewport().push_input(evf, true) + # 完整点击=press+release 成对(卷轴视口批阈值语义:拖距跟踪器由按下起武装, + # 单 press 沿会留 armed 态污染后续 motion 判定——判据语义不变:雾下点击零提交) + var evf2 := InputEventMouseButton.new() + evf2.button_index = MOUSE_BUTTON_LEFT + evf2.pressed = false + evf2.position = fog_pos + evf2.global_position = fog_pos + main.get_viewport().push_input(evf2, true) await get_tree().process_frame await get_tree().process_frame ro.route_chosen.disconnect(on_route) @@ -114,6 +122,14 @@ func _ready() -> void: evc.position = gpos evc.global_position = gpos main.get_viewport().push_input(evc, true) + # 完整点击=press+release 成对(卷轴视口批阈值语义:提交沿=未超阈值的抬杆—— + # 判据语义不变:图上真实点击提交选路;单 press 沿不再是提交沿) + var evr := InputEventMouseButton.new() + evr.button_index = MOUSE_BUTTON_LEFT + evr.pressed = false + evr.position = gpos + evr.global_position = gpos + main.get_viewport().push_input(evr, true) await get_tree().process_frame await get_tree().process_frame if main.ui.modals.run.level_screen.visible or main.run_scope.runner.awaiting_choice(): diff --git a/game/sim/adventure_monte_carlo.gd b/game/sim/adventure_monte_carlo.gd index a669679d..4c43b7f6 100644 --- a/game/sim/adventure_monte_carlo.gd +++ b/game/sim/adventure_monte_carlo.gd @@ -72,7 +72,7 @@ func _initialize() -> void: if attune_mode == "on": # 贪心画像:Slots=3(槽位判定源=树同调位节点点亮数,模拟画像=三节点 # 全点亮常量);装配序 = - # 表序 passive 优先至 Slots-1 + 首个 active(slots=3 → [淬毒刃纹, 标中殉爆, 掘进蓄能], + # 表序 passive 优先至 Slots-1 + 首个 active(slots=3 → 表序头三件占满示例, # provisional 画像归调参);Y = 营地升级 Lv1 表值 1。autoplay 默认不刷——Y 仅注入容量, # 刷新消费归玩家策略面(钓鱼归玩家)。 var slots: int = 3 diff --git a/game/sim/level_autoplay.gd b/game/sim/level_autoplay.gd index 13f34021..ec06dab9 100644 --- a/game/sim/level_autoplay.gd +++ b/game/sim/level_autoplay.gd @@ -411,14 +411,12 @@ static func _on_step_row(row: Dictionary, _runner, st: Dictionary, collect_rows: # 封印经济聚合(自结算流水聚合,零新随机源): # ⚠ 行侧判据考证实录:结算段对存续增益实例每轮复发行 effect 行且复用实例 source_class #(battle_orchestrator 状态列举行)——source_class=seal_hit 的行 ≠ 标中事件。事件行唯一判据 - # = 授予行专带 highlight="seal_hit"(实测 87 行 == 受理封印 87 动作,1:1 精确); - # 雷区图谱追加分grant 无高光,以 display_name 后缀补捕(计入事件与合计——同封印第二份兑付)。 + # = 授予行专带 highlight="seal_hit"(实测 87 行 == 受理封印 87 动作,1:1 精确)。 # 满层拒收行(reward_ineffective)零兑付不入。标空事件=归因行(type=seal_miss 每格恰一次); # 标空惩罚合计只入一次性兑付行(flat 直伤 damage/heal + gp_loss escrow 行)——sapped 授予行 # 与其状态复发行行侧不可区分不入合计(登记注记,判读侧按事件数补算)。 if row.source_class == "seal_hit" and row.type != "reward_ineffective": - if String(row.get("highlight", "")) == "seal_hit" \ - or String(row.get("display_name", "")).contains("雷区图谱"): + if String(row.get("highlight", "")) == "seal_hit": st.seal_hit += 1 st.seal_hit_gain += int(row.value) if row.type == "seal_miss": diff --git a/game/tests/README.md b/game/tests/README.md index 5c681ddb..62f9242a 100644 --- a/game/tests/README.md +++ b/game/tests/README.md @@ -160,7 +160,7 @@ tests/ | 四柜台拆解后五界面入口改挂与写口零回归 | 营地五界面 | 五界面意图信号 → `CampShopPresenter` → `MetaProgression.tree_buy()` 写口 | `forge_workbench_test.gd:_run`(承载类同迁断言十二项);`ui_click_probe.gd:_ready`(写口链段) | | 营地篝火回主菜单与笔记本改挂动线 | 营地交互流 | 笔记本命中区 → `CareerOverlay.open()`;篝火命中区 → ConfirmDialog 二次确认 → 主菜单 | `ui_struct_probe.gd:_ready`(career 段);`screen_flow_probe.gd:_ready`(篝火/开合段) | | 关卡画面视口跟随 | 关卡画面(选路枢纽) | 停点切换与兑付回图经真实屏流程;读面 `LevelMapViewport.viewport_center_canvas_pos()` | `level_map_viewport_probe.gd:_probe_follow_anchor`;`level_map_viewport_probe.gd:_probe_smooth_scroll_reduced_motion` | -| 关卡画面视口平移 | 关卡画面(选路枢纽) | 合成拖拽沿与滚轮沿经 `push_input`(`ProbeUiUtil.push_drag_at()`/`push_wheel_at()`) | `level_map_viewport_probe.gd:_probe_drag_pan`;`level_map_viewport_probe.gd:_probe_wheel_pan`;`level_map_viewport_probe.gd:_probe_clamp_bounds`;`level_map_viewport_probe.gd:_probe_drag_no_submit` | +| 关卡画面视口平移 | 关卡画面(选路枢纽) | 合成拖拽沿与滚轮沿经 `push_input`(`ProbeUiUtil.push_drag_at()`/`push_wheel_at()`) | `level_map_viewport_probe.gd:_probe_drag_pan`;`level_map_viewport_probe.gd:_probe_wheel_pan`;`level_map_viewport_probe.gd:_probe_clamp_bounds_and_columns`;`level_map_viewport_probe.gd:_probe_drag_no_submit` | | 关卡画面视口指引 | 关卡画面(选路枢纽) | 深图经 `LevelMapScreen.open_choices()` 真实停点 | `level_map_viewport_probe.gd:_probe_guidance_chrome` | | 营地图鉴占位挂起态与反参照结构纪律 | 营地场景视图 | 布告板命中区 → `CampSceneView._gui_input()`(object_pressed 信号) → 图鉴占位视图;营地根态场景树审计 | `ui_struct_probe.gd:_ready`(占位禁词/禁行段);`ui_struct_probe.gd:_ready`(结构禁词段) | | 营地刷新单口与场景根 HUD 同步 | 营地刷新总线 | 写口尾 → `CampRefreshBus.refresh()` → 现开界面刷新+场景根 HUD 刷新 | `ui_struct_probe.gd:_ready`(顶栏位段);`ui_click_probe.gd:_ready`(写口尾刷新段) | diff --git a/game/tests/golden/README.md b/game/tests/golden/README.md index 4dac9021..8a4a183f 100644 --- a/game/tests/golden/README.md +++ b/game/tests/golden/README.md @@ -4,17 +4,21 @@ `windows/` 与 `mac/` 子目录各持一套,平台内部照常严格比对,跨平台不互比; 平台选择实现单点 = `tools/gate_platform.sh` 的 `gate_platform_id`,比较器 `tools/golden_compare.py` 自动拼平台子目录)。本目录现持:`windows/`(棋盘三图 `board_L1_dungeon` / `board_L2_underdark` / -`board_L3_deep_underdark`,裁剪区 900×760 + 启动态菜单 `menu_boot`,全屏)、 -`level_map`(关卡画面全屏,历史基线留根——现行闸件未对拍它)、 +`board_L3_deep_underdark`,裁剪区 900×760 + 启动态菜单 `menu_boot`,全屏 + 关卡视口两 Shot +`lmap_entry` / `lmap_boss`——2026-11-05 用户裁两 Shot 面:只对拍关卡最开始(入口停点)与 +关卡 Boss 节点(末列锚定),中间过程不对拍)、 +`level_map`(关卡画面全屏,历史基线留根——现行闸件未对拍它,由两 Shot 面接替)、 `palette_baseline/`(色板基线,纯文件内容比对与平台无关)。 比对判据与工具见 `tools/golden_compare.py` -(逐像素灰度差 >16 的像素占比 ≤2%)。 +(逐像素灰度差 >16 的像素占比 ≤2%)。两 Shot 基线采集=`res://probe/level_map_shot_probe.tscn` +(固定种子单关直构——确定性采集),换代入口=`tools/golden_baseline_refresh.sh --lmap`; +他平台(mac)的关卡视口基线尚未冻结——渲染闸对本平台缺基线记 SKIP 不判红,冻结义务归批末呈报。 **未建锚面备案**:营地/设置/战斗三屏与战末结算/三选一/图例/调试浮窗四浮层无改前 golden 锚;这些呈现面的等价证据承载=现役探针/单元测试断言面+实机目检(体验验证清单 ㊵ 节)。 ## 时效判定纪律 -基线冻结于 2026-09-30。**判定先于动作**:每代换代面(视觉/素材/布局改动) +基线冻结于 2026-09-30;关卡视口两 Shot(lmap_entry/lmap_boss)冻结于 2026-11-05——两 Shot 面口径(只对拍入口停点与 Boss 节点)见上文现持清单行。**判定先于动作**:每代换代面(视觉/素材/布局改动) 跑批时先跑一次渲染闸看差异落不落在裁剪区与容差内—— - **判有效**(changed 远低于 2% 阈值):基线**留在位**,不得顺手重制; diff --git a/game/tests/golden/windows/lmap_boss.png b/game/tests/golden/windows/lmap_boss.png new file mode 100644 index 00000000..90d14187 Binary files /dev/null and b/game/tests/golden/windows/lmap_boss.png differ diff --git a/game/tests/golden/windows/lmap_entry.png b/game/tests/golden/windows/lmap_entry.png new file mode 100644 index 00000000..d710cd53 Binary files /dev/null and b/game/tests/golden/windows/lmap_entry.png differ diff --git a/game/tests/unit/battle_settlement/variant_placement_test.gd b/game/tests/unit/battle_settlement/variant_placement_test.gd index 415c86de..620c3be3 100644 --- a/game/tests/unit/battle_settlement/variant_placement_test.gd +++ b/game/tests/unit/battle_settlement/variant_placement_test.gd @@ -236,7 +236,7 @@ func _smoke_global_variant() -> void: Kit.check("生存族下调:heal 4→3 / defense 11→7(v=30 构造例)", wmap.get("heal_minor", -1) == 3 and wmap.get("ac_up", -1) == 7) Kit.check("进攻族上浮:attack 14→15 / blast 16→18 / pierce 8→9 / magic 8→9(v=30 构造例,等比上浮守恒)", wmap.get("attack_up", -1) == 15 and wmap.get("spark_aoe", -1) == 18 and wmap.get("pierce_strike", -1) == 9 and wmap.get("magic_bolt", -1) == 9) eco.roller.board_skew = 30 - var combo_rows: Array = eco.roller._biased_cell_rows(2) # 件级加算 2=偏运硬币行 value(reward_roll bias_rewards 命中面同值) + var combo_rows: Array = eco.roller._biased_cell_rows(2) # 件级加算 2=订阅行 value(reward_roll bias_rewards 命中面同值) var cw := _rows_weight_map(combo_rows) Kit.check("合成序:图级偏斜先(乘法面)件级 luck_bias 后(加法 +2×四稀有类)——合计 77+8", eco.roller._pool_weight(combo_rows) == 77 + 4 * 2) Kit.check("合成序构造例:skew(heal 3)+2=5 / gp 2+2=4 / pierce 9+2=11 / magic 9+2=11(图级定型后玩家构筑叠加)", cw.get("heal_minor", -1) == 5 and cw.get("gp_coin", -1) == 4 and cw.get("pierce_strike", -1) == 11 and cw.get("magic_bolt", -1) == 11) diff --git a/game/tests/unit/core/meta_progression_forge_test.gd b/game/tests/unit/core/meta_progression_forge_test.gd index 7048c521..84bdd583 100644 --- a/game/tests/unit/core/meta_progression_forge_test.gd +++ b/game/tests/unit/core/meta_progression_forge_test.gd @@ -674,7 +674,7 @@ func _smoke_tree_rules() -> void: ctx_fz2.level_bucket = lvl_shared # 挂同一 level 桶引用(新形态跨战持久:无 export/seed,同字典即持久) Kit.check("锋:限次跨战持久(level 桶共享引用——替代旧副本搬运链)〔无状态化 V4〕", (fz2.dispatch(ctx_fz2, "mine_hit", {}, Callable()) as Array).is_empty()) - # 锋·惊巢反制效果行换代断言(迭代36 u13/wa12——_grant_routed 减半分支效果行=战斗日志 + # 锋·挖掘惩罚减半效果行换代断言(迭代36 u13/wa12——_grant_routed 减半分支效果行=战斗日志 # step_row 真实生产面;display_name 玩家可见,禁字「挖雷」由此行清零钉住) var orch_nc = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777003)) orch_nc.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, @@ -687,17 +687,17 @@ func _smoke_tree_rules() -> void: # heal 负向行表值 power=6(极性字典行值——负向 power 走表值不走快照)→ halve 50% ceil=3 orch_nc.reward_grant().grant_routed({"cell_type": "heal", "power": 10}, "negative", {"source_class": "mine_hit", "cell": Vector2i.ZERO, "ri": 0, "halve_pct": 50}) - # 受理判据=惊巢反制效果行落 step_row(halve 分支先于 dispatch 发行——flat 行 _apply_flat_row 分派 + # 受理判据=减半效果行落 step_row(halve 分支先于 dispatch 发行——flat 行 _apply_flat_row 分派 # 恒返 false,返回值不承载受理语义;行发行即分支受理) var nc_hit := "" for r in nc_rows: - if String((r as Dictionary).get("display_name", "")).contains("惊巢反制"): + if String((r as Dictionary).get("display_name", "")).contains("挖掘惩罚减半"): nc_hit = String((r as Dictionary).get("display_name", "")) break - Kit.check("锋:惊巢反制 mine_hit 减半分支受理(wa12 前提——halve_pct=50 效果行落 step_row)〔wa12〕", + Kit.check("锋:挖掘惩罚减半 mine_hit 减半分支受理(wa12 前提——halve_pct=50 效果行落 step_row)〔wa12〕", nc_hit != "") - Kit.check("惊巢反制效果行==「惊巢反制——危险格挖掘惩罚减半(6→3)」零「挖雷」(u13 换代)〔wa12〕", - nc_hit == "惊巢反制——危险格挖掘惩罚减半(6→3)" and not nc_hit.contains("挖雷")) + Kit.check("减半效果行==「挖掘惩罚减半(6→3)」零「挖雷」(u13 换代)〔wa12〕", + nc_hit == "挖掘惩罚减半(6→3)" and not nc_hit.contains("挖雷")) # 守侧五场景(契约 12):编排 rig——begin_battle 携 sense_guard,授予段 _grant_seg._judge_seal 直注 var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777001)) Kit.check("守侧 rig:战斗编排启动(tree_rules=sense_guard 携带)", diff --git a/game/tests/unit/data_config/effect_kind_retired_test.gd b/game/tests/unit/data_config/effect_kind_retired_test.gd index fe9819cb..a3b46a29 100644 --- a/game/tests/unit/data_config/effect_kind_retired_test.gd +++ b/game/tests/unit/data_config/effect_kind_retired_test.gd @@ -11,12 +11,33 @@ const Kit := preload("res://tests/unit/test_kit.gd") const SRC_DIR := "res://data/tables" const BAD_OVERFLOW_DIR := "user://effect_kind_fixture_overflow" const BAD_SEGMENTS_DIR := "user://effect_kind_fixture_segments" +const BAD_SPIKE_DIR := "user://effect_kind_fixture_spike" +const BAD_TRADEKEY_DIR := "user://effect_kind_fixture_tradekey" # 注入探针:死通道 do.kind(甲案摘名两族——各一探针行) const PROBE_OVERFLOW := ' {\n "feat_id": "kind_overflow_probe",\n "display_name": "通道探针·溢出",\n "rarity": "common",\n "triggers": [ { "on": "dig_reveal", "when": [], "do": { "kind": "overflow", "value": 50 } } ],\n "desc": "夹具探针——甲案退役词 overflow"\n},\n' const PROBE_SEGMENTS := ' {\n "feat_id": "kind_segments_probe",\n "display_name": "通道探针·连珠",\n "rarity": "common",\n "triggers": [ { "on": "dig_reveal", "when": [], "do": { "kind": "attack_segments", "cap": 2, "divisor": 3, "value": 0 } } ],\n "desc": "夹具探针——甲案退役词 attack_segments"\n},\n' +# 注入探针:改写族退役词(改写族清退批 T2)——spike kind 出域与 trade 键出域(各一探针行) +const PROBE_SPIKE_MODE := ' { + "feat_id": "kind_spike_probe", + "display_name": "词表探针·甲", + "rarity": "common", + "triggers": [ { "on": "dig_reveal", "when": [], "do": { "kind": "spike", "value": 2 } } ], + "desc": "夹具探针——改写族退役 kind spike" +}, +' +const PROBE_TRADE_KEY := ' { + "feat_id": "kind_tradekey_probe", + "display_name": "键闭探针·乙", + "rarity": "common", + "triggers": [ { "on": "dig_reveal", "when": [], "do": { "kind": "grant_ap", "trade_hit": 5, "value": 50 } } ], + "desc": "夹具探针——改写族退役 do 键 trade_hit" +}, +' + + func _inject(dir: String, probe: String) -> bool: var path := dir + "/feats.tres" var f := FileAccess.open(path, FileAccess.READ) @@ -65,5 +86,11 @@ func _init() -> void: if not _assert_rejected(BAD_SEGMENTS_DIR, PROBE_SEGMENTS, "effect_kind_retired_test/②attack_segments 出域"): quit(2) return - print("[effect_kind_retired_test] 双探针均正确拒绝(甲案词表退役——死通道误写加载致命)〔架构整改 V3〕") + if not _assert_rejected(BAD_SPIKE_DIR, PROBE_SPIKE_MODE, "effect_kind_retired_test/③词表闭面:kind=spike 出域拒绝"): + quit(2) + return + if not _assert_rejected(BAD_TRADEKEY_DIR, PROBE_TRADE_KEY, "effect_kind_retired_test/④do 键闭面:trade_hit 出域拒绝"): + quit(2) + return + print("[effect_kind_retired_test] 四探针均正确拒绝(甲案词表退役+改写族清退——退役词/键误写加载致命)〔架构整改 V3〕〔改写族清退 T2〕") quit(1) diff --git a/game/tests/unit/data_config/forge_table_test.gd b/game/tests/unit/data_config/forge_table_test.gd index 7bca3562..e26e7c61 100644 --- a/game/tests/unit/data_config/forge_table_test.gd +++ b/game/tests/unit/data_config/forge_table_test.gd @@ -212,9 +212,10 @@ func _test_negative_cases() -> void: # ---- V11:效果类型闭集零新增(满级特效本期不开新词)---- # 实数订正(L2 在册):卡片 F11「24 种」系设计文档事实快照, # 现行 31;本套件零扩签,钉现行数(将来任何加词即转红提示复核)。 +# 32→31:改写族清退批摘 spike(CI run 87 追绿——词表合法收窄的断言随动)。 func _test_effect_kinds_closed() -> void: - Kit.check("效果引擎效果类型闭集 32 种(甲案词表退役——overflow/attack_segments 摘名)〔锻造工坊批 V11〕", - EffectEngineScript.EFFECT_KINDS.size() == 32) + Kit.check("效果引擎效果类型闭集 31 种(甲案词表退役 overflow/attack_segments+改写族清退摘 spike)〔锻造工坊批 V11〕", + EffectEngineScript.EFFECT_KINDS.size() == 31) Kit.check("效果引擎事件词表在册 21 种(charge_full 位摘名——蓄力层删键批)〔锻造工坊批 V11〕", EffectEngineScript.EVENTS.size() == 21) diff --git a/game/tests/unit/presentation/level_map_info_fog_test.gd b/game/tests/unit/presentation/level_map_info_fog_test.gd index f3b2d0fe..4c4163fa 100644 --- a/game/tests/unit/presentation/level_map_info_fog_test.gd +++ b/game/tests/unit/presentation/level_map_info_fog_test.gd @@ -221,7 +221,10 @@ func _test_layout_cache(db) -> void: await process_frame # 换代图真实绘制路径重算(缓存随内容变失效重写) var pc1: Dictionary = screen.layout_probe(chain) var pc2: Dictionary = screen.layout_probe(chain) + # 第三判据原=新旧图 id0 位置差——改前靠容器延迟收敛的 origin 中间态偶然成立(换图首探针 + # 画布未及重排,origin≠0);视口化批画布同帧收敛(canvas.size 恒=内容包络)后两图 origin + # 恒等、id0 同层同槽位置恒等,前提失义。等强换键=重算字典键数恰为新图节点数(旧缓存 + # 63 键,换代不重算必不成立),同一性判据(核心失效重算面)不动。 Kit.check("布局缓存换代:新图查询返回新实例非旧缓存(失效重算生效)〔levelmap批 V2〕", - not is_same(p1, pc1) and is_same(pc1, pc2) - and (pc1[chain[0]] as Vector2) != (p1[nodes[0]] as Vector2)) + not is_same(p1, pc1) and is_same(pc1, pc2) and pc1.size() == chain.size()) screen.queue_free() diff --git a/reports/普查-效果通道与事件词零现役-2026-11-06.md b/reports/普查-效果通道与事件词零现役-2026-11-06.md new file mode 100644 index 00000000..72b524a2 --- /dev/null +++ b/reports/普查-效果通道与事件词零现役-2026-11-06.md @@ -0,0 +1,45 @@ +# 普查:效果通道与事件词零现役(2026-11-06) + +> **接收方**:待办清单「效果通道与事件词的零现役普查」条——本报告是该条的产出合同。 +> **结论**:需要你处理的事只有 **2 件**(第一节);扫描面内其余全部属预留埋点,**全部保留、零处理项**(第二节只留计数供存档)。词表零改动、内容层零孤儿、零代码改动。 + +## 一、需要你裁断的两件事 + +### 1. 注释叙述订正 5 处——做不做 + +代码注释把三件**未设计入表**的专长(掘进蓄能/深挖钻头/爆破工兵包)写成了在役叙述,后来读代码或写卡的人会被误导(迭代 53 已发生过一次:spike 承接句按注释误判「零新代码」)。订正方式=把注释改成「预留、未设计入表」,不改任何逻辑。 + +| 位置 | 现行叙述 | 事实 | +|---|---|---| +| effect_engine.gd OPS 头注 | mod_zero「掘进蓄能」、gte_ref/lt_ref「深挖钻头配额」 | 两专长零在表 | +| effect_engine.gd EFFECT_KINDS 头注 | spike 独立 kind 在词表 | 实走 modify_ticket 的 mode 通道 | +| reward_grant.gd:130 | 「counter_spend 订阅=掘进蓄能积点账本」 | 掘进蓄能零在表 | +| action_economy.gd:82 | 「深挖钻头付费开掘」 | 深挖钻头零在表 | +| treasure_item_table.gd:20 | 「cell_removal = 爆破工兵包中立移除格」 | 爆破工兵包零在表 | + +### 2. 秘宝表旧词「迁移完成」收尾——做不做、何时做 + +秘宝表 schema 词表里留着 9 个旧目录时代的词面(git 查证:迭代 22 秘宝批把旧目录 12 行全量换代成现役 12 件后,旧词面按 schema 自己注的「旧词保留共存至迁移完成」留下)。按关联效果列名: + +| 旧词承载的效果 | 类型 | +|---|---| +| 雷区揭示/奖励预览/行动点授予(3 个旧词形) | 新旧词共存——现役等价能力已由新词形承载 | +| 踩雷伤害减半/受击反弹伤害/开局前几轮行动点加成/爆破工兵包中立移除格/奖励摇号权重偏置/换图刷新加算区(6 个) | 旧内容已裁、通道位预留 | + +裁断点:这批「迁移完成」收尾要不要做、什么时候做。**词面现在不动**;收尾属于单独一批改动,做的时候再设计。 + +## 二、无需处理项汇总(零裁决点,计数存档) + +扫描面内以下词面全部判定**保留**(预留埋点——引擎架构=词表先行、内容后接,加一件已有效果类型的专长/秘宝只写数据行零代码): + +- **事件词预留 4**:armor_roll(事件岗)/reward_roll/treasure_used/seal_settle——发射点全在役运行,等订阅内容;armor_roll 另有掷点流名岗在役,同词两用。 +- **效果类型词预留 10**:spawn_spell(锻造工坊法杖设计在册待接线);bias_rewards/counter_spend(架构在役机制承载位——偏运合计查询口/受理闸模式);add_ticket_rider/yield_mult/apply_status/bias_wallet/refresh_pick/preview/spike 词面(预留位)。 +- **事实词预留 6**:is_first_dig_of_battle/terrain_kind(载荷特判支撑在)/target_armor_class/seals_count/nest_count/battle_index。 +- **算子词预留 7**:neq/gt/ends_with/has/mod_zero/gte_ref/lt_ref(实现函数全在)。 +- **channel 反弹行族**:open_ticket 反弹域零数据行(预留)。 + +其余全部事件词/效果词/事实词/算子词的数据面订阅行在役(feats/treasure 数据行+天赋树规则合成行),零孤儿。 + +## 三、普查方法(存档) + +四张词表封闭在 effect_engine.gd 常量:事件 21/效果类型 31/事实 18/算子 13;邻接面=秘宝表 schema 旧词层 10 词+channel 域。数据面使用按行结构键(`"on"`/`"kind"`/`"fact"`/`"op"`/`"channel"`)统计自 `game/data/tables/*.tres` 全域+天赋树规则合成行+秘宝表主动件域;注释裸词面不计。skills.tres(敌技)的 when/fact 走独立系统,不入本普查面。 diff --git a/reports/读数-三关色相互辨抽测-2026-11-05.md b/reports/读数-三关色相互辨抽测-2026-11-05.md new file mode 100644 index 00000000..db50e709 --- /dev/null +++ b/reports/读数-三关色相互辨抽测-2026-11-05.md @@ -0,0 +1,186 @@ +# 读数:三关色相 × 五组微光互辨抽测(2026-11-05) + +> 接收方:待办清单「三关色相 × 五组微光互辨抽测——三关图类型色板终值冻结的前置抽测」条。 +> 执行依据:reports/提案-三关色相互辨抽测方案-2026-10-29.md(S1/S2 步)。 +> 样图:artwork/mockups/glow-discrimination-sheet-2026-11-05.png(不入 git)。 + +## 机检汇总 + +| 项 | 判定 | 说明 | +|---|---|---| +| D-dungeon | PASS | 铺底代表值门(L/饱和/禁入带;近色只报读数):RGB=(181, 155, 119) L=0.346 饱和=0.343 色相=34.8° 近色对=4(在役素材色,非新增) | +| D-underdark | PASS | 铺底代表值门(L/饱和/禁入带;近色只报读数):RGB=(186, 193, 210) L=0.532 饱和=0.114 色相=222.5° 近色对=4(在役素材色,非新增) | +| D-deep_underdark | PASS | 铺底代表值门(L/饱和/禁入带;近色只报读数):RGB=(150, 158, 186) L=0.345 饱和=0.194 色相=226.7° 近色对=9(在役素材色,非新增) | +| A-stair-dungeon-素砖 | PASS | 明度阶梯单调递减:165.242→145.992→130.196→114.266→92.362 | +| A-stair-dungeon-裂纹 | PASS | 明度阶梯单调递减:165.242→145.992→130.196→114.266→92.362 | +| A-stair-dungeon-碎石斑 | PASS | 明度阶梯单调递减:164.957→144.992→129.269→113.266→91.434 | +| A-stair-underdark-素砖 | PASS | 明度阶梯单调递减:161.080→141.829→125.179→109.389→87.485 | +| A-stair-underdark-裂纹 | PASS | 明度阶梯单调递减:164.152→144.902→129.106→112.461→90.557 | +| A-stair-underdark-碎石斑 | PASS | 明度阶梯单调递减:158.152→138.829→123.106→106.461→84.557 | +| A-stair-deep_underdark-素砖 | PASS | 明度阶梯单调递减:145.437→126.186→109.748→93.746→71.914 | +| A-stair-deep_underdark-裂纹 | PASS | 明度阶梯单调递减:147.509→128.259→112.536→95.818→73.986 | +| A-stair-deep_underdark-碎石斑 | PASS | 明度阶梯单调递减:142.509→123.186→106.748→90.746→68.914 | +| A-CR | PASS | 组间合成对比最弱对 ≥1.20(90 对):最弱 underdark/HEAL/PIERCE CR=1.224 | +| B | PASS | 格底-微光可辨底线(45 行三通道差不全<8):违例 0 行:无 | +| C | PASS | 绘制参数校准(45 行模型差≤±2/255):违例 0 行:无 | +| GRAY | PASS | 灰度副本段零彩色像素:命中 0 | +| EDGE | PASS | 四缘 10px 带内零内容像素:命中 0 | + +## S1 机检 D:铺底代表值(H2 槽)五门 + +| 关 | RGB | L(线性域) | 饱和 | 色相 | 近色对 | 判定 | +|---|---|---|---|---|---|---| +| dungeon | (181, 155, 119) | 0.346 | 0.343 | 34.8° | 4 | 过 | +| underdark | (186, 193, 210) | 0.532 | 0.114 | 222.5° | 4 | 过 | +| deep_underdark | (150, 158, 186) | 0.345 | 0.194 | 226.7° | 9 | 过 | + +## 表一 逐对表(45 行) + +| 关 | 底型 | 微光组 | 带内合成均值 | 格心素底均值 | 模型带内均值 | 模型差 | 三通道差 | 色相差° | 判定 | +|---|---|---|---|---|---|---|---|---|---| +| dungeon | 素砖 | BUFF | (193, 166, 76) | (145, 116, 84) | (193, 166, 76) | 0 | [48, 50, 8] | 17 | 过 | +| dungeon | 素砖 | HEAL | (126, 159, 76) | (145, 116, 84) | (126, 159, 75) | 1 | [19, 43, 8] | 69 | 过 | +| dungeon | 素砖 | PIERCE | (100, 140, 122) | (145, 116, 84) | (100, 140, 122) | 0 | [45, 24, 38] | 145 | 过 | +| dungeon | 素砖 | MAGIC | (135, 105, 145) | (145, 116, 84) | (135, 105, 145) | 0 | [10, 11, 61] | 139 | 过 | +| dungeon | 素砖 | AOE | (155, 76, 70) | (145, 116, 84) | (155, 76, 70) | 0 | [10, 40, 14] | 46 | 过 | +| dungeon | 裂纹 | BUFF | (193, 166, 76) | (143, 115, 83) | (193, 166, 76) | 0 | [50, 51, 7] | 17 | 过 | +| dungeon | 裂纹 | HEAL | (126, 159, 76) | (143, 115, 83) | (126, 159, 76) | 0 | [17, 44, 7] | 69 | 过 | +| dungeon | 裂纹 | PIERCE | (100, 140, 122) | (143, 115, 83) | (100, 140, 122) | 0 | [43, 25, 39] | 145 | 过 | +| dungeon | 裂纹 | MAGIC | (135, 105, 145) | (143, 115, 83) | (135, 105, 145) | 0 | [8, 10, 62] | 139 | 过 | +| dungeon | 裂纹 | AOE | (155, 76, 70) | (143, 115, 83) | (155, 76, 70) | 0 | [12, 39, 13] | 46 | 过 | +| dungeon | 碎石斑 | BUFF | (192, 166, 75) | (147, 118, 85) | (192, 166, 75) | 0 | [45, 48, 10] | 17 | 过 | +| dungeon | 碎石斑 | HEAL | (125, 158, 75) | (147, 118, 85) | (125, 159, 75) | 1 | [22, 40, 10] | 69 | 过 | +| dungeon | 碎石斑 | PIERCE | (99, 139, 122) | (147, 118, 85) | (99, 139, 122) | 0 | [48, 21, 37] | 145 | 过 | +| dungeon | 碎石斑 | MAGIC | (134, 104, 144) | (147, 118, 85) | (134, 104, 144) | 0 | [13, 14, 59] | 139 | 过 | +| dungeon | 碎石斑 | AOE | (154, 75, 70) | (147, 118, 85) | (154, 75, 70) | 0 | [7, 43, 15] | 46 | 过 | +| underdark | 素砖 | BUFF | (172, 164, 100) | (104, 110, 128) | (172, 164, 100) | 0 | [68, 54, 28] | 170 | 过 | +| underdark | 素砖 | HEAL | (105, 157, 100) | (104, 110, 128) | (105, 157, 100) | 0 | [1, 47, 28] | 119 | 过 | +| underdark | 素砖 | PIERCE | (78, 137, 147) | (104, 110, 128) | (78, 137, 147) | 0 | [26, 27, 19] | 42 | 过 | +| underdark | 素砖 | MAGIC | (114, 102, 169) | (104, 110, 128) | (114, 102, 169) | 0 | [10, 8, 41] | 33 | 过 | +| underdark | 素砖 | AOE | (134, 73, 94) | (104, 110, 128) | (134, 73, 95) | 1 | [30, 37, 34] | 126 | 过 | +| underdark | 裂纹 | BUFF | (175, 167, 104) | (113, 120, 137) | (175, 167, 104) | 0 | [62, 47, 33] | 170 | 过 | +| underdark | 裂纹 | HEAL | (108, 160, 104) | (113, 120, 137) | (108, 160, 104) | 0 | [5, 40, 33] | 119 | 过 | +| underdark | 裂纹 | PIERCE | (82, 141, 150) | (113, 120, 137) | (82, 141, 150) | 0 | [31, 21, 13] | 42 | 过 | +| underdark | 裂纹 | MAGIC | (117, 105, 173) | (113, 120, 137) | (117, 105, 173) | 0 | [4, 15, 36] | 33 | 过 | +| underdark | 裂纹 | AOE | (137, 76, 98) | (113, 120, 137) | (137, 76, 98) | 0 | [24, 44, 39] | 126 | 过 | +| underdark | 碎石斑 | BUFF | (169, 161, 98) | (124, 131, 149) | (169, 161, 98) | 0 | [45, 30, 51] | 170 | 过 | +| underdark | 碎石斑 | HEAL | (102, 154, 97) | (124, 131, 149) | (102, 154, 97) | 0 | [22, 23, 52] | 119 | 过 | +| underdark | 碎石斑 | PIERCE | (76, 135, 144) | (124, 131, 149) | (76, 135, 144) | 0 | [48, 4, 5] | 42 | 过 | +| underdark | 碎石斑 | MAGIC | (111, 99, 167) | (124, 131, 149) | (111, 99, 167) | 0 | [13, 32, 18] | 33 | 过 | +| underdark | 碎石斑 | AOE | (131, 70, 92) | (124, 131, 149) | (131, 70, 92) | 0 | [7, 61, 57] | 126 | 过 | +| deep_underdark | 素砖 | BUFF | (157, 148, 86) | (76, 80, 102) | (157, 148, 86) | 0 | [81, 68, 16] | 175 | 过 | +| deep_underdark | 素砖 | HEAL | (90, 141, 86) | (76, 80, 102) | (90, 141, 86) | 0 | [14, 61, 16] | 123 | 过 | +| deep_underdark | 素砖 | PIERCE | (64, 121, 133) | (76, 80, 102) | (64, 121, 133) | 0 | [12, 41, 31] | 47 | 过 | +| deep_underdark | 素砖 | MAGIC | (99, 86, 155) | (76, 80, 102) | (99, 86, 155) | 0 | [23, 6, 53] | 29 | 过 | +| deep_underdark | 素砖 | AOE | (119, 57, 81) | (76, 80, 102) | (119, 57, 81) | 0 | [43, 23, 21] | 122 | 过 | +| deep_underdark | 裂纹 | BUFF | (159, 150, 89) | (84, 89, 111) | (159, 150, 89) | 0 | [75, 61, 22] | 175 | 过 | +| deep_underdark | 裂纹 | HEAL | (92, 143, 89) | (84, 89, 111) | (92, 143, 89) | 0 | [8, 54, 22] | 123 | 过 | +| deep_underdark | 裂纹 | PIERCE | (66, 124, 136) | (84, 89, 111) | (66, 124, 136) | 0 | [18, 35, 25] | 47 | 过 | +| deep_underdark | 裂纹 | MAGIC | (101, 88, 158) | (84, 89, 111) | (101, 88, 158) | 0 | [17, 1, 47] | 29 | 过 | +| deep_underdark | 裂纹 | AOE | (121, 59, 84) | (84, 89, 111) | (121, 59, 84) | 0 | [37, 30, 27] | 122 | 过 | +| deep_underdark | 碎石斑 | BUFF | (154, 145, 84) | (91, 97, 121) | (154, 145, 83) | 1 | [63, 48, 37] | 175 | 过 | +| deep_underdark | 碎石斑 | HEAL | (87, 138, 83) | (91, 97, 121) | (87, 138, 83) | 0 | [4, 41, 38] | 123 | 过 | +| deep_underdark | 碎石斑 | PIERCE | (61, 118, 130) | (91, 97, 121) | (61, 118, 130) | 0 | [30, 21, 9] | 47 | 过 | +| deep_underdark | 碎石斑 | MAGIC | (96, 83, 152) | (91, 97, 121) | (96, 83, 152) | 0 | [5, 14, 31] | 29 | 过 | +| deep_underdark | 碎石斑 | AOE | (116, 54, 78) | (91, 97, 121) | (116, 54, 78) | 0 | [25, 43, 43] | 122 | 过 | + +## 表二 组间表(90 行) + +| 关 | 底型 | 组对 | 合成对比 | 灰度亮度a | 灰度亮度b | 判定 | +|---|---|---|---|---|---|---| +| dungeon | 素砖 | BUFF/HEAL | 1.270 | 165.242 | 145.992 | 过 | +| dungeon | 素砖 | BUFF/PIERCE | 1.583 | 165.242 | 130.196 | 过 | +| dungeon | 素砖 | BUFF/MAGIC | 1.979 | 165.242 | 114.266 | 过 | +| dungeon | 素砖 | BUFF/AOE | 2.510 | 165.242 | 92.362 | 过 | +| dungeon | 素砖 | HEAL/PIERCE | 1.247 | 145.992 | 130.196 | 过 | +| dungeon | 素砖 | HEAL/MAGIC | 1.559 | 145.992 | 114.266 | 过 | +| dungeon | 素砖 | HEAL/AOE | 1.977 | 145.992 | 92.362 | 过 | +| dungeon | 素砖 | PIERCE/MAGIC | 1.250 | 130.196 | 114.266 | 过 | +| dungeon | 素砖 | PIERCE/AOE | 1.585 | 130.196 | 92.362 | 过 | +| dungeon | 素砖 | MAGIC/AOE | 1.268 | 114.266 | 92.362 | 过 | +| dungeon | 裂纹 | BUFF/HEAL | 1.270 | 165.242 | 145.992 | 过 | +| dungeon | 裂纹 | BUFF/PIERCE | 1.583 | 165.242 | 130.196 | 过 | +| dungeon | 裂纹 | BUFF/MAGIC | 1.979 | 165.242 | 114.266 | 过 | +| dungeon | 裂纹 | BUFF/AOE | 2.510 | 165.242 | 92.362 | 过 | +| dungeon | 裂纹 | HEAL/PIERCE | 1.247 | 145.992 | 130.196 | 过 | +| dungeon | 裂纹 | HEAL/MAGIC | 1.559 | 145.992 | 114.266 | 过 | +| dungeon | 裂纹 | HEAL/AOE | 1.977 | 145.992 | 92.362 | 过 | +| dungeon | 裂纹 | PIERCE/MAGIC | 1.250 | 130.196 | 114.266 | 过 | +| dungeon | 裂纹 | PIERCE/AOE | 1.585 | 130.196 | 92.362 | 过 | +| dungeon | 裂纹 | MAGIC/AOE | 1.268 | 114.266 | 92.362 | 过 | +| dungeon | 碎石斑 | BUFF/HEAL | 1.282 | 164.957 | 144.992 | 过 | +| dungeon | 碎石斑 | BUFF/PIERCE | 1.598 | 164.957 | 129.269 | 过 | +| dungeon | 碎石斑 | BUFF/MAGIC | 2.001 | 164.957 | 113.266 | 过 | +| dungeon | 碎石斑 | BUFF/AOE | 2.536 | 164.957 | 91.434 | 过 | +| dungeon | 碎石斑 | HEAL/PIERCE | 1.247 | 144.992 | 129.269 | 过 | +| dungeon | 碎石斑 | HEAL/MAGIC | 1.561 | 144.992 | 113.266 | 过 | +| dungeon | 碎石斑 | HEAL/AOE | 1.979 | 144.992 | 91.434 | 过 | +| dungeon | 碎石斑 | PIERCE/MAGIC | 1.252 | 129.269 | 113.266 | 过 | +| dungeon | 碎石斑 | PIERCE/AOE | 1.587 | 129.269 | 91.434 | 过 | +| dungeon | 碎石斑 | MAGIC/AOE | 1.267 | 113.266 | 91.434 | 过 | +| underdark | 素砖 | BUFF/HEAL | 1.248 | 161.080 | 141.829 | 过 | +| underdark | 素砖 | BUFF/PIERCE | 1.549 | 161.080 | 125.179 | 过 | +| underdark | 素砖 | BUFF/MAGIC | 1.969 | 161.080 | 109.389 | 过 | +| underdark | 素砖 | BUFF/AOE | 2.637 | 161.080 | 87.485 | 过 | +| underdark | 素砖 | HEAL/PIERCE | 1.241 | 141.829 | 125.179 | 过 | +| underdark | 素砖 | HEAL/MAGIC | 1.577 | 141.829 | 109.389 | 过 | +| underdark | 素砖 | HEAL/AOE | 2.112 | 141.829 | 87.485 | 过 | +| underdark | 素砖 | PIERCE/MAGIC | 1.271 | 125.179 | 109.389 | 过 | +| underdark | 素砖 | PIERCE/AOE | 1.702 | 125.179 | 87.485 | 过 | +| underdark | 素砖 | MAGIC/AOE | 1.339 | 109.389 | 87.485 | 过 | +| underdark | 裂纹 | BUFF/HEAL | 1.245 | 164.152 | 144.902 | 过 | +| underdark | 裂纹 | BUFF/PIERCE | 1.524 | 164.152 | 129.106 | 过 | +| underdark | 裂纹 | BUFF/MAGIC | 1.952 | 164.152 | 112.461 | 过 | +| underdark | 裂纹 | BUFF/AOE | 2.612 | 164.152 | 90.557 | 过 | +| underdark | 裂纹 | HEAL/PIERCE | 1.224 | 144.902 | 129.106 | 过 | +| underdark | 裂纹 | HEAL/MAGIC | 1.567 | 144.902 | 112.461 | 过 | +| underdark | 裂纹 | HEAL/AOE | 2.097 | 144.902 | 90.557 | 过 | +| underdark | 裂纹 | PIERCE/MAGIC | 1.280 | 129.106 | 112.461 | 过 | +| underdark | 裂纹 | PIERCE/AOE | 1.713 | 129.106 | 90.557 | 过 | +| underdark | 裂纹 | MAGIC/AOE | 1.338 | 112.461 | 90.557 | 过 | +| underdark | 碎石斑 | BUFF/HEAL | 1.252 | 158.152 | 138.829 | 过 | +| underdark | 碎石斑 | BUFF/PIERCE | 1.538 | 158.152 | 123.106 | 过 | +| underdark | 碎石斑 | BUFF/MAGIC | 1.981 | 158.152 | 106.461 | 过 | +| underdark | 碎石斑 | BUFF/AOE | 2.658 | 158.152 | 84.557 | 过 | +| underdark | 碎石斑 | HEAL/PIERCE | 1.229 | 138.829 | 123.106 | 过 | +| underdark | 碎石斑 | HEAL/MAGIC | 1.583 | 138.829 | 106.461 | 过 | +| underdark | 碎石斑 | HEAL/AOE | 2.123 | 138.829 | 84.557 | 过 | +| underdark | 碎石斑 | PIERCE/MAGIC | 1.288 | 123.106 | 106.461 | 过 | +| underdark | 碎石斑 | PIERCE/AOE | 1.728 | 123.106 | 84.557 | 过 | +| underdark | 碎石斑 | MAGIC/AOE | 1.341 | 106.461 | 84.557 | 过 | +| deep_underdark | 素砖 | BUFF/HEAL | 1.265 | 145.437 | 126.186 | 过 | +| deep_underdark | 素砖 | BUFF/PIERCE | 1.585 | 145.437 | 109.748 | 过 | +| deep_underdark | 素砖 | BUFF/MAGIC | 2.040 | 145.437 | 93.746 | 过 | +| deep_underdark | 素砖 | BUFF/AOE | 2.734 | 145.437 | 71.914 | 过 | +| deep_underdark | 素砖 | HEAL/PIERCE | 1.253 | 126.186 | 109.748 | 过 | +| deep_underdark | 素砖 | HEAL/MAGIC | 1.613 | 126.186 | 93.746 | 过 | +| deep_underdark | 素砖 | HEAL/AOE | 2.162 | 126.186 | 71.914 | 过 | +| deep_underdark | 素砖 | PIERCE/MAGIC | 1.287 | 109.748 | 93.746 | 过 | +| deep_underdark | 素砖 | PIERCE/AOE | 1.725 | 109.748 | 71.914 | 过 | +| deep_underdark | 素砖 | MAGIC/AOE | 1.340 | 93.746 | 71.914 | 过 | +| deep_underdark | 裂纹 | BUFF/HEAL | 1.263 | 147.509 | 128.259 | 过 | +| deep_underdark | 裂纹 | BUFF/PIERCE | 1.562 | 147.509 | 112.536 | 过 | +| deep_underdark | 裂纹 | BUFF/MAGIC | 2.028 | 147.509 | 95.818 | 过 | +| deep_underdark | 裂纹 | BUFF/AOE | 2.720 | 147.509 | 73.986 | 过 | +| deep_underdark | 裂纹 | HEAL/PIERCE | 1.237 | 128.259 | 112.536 | 过 | +| deep_underdark | 裂纹 | HEAL/MAGIC | 1.606 | 128.259 | 95.818 | 过 | +| deep_underdark | 裂纹 | HEAL/AOE | 2.154 | 128.259 | 73.986 | 过 | +| deep_underdark | 裂纹 | PIERCE/MAGIC | 1.298 | 112.536 | 95.818 | 过 | +| deep_underdark | 裂纹 | PIERCE/AOE | 1.742 | 112.536 | 73.986 | 过 | +| deep_underdark | 裂纹 | MAGIC/AOE | 1.341 | 95.818 | 73.986 | 过 | +| deep_underdark | 碎石斑 | BUFF/HEAL | 1.268 | 142.509 | 123.186 | 过 | +| deep_underdark | 碎石斑 | BUFF/PIERCE | 1.593 | 142.509 | 106.748 | 过 | +| deep_underdark | 碎石斑 | BUFF/MAGIC | 2.055 | 142.509 | 90.746 | 过 | +| deep_underdark | 碎石斑 | BUFF/AOE | 2.753 | 142.509 | 68.914 | 过 | +| deep_underdark | 碎石斑 | HEAL/PIERCE | 1.256 | 123.186 | 106.748 | 过 | +| deep_underdark | 碎石斑 | HEAL/MAGIC | 1.620 | 123.186 | 90.746 | 过 | +| deep_underdark | 碎石斑 | HEAL/AOE | 2.170 | 123.186 | 68.914 | 过 | +| deep_underdark | 碎石斑 | PIERCE/MAGIC | 1.290 | 106.748 | 90.746 | 过 | +| deep_underdark | 碎石斑 | PIERCE/AOE | 1.728 | 106.748 | 68.914 | 过 | +| deep_underdark | 碎石斑 | MAGIC/AOE | 1.339 | 90.746 | 68.914 | 过 | + +--- + +机检 17 项,FAIL 0 项。 + +目检(S3)归用户:四条判据见方案 §2.2(逐对指认 45/45/灰度副本 45/45/相邻并列 36 对可辨/装饰不抢读)。 diff --git a/reports/需求卡片-卷轴视口批-2026-11-05.md b/reports/需求卡片-卷轴视口批-2026-11-05.md deleted file mode 100644 index 47c14619..00000000 --- a/reports/需求卡片-卷轴视口批-2026-11-05.md +++ /dev/null @@ -1,140 +0,0 @@ -# 需求卡片:卷轴视口批(关卡画面选路屏视口化) - -- 来源:[待办清单](../待办清单.md)「超预算图策略升级路径——等比缩放与滚动视口两案待裁」条(状态=文档设计已闭合+待写卡,2026-11-05 用户裁滚动视口案落地)。 -- **唯一事实源**:[level-map.md v2.0](../design/gdd/ux/level-map.md) §布局决策 5(行为四条表 :101–104+内嵌视口效果图 :108–153+像素级视觉判据 :154)+[关卡节点图生成.md v1.5](../design/gdd/systems/冒险循环/关卡节点图生成.md) §参数契约(图规模无呈现侧上限)。设计讨论四口子与效果图人审均 2026-11-05 用户裁决在案。 -- **验收档:中**(呈现交互面——玩家可见但不损持久状态;无存档写读、无结算公式、无 RNG 触碰;批卡 U0 时确认或改判)。 - -## 1. 目标与完成定义 - -关卡画面(选路屏)落卷轴视口范式:画布随图生长可超屏,屏为窗口——①视口锚点=当前节点居中;②停点切换与兑付回图平滑滚动跟随(减动效瞬切);③拖拽+滚轮手动平移(clamp 画布包络、拖拽不触发选路);④屏缘渐隐带+平移提示 chip 指引。 - -完成定义:验证闸对照表 R1–R9 全部有绿读数;场景探针家族扩展件当轮真实跑批通过;代码域闸全绿;用户实机目检「深度 13 图拖拽可达首末列、推进跟随方向感」登记不阻塞。 - -## 2. 现状事实(开工自行复核) - -- [level_map_screen.gd:26-32](../game/presentation/views/level_map_screen.gd) 冻结公式常量:`LAYER_BASE_X=70`/`LAYER_STEP_X=122`/`SLOT_BASE_Y=52`/`SLOT_STEP_Y=58`/`NODE_ICON_PX=20.0`/`NODE_HALF(36.5,25)`(52×36 的 1.4× 绘制)/`HIT_PAD=6.0`。 -- [level_map_screen.gd:66-69](../game/presentation/views/level_map_screen.gd) `_content_size(620,240)`、`_layout_cache`——`open_choices` 按图拟合画布尺寸。 -- [level_map_screen.gd:77-82](../game/presentation/views/level_map_screen.gd) 画布由 `CenterContainer`(`PRESET_FULL_RECT`)承载:整幅居中、无裁切无平移——超屏图(L3 层深 13,画布宽 1700.5px)两侧各 ~210px 出屏。 -- [level_gen_params.tres:41](../game/data/tables/level_gen_params.tres) L3 `layers=13`——深度 13 是现行定值,非极端情形。 -- [level_gen_params.gd:3-4、:69](../game/data/schemas/level_gen_params.gd) 头注「域校验沿关卡节点图生成.md v1.1 调参表已锁条款」「域并单点(关卡节点图生成.md v1.4 调参表)」——owner 文档已 v1.5 换代参数契约节,指针失效待反转(本卡 T4 随动)。 -- [ui_kit.gd:231-232](../game/presentation/views/ui_kit.gd) `UiKit.motion(dur)` 呈现层 tween 时长单点包装(减动效面)。 -- [ui_struct_probe.gd:204](../game/probe/ui_struct_probe.gd) `_audit("level_map", ...)` 既有结构审计锚;[golden/README.md:8](../game/tests/golden/README.md) level_map 全屏 golden=历史留根、现行闸件未对拍(本批不触渲染闸对拍)。 -- 全文件 662 行——超 code-conventions §1.1 的 ~400 行审视线,视口承载拆件有据。 - -## 3. 设计契约(违反=L3 停机呈报) - -- 🔴 **冻结值零改动**:节点图标 20px 烘焙 1:1、通道折线 2/3/4px、画布公式(宽=70+层深×122+44.5、高=52+槽位×58+25+24、下限 620×256)、节点方块 52×36×1.4 绘制、编号 10px——视口批只加行为,不动任何像素冻结件(level-map.md:69、:154)。 -- 🔴 **视口纯呈现态**:平移位置零核心写、不进存档(重进本屏按锚点规则重新定位);Core 层零触碰、RNG 零触碰(level-map.md:103 行、:107 视口为纯呈现态句)。 -- 🔴 **选路唯一通道**:图上点击提交语义零改动;拖拽超阈值不得触发选路提交(level-map.md:103)。 -- 🔴 **零自动推进**:视口跟随只随停点切换/兑付回图发生,不构成任何自动前进路径(level-map.md:141 零自动推进纪律不变)。 -- 🔴 **拆分边界纪律**(code-conventions §1.1,2026-10-28 用户裁):每个拆出单元有可陈述职责语句与单一变化原因;拆出单元有集体身份;「过阈值」只作发现信号;拆不出内聚单元=停卡走豁免通道,禁硬拆凑绿。 -- 🔴 **禁为机检文本保留无效成员**(code-conventions §1.1⑤):机检键与卡片契约互斥=停卡呈报,禁自造第三条路。 - -## 4. 范围边界 - -做:视口承载件、锚点跟随、平滑滚动、减动效瞬切、拖拽/滚轮平移、clamp、渐隐带与提示 chip、探针断言、schema 头注指针反转。 -不做:缩放与缩略图(等比缩放案已否决)、其余视口暗示形态(边缘回弹等——布局决策 5 注后置)、顶栏/底栏/放弃按钮/覆盖层改动、生成器与参数表改动、键盘/手柄输入(排 Beta)。 - -## 5. 关联文档同步面 - -| 文档 | 关联点 | 同步义务 | -|---|---|---| -| [level-map.md](../design/gdd/ux/level-map.md) v2.0 | 布局决策 5 行为实现与 AC :260–271——实现若发现 spec 缺口 | 设计权威——实现批禁写,缺口逐条列反哺点清单由文档批落 | -| [game/tests/README.md](../game/tests/README.md) 能力清单 | 本卡新增能力行三行(写卡段已同批落表,见 §8 能力行列) | 机械面——实现批禁写;批末收尾核对表上名与落地一致(改名则回表) | -| [level_gen_params.gd](../game/data/schemas/level_gen_params.gd) | 头注 :3-4/:69 指针失效(owner 已 v1.5 参数契约) | 本卡 T4 随批反转指针(非行为改动——`game/` 内实现批可写面) | -| [关卡节点图生成.md](../design/gdd/systems/冒险循环/关卡节点图生成.md) v1.5 | 无——本批实现不改变其任何陈述 | 无同步义务 | - -## 6. 文件结构图(行为改动→对照表行互指) - -| 文件 | 职责 | 改动性质 | -|---|---|---| -| `game/presentation/views/level_map_viewport.gd` | 新增——卷轴视口承载件:画布宿主与裁切、平移状态与 clamp、锚点跟随、平滑滚动、指引件(拖拽/滚轮判定留画布 `_gui_input`——事件流同源) | 行为改动(R2–R8)·新增 | -| `game/presentation/views/level_map_screen.gd` | 修改——CenterContainer 承载换视口件;停点切换/回图接线 `reanchor()`;`_content_size` 拟合后调 `reanchor(false)` 首锚 | 行为改动(R1+R2/R3 随动)·复用既有文件 | -| `game/data/schemas/level_gen_params.gd` | 修改——头注指针反转(owner 单点改为本文件域校验+参数契约指针) | 非行为改动(R9)·复用既有文件 | -| `game/probe/level_map_viewport_probe.gd` + `.tscn` | 新增——视口行为场景探针(真输入驱动) | 行为改动(R2–R8 验收载体)·新增 | -| `game/probe/probe_ui_util.gd` | 修改——新增拖拽沿与滚轮沿真输入合成工具 `push_drag_at`/`push_wheel_at`(沿 `push_click_at` :717-733 同式;探针共用面唯一落点) | 非行为改动(探针侧工具)·复用既有文件 | - -## 7. 任务卡 T0..T4 - -### T0 读数与基线(0.5 工作日) -- [ ] 核对 `tools/gdstyle_baseline.txt` 有无 `level_map_screen` 相关 S13 键;有则记录登记值(改后只降不升),无则记「无键、按工具默认」 -- [ ] 跑场景探针现状读数:`…console.exe --headless --path . res://probe/ui_struct_probe.tscn`(level_map 审计段绿)+ `res://probe/screen_flow_probe.tscn`——读数记入本步提交信息 -- [ ] 跑冒烟 `python tools/run_smoke.py`——基线读数在案 - -### T1 视口承载件与手动平移(1–1.5 工作日) -- [ ] 新增 `level_map_viewport.gd`(接口块见 §10):`setup(canvas, anchor_provider, viewport_size)`、`pan_by(delta)`、`viewport_center_canvas_pos() -> Vector2`、`is_clamped_at_edge() -> bool`;`clip_contents=true`;成员 ≤8 -- [ ] `level_map_screen.gd` 承载改造:CenterContainer 换视口件实例;`open_choices` 拟合后调 `reanchor(false)` 首锚(入口停点锚入口组、其余锚当前节点——`anchor_provider` 由 screen 提供画布坐标) -- [ ] 拖拽/滚轮判定落**画布既有 `_gui_input`**(画布已持有鼠标事件流——悬停/选路同源,禁在视口件层做父子事件路由竞争;视口件只管裁切/offset/clamp/跟随):拖动距离阈值(provisional 8px)判定,超阈值进拖拽态逐帧调 `viewport.pan_by()`;滚轮横滚映射 `pan_by`(代拍板:滚轮下=向右/前进向) -- [ ] clamp:`pan_by` 后 offset 收敛在画布包络内(`viewport_center_canvas_pos()` 画布坐标恒在 `[half_view, canvas_size-half_view]` 夹紧域;**画布小于视口=居中分支**——夹紧域退化为居中定位);探针宿主沿 ui_struct_probe :204 同款(组合根真实屏 `main.ui.modals.run.level_screen`),禁合成投喂替代真实路径 -- [ ] 探针 `level_map_viewport_probe.gd`:断言 R3(拖拽移视口)、R4(滚轮移视口)、R5(clamp 边界)、R6(拖拽不触发选路——`route_chosen` 零发射);真实入口=probe_ui_util 合成沿走 `push_input` - -### T2 锚点跟随与平滑滚动(0.5–1 工作日) -- [ ] `reanchor(smooth)`:目标 offset=锚点画布坐标−视口半宽(居中);`smooth=true` 走单段缓动 tween 时长 `UiKit.motion(0.3)`(provisional);`smooth=false` 即达;重入先杀在飞 tween 再建新(快速连续停点切换防叠加) -- [ ] 减动效:`UiKit.motion` 通道既有减动效语义下滚动跳终态——探针断言 R7(减动效开=位置即达,无 tween 中间帧) -- [ ] level_map_screen.gd 停点切换与兑付回图路径接 `reanchor(true)`——入口停点锚入口节点组(多入口取组包围盒居中) -- [ ] 探针断言 R2(停点切换后当前节点画布坐标投影=视口中线 x=640±2) - -### T3 指引件(0.5 工作日) -- [ ] 视口件内渐隐带:屏缘左右各 70px 渐变(色取屏底同族——代拍板渐变 stops);浅图画布不超屏时渐隐带隐藏(无暗示必要) -- [ ] 平移提示 chip:240×30 圆角 6、深底石灰描边、文案「◄ 拖拽画布 / 滚轮平移 ►」14px;图区底部居中(level-map.md:104) -- [ ] 探针断言 R8(渐隐带与 chip 可见性+几何判据——像素级判据②③,level-map.md:154) - -### T4 指针反转与收口(0.5 工作日) -- [ ] `level_gen_params.gd` 头注 :3-4/:69 反转:「取值域与字段契约单点=本文件域校验;概念与设计意图=关卡节点图生成.md v1.5 参数契约节」(非行为改动——grep 判据 R9) -- [ ] 全探针最窄回归:ui_struct_probe level_map 段+screen_flow_probe 复跑绿(波内修复只做出红最小单元的最窄确认) -- [ ] 产出物落位:`reports/iter-实现报告-<日期>.md`(各卡状态+实机目检总清单+实现决定记录)+ `reports/iter-反哺点-<日期>.md`(同步面所列逐条落点)——轻闸全绿后、合并前写齐 - -估时合计:3–4.5 工作日(中值 3.5)。 - -## 8. 验证闸(改动—验收对照表) - -| 需求点 | 改动(文件:行/行为) | 验收测试(名称+落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 能力行 | -|---|---|---|---|---|---|---| -| R1 浅图整幅居中无裁切+画布入视口件承载(锚 design/gdd/ux/level-map.md:260) | level_map_screen.gd(CenterContainer→viewport 件) | `level_map_viewport_probe.gd:_probe_shallow_no_crop`〔卷轴批 V1〕 | 探针 | 场景探针实例化本屏+open_choices 真实停点 | 画布尺寸≤视口时居中分支 offset 定位、无裁切 | 新增:关卡画面视口跟随 | -| R2 视口锚点:停点切换/回图后当前节点居中 x=640±2(锚 design/gdd/ux/level-map.md:101、:269) | level_map_viewport.gd `reanchor`+screen 接线 | `level_map_viewport_probe.gd:_probe_follow_anchor`〔卷轴批 V2〕 | 探针 | 合成推进停点切换经真实屏流程 | 当前节点画布坐标经 `viewport_center_canvas_pos()` 投影=640±2 | 新增:关卡画面视口跟随 | -| R3 拖拽平移移动视口(锚 design/gdd/ux/level-map.md:103) | 画布 `_gui_input`(level_map_screen.gd)拖拽态调 `pan_by` | `level_map_viewport_probe.gd:_probe_drag_pan`〔卷轴批 V3〕 | 探针 | `ProbeUiUtil.push_drag_at` 拖拽沿走 `push_input` | `_offset` 位移量=拖拽位移(clamp 前) | 新增:关卡画面视口平移 | -| R4 滚轮横滚移动视口(锚 design/gdd/ux/level-map.md:103) | 画布 `_gui_input` wheel 分支调 `pan_by` | `level_map_viewport_probe.gd:_probe_wheel_pan`〔卷轴批 V4〕 | 探针 | `ProbeUiUtil.push_wheel_at` 合成滚轮沿走 `push_input` | `_offset` 位移=滚轮步进映射 | 新增:关卡画面视口平移 | -| R5 clamp 边界与首末列可达:不出露屏底空档、拖拽两极首列与末列(含 Boss)入屏(锚 design/gdd/ux/level-map.md:103、:260) | `pan_by` 收敛逻辑+包络两极 | `level_map_viewport_probe.gd:_probe_clamp_bounds`〔卷轴批 V5〕 | 探针 | 极端拖拽沿(超包络、两极到边) | `viewport_center_canvas_pos()` 恒在夹紧域;浅图居中分支;`is_clamped_at_edge()` 翻转;两极时深度 0 与深度 13 列中心入视口 | 新增:关卡画面视口平移 | -| R6 拖拽不触发选路提交(锚 design/gdd/ux/level-map.md:103) | 画布 `_gui_input` 阈值判定(provisional 8px) | `level_map_viewport_probe.gd:_probe_drag_no_submit`〔卷轴批 V6〕 | 探针 | 拖拽沿起止于候选节点上方 | `route_chosen` 零发射+屏幕未关 | 新增:关卡画面视口平移 | -| R7 平滑滚动与减动效瞬切(锚 design/gdd/ux/level-map.md:102、:188、:227) | `UiKit.motion(0.3)` tween/减动效跳终态 | `level_map_viewport_probe.gd:_probe_smooth_scroll_reduced_motion`〔卷轴批 V7〕 | 探针 | 停点切换+设置面减动效置位 | smooth=true 有 tween 中间帧;减动效开=位置即达 | 新增:关卡画面视口跟随 | -| R8 指引件:渐隐带 70px+chip 240×30 呈现(锚 design/gdd/ux/level-map.md:104、:154) | 视口件指引子件 | `level_map_viewport_probe.gd:_probe_guidance_chrome`〔卷轴批 V8〕 | 探针 | 深图 open_choices 真实停点 | 像素级判据②③(level-map.md:154)——可见性+几何 | 新增:关卡画面视口指引 | - -非行为改动行:schema 头注指针反转(level_gen_params.gd:3-4/:69)——判据=全仓无「域并单点=调参表」残留、头注含「参数契约节」指向(锚 design/gdd/systems/冒险循环/关卡节点图生成.md:109);由 §6 文件结构图该行覆盖,T4 落步,实现报告对照表列非行为改动行。 -锚行核对义务:开工时行号以当版文件为准(本卡锚基于 v2.0/v1.5 抄录)。 - -- 测试方式两分法:R1–R8 全走探针自动化(调试注入不适用——本卡零数据面改动,状态经真实停点与真实输入驱动);用户目检项=「深度 13 图拖拽可达首末列、跟随方向感、chip 不碍选路」登记进实现报告实机目检总清单(登记不阻塞)。 -- 回归措辞:里程碑步(T 段)出口跑一次场景探针族全套;波内修复只做出红最小单元的最窄确认;新增探针先行孤立验证再并入全套。 -- 测试削弱申报:无删旧断言申报;ui_struct_probe level_map 段既有审计判据不改——承载改造若触其结构断言面,适配性调整须在实现报告申报并保持判据语义。 -- 独立验收审计:`reports/iter-验收审计-<日期>.md`,由未参与实现的会话执行;本卡验收档=中,审计深度按 test-conventions §8.7 中档执行(对照表逐行全量、判据绑定面抽查、削弱检查逐处、直查例外逐条、最窄复跑以远端 CI 现查读数代证)。 - -## 9. 异常分级 - -- L1 自救:探针断言红→改后只跑出红探针的最小单元;闸红只跑红件。 -- L2 记录继续:画布 `_gui_input` 内拖拽态与悬停/选路命中分支次序的调整、渐变/chip 视觉微调(provisional 区间内)、S13 读数波动在只降不升约束内的消化。 -- L3 停机写报告等用户:触碰 §3 任一 🔴;S13 键上升且无法只降;拆不出内聚单元;机检预算互斥=上游缺陷停卡退回写卡段;拖拽误触选路提交且路由层面消不掉。 - -## 10. 落位与可复用性预检 - -- **level_map_viewport.gd(新增)**:不复用 `ScrollContainer`——其滚动条 UI、焦点/滚动语义与本屏「纯鼠标、无滚动条、锚点居中跟随」冲突,且平滑滚动与 clamp 语义仍需全自管;承载裁切用 Control `clip_contents`(引擎内建,项目已有 clip 先例)。唯一面:视口 offset 状态只落本件(screen 的 `_layout_cache` 仍管布局、不管视口);全仓 grep 无第二处视口/平移逻辑。分层:呈现层内部子件——构造注入(canvas/anchor_provider/viewport_size 一次交齐,code-conventions §1.6 拆分交接);读核心只读面经 screen 既有出口,零新增核心触点。 -- **probe_ui_util.gd 增补(复用既有文件)**:`push_drag_at`/`push_wheel_at` 落探针共用面唯一落点——禁另起工具文件散落;`push_click_at`(ui_input_probe :717-733 同式)点击沿同式扩展。 -- **指引件(渐隐带/chip)**:随视口件承载(同职责:视口呈现行为);不立独立文件——拆出「命不出名字的打包」即凑计数袋。 -- **level_gen_params.gd 头注反转(复用既有文件)**:注释单行改动,无新增件。 - -## 11. 实现规范预算 - -- **新增 level_map_viewport.gd 三查**:①接口块公开方法 4(setup/pan_by/viewport_center_canvas_pos/is_clamped_at_edge)≤20 ✓;②成员变量 ≤8(_canvas/_anchor_provider/_viewport_size/_offset/_tween/_drag_active/_drag_accum/_guided_visible)≤15 ✓;③无迁移函数(新件零存量函数迁入——screen 侧只是接线改造,hover/命中逻辑零迁移)。 -- **存量触面**:level_map_screen.gd——T0 记 S13 键基线;本批改动为承载替换与 reanchor 接线,预期键值持平或只降;达成手段=T4 复跑 S13 对账。 -- 指标例外:无申报。 - -## 12. 代拍板白名单 - -1. 画布 `_gui_input` 内拖拽态与悬停/选路命中的事件分支次序(红线=R6 判据不破)。 -2. 拖拽距离阈值具体值(8px 附近 provisional 区间内定值)。 -3. 平滑滚动缓动曲线类型(单段缓动家族内选型)。 -4. 渐隐带渐变 stops 与 chip 深底/描边色值(art-bible §4.1 冻结色板同族取值)。 -5. 滚轮方向映射(下=向右/前进向)。 -超清单决定记入实现报告「实现决定记录」节。 - -## 13. 待裁点 - -零待裁点,开工门=U0(四口子与效果图 2026-11-05 用户已裁决;验收档「中」随批卡确认)。 diff --git a/reports/需求卡片-改写族孤儿机制与退役名清退批-2026-11-05.md b/reports/需求卡片-改写族孤儿机制与退役名清退批-2026-11-05.md deleted file mode 100644 index 131cf7fb..00000000 --- a/reports/需求卡片-改写族孤儿机制与退役名清退批-2026-11-05.md +++ /dev/null @@ -1,267 +0,0 @@ -# 需求卡片·改写族孤儿机制与退役名清退批(2026-11-05) - -来源:待办清单条目「damage_controller 按序改写族——内容孤儿机制与退役名注释层」(2026-11-04 登记,来源=会话提及)。 -唯一事实源:`design/gdd/systems/跨局循环/锻造工坊.md` v0.13 §满级特效扩展位(本期已按本批口径订正)+本卡「现状事实」所列代码读数;机制处置与注释口径的裁决记录=2026-11-05 会话,用户裁「只要有问题的都要改」,退役内容名一律清退、零现役注册的改写分支一律摘除。 -验收档:中(一处玩家可见明细行文案换代+若干不可达分支摘除;不写存档、不改乘区公式与数值表)。 -估时:2.25–2.75 个工作日(T0 0.25+T1 0.5+T2 0.5+T3 0.5–1+T4 0.25+T5 0.25 加总)。切分预案:T3 注释面若单轮改不完,按文件切成 T3a(core 八件)/T3b(schemas+sim+tests),两张各自收尾,不并进 T1。 - ---- - -## 1 目标与完成定义 - -做完这五件事算完: - -1. 主角先手攻击的四个改写分支摘除——它们零数据表行注册,游戏里从不执行。 -2. 连带收窄两处引擎词表:`EFFECT_KINDS` 摘 `spike`、`DO_KEYS` 摘 `trade_hit`/`trade_atk`,并同批补负域探针取证。 -3. `game/` 注释面退役内容名清零(42 行/13 文件),只换名不换职责陈述。 -4. 三处挂退役件名的玩家可见文案改中性机制文案(其中一处今天可达),一处 sim 死匹配删除。 -5. 产出物两份写齐(实现报告+反哺点清单),代码域闸全绿,改前改后对拍逐位一致。 - -判据总口径:本批除第 4 项文案外全部是非行为改动——零现役注册行=分支不可达=删了也不改变任何一帧输出。这条不靠感觉,T0 用三条 grep 读数坐实。 - -## 2 设计事实源 - -- 设计线闭合凭据:`design/gdd/systems/跨局循环/锻造工坊.md` v0.13(2026-11-05)§满级特效扩展位——本批订正该节四处陈述,其中重甲反震与长枪先手连击两条候选的代价档由「零新代码」改判「需新代码」,效果类型闭集词数不再抄进文档。 -- 机制处置裁决:本卡所属待办条目「实况」段所载 2026-11-05 会话裁决。 -- 代码现状取证:本卡第 3 节全部带 `文件:行号`,无一条凭记忆。 - -## 3 现状事实 - -### 3.1 四个改写分支零注册(本批要摘的) - -| 分支 | 代码位置 | 触发条件 | 现役注册行 | 退役名注释 | -|---|---|---|---|---| -| 命中交换 | `game/core/damage_controller.gd:297-302`(命中侧)、`:351-353`(增伤侧) | `kind=modify_ticket` 且载荷带 `trade_hit`/`trade_atk` | 0 行(`game/data/tables/` 全域零命中) | :285-286、:341、:350(矿工重锤) | -| 免掷恒折扣 | `:305-309` | `kind=modify_ticket` 且 `mode=armor_rewrite` | 0 行 | :287(专注) | -| 满层必中与倍乘 | `:310-313`(必中置位)、`:368-372`(倍乘) | `kind=modify_ticket` 且 `mode=spike` | 0 行 | :288(厚积薄发,行内含 spike)、:342 与 :368 | -| 轻甲折扣档改善 | `:320-323` +私有函数 `_discount_shift_up`(`:623`) | `kind=modify_ticket` 且 `mode=discount_shift` | 0 行 | :288、:622(猎弱本能) | - -穿线键 `spike_ready` 出现在 `:282`、`:310`、`:313`、`:325`、`:369` 五处,随分支一起消失。 - -克制点数面(`mode=clamp_bonus`)有现役一行注册:`game/data/tables/feats.tres:853-857` 淬毒刃纹(`kind=modify_ticket`/`mode=clamp_bonus`/`delta=2`)——**不摘**,`_ctx_clamp_delta`(`:333-338`)与 `clamp_check` 发射点(`:315`)保持原样。 - -### 3.2 两处词表在册 - -- `game/core/effect_engine.gd:30` `EFFECT_KINDS` 含 `"spike"`;该常量实数 32 词(本卡现读,摘后 31)。全仓按 `kind == "spike"` 派发效果的代码**不存在**——`spike` 只在词表与注释里在册(另两处:`:30` 词表、`:452` 「任意真族(任一命中即生效):spawn_spell、spike」注释)。 -- `game/core/effect_engine.gd:82` `DO_KEYS`(现 26 键)含 `"trade_atk"`, `"trade_hit"`;执法点在 `:417`(`if not DO_KEYS.has(String(k))` 出域即拒绝)。摘后 24 键。 -- `attack_segments` 不属本批:它已随架构审查整改甲案整链退役(`grep -r attack_segments game/` 仅命中 `game/tests/unit/data_config/effect_kind_retired_test.gd:3` 的注释),本批只把它在 GDD 里的错句一并订正(见第 6 节)。 - -### 3.3 注释面退役名全仓扫描读数 - -名单来源=`design/content-archive/道具退役素材库.md` 两张表的「原名」列,共 41 名;减去表内仍作为现役 `display_name` 出现的 1 名(淬毒刃纹)=40 名。扫 `game/` 全域 `.gd`/`.tres`/`.md` 命中 **53 行/13 文件**,无一条落在 `.tres` 数据表里。 - -| 文件 | 注释面行号(本批改) | 字面量行号 | 字面量处置 | -|---|---|---|---| -| `game/core/damage_controller.gd` | 109、138、285、287、288、328、341、350、394、424、622 | — | — | -| `game/core/reward_grant.gd` | 2、130、183、221 | 189、226 | 两处都改中性文案(T4) | -| `game/core/settlement/grant_segment.gd` | 127、128、232、240、265、279、283、351、362、373 | 166 | 玩家可见标签改中性(T4) | -| `game/core/reward_roller.gd` | 1、41、46、86 | — | — | -| `game/core/effect_engine.gd` | 64、65、157、452 | — | 452 随 T1 摘词改写 | -| `game/core/battle_orchestrator.gd` | 258、538 | — | — | -| `game/core/action_economy.gd` | 82 | — | — | -| `game/data/schemas/treasure_item_table.gd` | 20、122 | — | — | -| `game/sim/adventure_monte_carlo.gd` | 75 | — | — | -| `game/sim/level_autoplay.gd` | 415 | 421 | 死匹配删除(T4);415 随删该处注释 | -| `game/tests/unit/core/meta_progression_forge_test.gd` | 677、690 | 694、697、699、700 | 断言串随 T4 文案换代(申报见第 8 节) | -| `game/tests/unit/battle_settlement/variant_placement_test.gd` | 239 | — | — | -| `game/tests/unit/core/treasure_items_test.gd` | — | 31、33、35 | 不动——StubDB 自造夹具名,不宣称内容现役 | - -字面量合计 11 行:本批动 8 行(三文件的四处文案/死串=`reward_grant:189`/`:226`、`grant_segment:166`、`level_autoplay:421`,加锻造测试四处断言串 `694`/`697`/`699`/`700`),其余 3 行夹具名保留。注释面 42 行全改,另加 `effect_engine.gd:452` 一句随摘词改写=本批改写 43 处。 - -### 3.4 玩家可见文案三处 - -- `game/core/reward_grant.gd:226`:`mine_hit` 减半路径发射的结算明细行标签——`"惊巢反制——危险格挖掘惩罚减半(%d→%d)" % [...]`,载荷 `"effect_id": "nest_counter"`。`惊巢反制` 与 `nest_counter` 都是退役件名(素材库表一 :18),而这行文案是玩家能看见的,且这条路径今天可达(有现役锻造锋行驱动——`meta_progression_forge_test.gd:694-700` 正在钉它)。 -- `game/core/reward_grant.gd:189`:`_cell_removal_open` 空集拒绝 reason——`"爆破工兵包:3×3 区域内无可移除危险格(须未揭示且未封印)"`。该通道 `cell_removal` 在册(`game/data/schemas/treasure_item_table.gd:26`、消费点 `reward_grant.gd:160-161`)但零现役数据行(`game/data/` 全域零命中),所以 reason 今天不可达;文案仍挂退役件名。通道本体不进本批(见第 5 节),本批只清文案。 -- `game/core/settlement/grant_segment.gd:166`:`mine_hit` 订阅中 `kind=open_ticket` 且 `channel="reflect"` 的行开单,明细标签硬编码 `"惊巢反制·反弹"`,归因串 `"attune_nest_counter"`。`"reflect"` 在 `game/data/tables/` 二十一张表逐张 grep 零命中 → 今天不可达,与 `:189` 同类问题(标签挂退役件名)。`reflect` 渠道本体不进本批(见第 5 节),本批只清标签与归因串。 - -### 3.5 一处 sim 死匹配 - -`game/sim/level_autoplay.gd:421` 用 `String(row.get("display_name", "")).contains("雷区图谱")` 捕高光事件,`:415` 是配套注释。`雷区图谱` 零现役行(`game/data/` 零命中)→ 该条件恒 false。 - -### 3.6 既有测试面读数(决定本批要不要改测试) - -- 四个 mode 与两个键在 `game/tests/`、`game/probe/`、`game/sim/` **零命中**(`grep -rn "trade_hit\|trade_atk\|armor_rewrite\|discount_shift\|spike" game/tests/ game/probe/ game/sim/` 空)。摘分支不动任何既有断言。 -- `armor_roll` 有既有断言:`game/tests/unit/battle_settlement/roll_multiplier_test.gd:125 _smoke_armor_roll()`——它测的是护甲骰与折扣读数,不是订阅消费。所以掷点面 `:293 _armor_roll()` 与发射调用 `:296 _fx_hits("armor_roll", ...)` 都保留(`armor_roll` 在 `effect_engine.gd:13` 事件词表在册),只删 `:305-309` 消费环,`ar_hits` 局部变量随删。 - -## 4 设计契约 - -- 🔴 **契约 1|零现役注册坐实才许摘**:T0 三条 grep 读数必须全为零——`game/data/tables/` 内 `spike`/`armor_rewrite`/`discount_shift`/`trade_hit`/`trade_atk` 各 0 行、`tree_rules` 面 0 行、`effect_kinds` 面 `cell_removal` 0 行(后者只作记录,本批不摘通道)。任一数非零=该分支可达=停卡 L3 呈报,禁直接摘。 -- 🔴 **契约 2|注释改写只换名不换职责**:42 行注释逐处保留「这段在做什么、按什么序、为什么这样取」的陈述,把退役件名替换成机制语言。整行删除、成片缩写、或删注释换 gdstyle 行数绿=违规(先例:宿主行数压至 999 时「禁删注释凑行数红线未碰」)。 -- 🔴 **契约 3|文案换代禁碰数值与发射序**:`:226` 只改标签字符串,`[int(row.power), half]` 取值、`emit_cb.call` 参数位序、`"effect_id"` 载荷键一律不动;`:189` 只改 reason 前缀,`{"ok": false}` 形态不动。 -- 🔴 **契约 4|词表收窄与负域探针同批**:摘 `EFFECT_KINDS` 的 `spike` 与 `DO_KEYS` 的两键,必须同批落 T2 探针;只摘词不取证=本卡未完成。 -- 🔴 **契约 5|禁留空壳**:`spike_ready` ctx 键、`ar_hits`/`shift`/`trade_hit` 局部变量、`_discount_shift_up` 函数随分支即删,禁保留空返回、别名或「以后可能用」的成员凑机检文本。 -- 🔴 **契约 6|实现批禁写文档**:`design/` 整个目录、`待办清单.md`、`体验验证清单.md`、`game/tests/README.md` 一律不写;要改的文档句子全进第 6 节与反哺点清单。 -- 🔴 **契约 7|零新增数值**:本批不引入任何玩法数值,不改 `.tres` 表值。MC 与 verifier 零跑(无数值改动零跑)。 - -## 5 范围边界 - -做:3.1 四段分支+穿线键+`_discount_shift_up`;3.2 两处词表摘名;3.3 注释面 43 处;3.4 三处文案;3.5 一处 sim 死匹配;3.6 消费环 `armor_rewrite` 删而发射点留;T2 负域探针;T4 断言串随动。 - -不做,去向写明: - -| 不收件面 | 为什么不做 | 去向 | -|---|---|---| -| `EVENTS` 零订阅词普查(含 `armor_roll` 发射点去留) | 与「改写分支」不同轴,是事件词表整体普查 | 待办清单本批同登条目「效果通道与事件词的零现役普查」 | -| `effect_kinds`/渠道零现役清退(`cell_removal`、`mine_hit_halve`、`open_ticket` 的 `channel="reflect"` 行族) | 通道在册、有消费点、只缺数据行——判「删通道」要连同 schema 词、归因标识与拒绝文案整体裁 | 同上条目,与 iter52 孤儿函数清退批同族 | -| GDD 其余仍提退役内容名的句子(`systems/冒险循环/奇物.md`、`systems/战斗循环/地图战斗资源.md`、`术语表.md`、`00-游戏概念.md`、`03-内容规模.md`) | 属文档面用语,不属代码批 | 反哺点清单,文档批落文 | -| `game/tests/unit/core/treasure_items_test.gd:31/33/35` 三个退役件名夹具 | StubDB 自造行,不宣称该件现役;改名动的是测试面 | 无——保留 | -| `attack_segments` 词表补摘 | 已随甲案退役,本批只在 GDD 订正错句 | 无——已完成 | -| 结算明细标签是否入表(文案硬编码在 `:226`) | 涉及「呈现文案归属」规范裁决,不属本卡 | 反哺点清单,文档批裁 | - -## 6 关联文档同步面 - -| 文档 | 关联点 | 同步义务 | -|---|---|---| -| `design/gdd/systems/跨局循环/锻造工坊.md` §满级特效扩展位 | 闭集词数句、长枪候选代价档、重甲候选代价档、七条前提句 | **已在本批前置文档段落文**(v0.13);实现批禁再写该文件 | -| `design/gdd/systems/跨局循环/锻造工坊.md` 同节 | 重甲反震候选的实现前提:反弹需新开消费点(本批摘 `spike` 后彻底无位) | 设计权威文档,实现批不直改;写入 `reports/<迭代号>-反哺点-<日期>.md`,用户裁后归文档批 | -| `design/gdd/systems/战斗循环/增益状态.md`/`伤害与护甲.md` | 若其中叙述「改写族四拍」的句子里挂了退役件名 | 机械面核对;有则列反哺点,无则实现报告记「零命中」 | -| `game/tests/README.md` 能力表 | 本批无新增能力、无能力退役;T2 探针挂既有行「加载期违规表拒绝:跨表悬空/计数器前缀/真视窄域/效果类型出域」(:177,代表用例含 `effect_kind_retired_test.gd:_assert_rejected`) | 无改动义务;批末核对表上名字与落地一致 | -| `待办清单.md`/`roadmap.md` | 条目状态与迭代行 | 由写卡段与编排段推进,实现批禁写 | - -## 7 任务卡 - -### 文件结构图 - -| 文件 | 职责 | 改动性质 | -|---|---|---| -| `game/core/damage_controller.gd` | 主角/敌方伤害结算编排 | 非行为改动(摘不可达分支+注释改写)→ 对照表 R1、R4 | -| `game/core/effect_engine.gd` | 效果词表与行校验 | 非行为改动(摘词两枚+注释改写)→ R2、R3、R4 | -| `game/tests/unit/data_config/effect_kind_retired_test.gd` | 负域加载探针宿主 | 新增两个探针常量与两次调用(非行为改动的取证面)→ R2、R3 | -| `game/core/reward_grant.gd` | 奖励与格面兑付 | 行为改动(一处明细行标签+一处拒绝 reason 文案)→ R5、R6 | -| `game/tests/unit/core/meta_progression_forge_test.gd` | 元进度/锻造断言宿主 | 断言串随 R5 换代(申报见第 8 节)→ R5 | -| `game/sim/level_autoplay.gd` | autoplay 驱动件 | 非行为改动(删恒 false 匹配+注释)→ R4、R8 | -| `game/core/settlement/grant_segment.gd` | 授予段结算编排 | 行为改动(`:166` 明细标签文本)→ R7;注释面 10 行改写 → R4 | -| `game/core/reward_roller.gd`、`game/core/battle_orchestrator.gd`、`game/core/action_economy.gd`、`game/data/schemas/treasure_item_table.gd`、`game/sim/adventure_monte_carlo.gd`、`game/tests/unit/battle_settlement/variant_placement_test.gd` | 注释面 | 非行为改动(注释改写)→ R4 | - -零新增文件、零新增函数、零新增数据件;唯一的"新增"是既有套件内的两个探针常量。 - -### T0 基线与读数对账(0.25 工作日) - -取用:无。产出:改前基线三件——对拍转储、S13 键读数、契约 1 三条 grep 读数。 - -- [ ] 跑契约 1 三条 grep,逐条把命中数写进实现报告 T0 节(期望全 0,除 `feats.tres:854` 的 `clamp_bonus` 一行作对照记录)。 -- [ ] 读 `tools/gdstyle_baseline.txt` 内 `damage_controller.gd`/`effect_engine.gd`/`reward_grant.gd`/`level_autoplay.gd` 现登记键,抄进实现报告「存量触面」表(第 14 节要求只降不升)。 -- [ ] 采改前对拍基线:`game/sim/dump_battle.gd` 六组(L{1,2}×S{42,779401,779501},stdout 单流转储,先例=拆件批 T6 同参)。 -- [ ] 采改前明细行读数:`meta_progression_forge_test.gd:_smoke_tree_rules` 现断言串的完整文本抄进实现报告(T4 换代后的比对基准)。 - -### T1 摘四段死分支与穿线键(0.5 工作日) - -取用:T0 的契约 1 读数全零。产出:`_ctx_rewrites` 与 `_hero_attack_ctx` 的收窄返回键集(T2、T4 引用)。 - -- [ ] `damage_controller.gd`:删 `:297-302`(命中侧交换)、`:351-353`(增伤侧交换)、`:305-309`(免掷覆盖)、`:310-313` 置位块+`:368-372`(spike 倍乘)、`:320-323`(移档调用)与 `:623` 函数 `_discount_shift_up(discount: int, shift: int) -> int`。 -- [ ] 同文件:删 `clamp_hits` 收集后残留的 `shift` 局部变量、`ar_hits` 局部变量(发射调用 `:296` 保留但不接收返回值,见 3.6);`spike_ready` 键从 `:282`、`:325` 返回字典与 `:369` 消费处一并去除。 -- [ ] 收窄后的签名与键集照此,不得自创: - - `_ctx_rewrites(t: Dictionary, target: Dictionary, armor: String, matchup: String) -> Dictionary` 返回键集 = `{roll_read, threshold, crit, discount, clamp_hits}`。 - - `_hero_attack_ctx(t: Dictionary) -> Dictionary` 返回键集 = 现键集去掉 `spike_ready`(`:278-282` 其余键逐字保留)。 - - `_hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float` 签名不变,体内删两拍。 -- [ ] 头注与分节注释随删随改(`:249-254`、`:264-266`、`:285-290`、`:341-342`、`:368`):改写后的四拍叙事只剩「掷点折扣读数 → 克制点数」两拍,注释口径同步收窄(这条与 T3 同判据,禁再挂退役件名)。 -- [ ] 单元侧适配:`game/tests/unit/battle_settlement/damage_controller_test.gd`、`roll_multiplier_test.gd`、`game/tests/unit/contracts/system_contracts_test.gd` 跑一遍;3.6 已证零命中,若真出红,只改装配不改进判据,并把改动写进实现报告「实现决定记录」。 - -### T2 词表收窄与负域探针(0.5 工作日) - -取用:T1 摘除后的 `damage_controller.gd` 键集(探针只需构造表行,不引用控制器内部键)。产出:`effect_kind_retired_test.gd` 两枚新探针常量 `_assert_rejected` 新断言名(R2/R3 引用)。 - -- [ ] `effect_engine.gd:30` 摘出 `"spike"`(`EFFECT_KINDS` 实数 32→31);`:82` 摘出 `"trade_atk"`, `"trade_hit"`(`DO_KEYS` 19→17 项,实数以摘后 `grep` 行内计数为准)。 -- [ ] 同文件 `:452` 组合规则注释改为只列现役:「任意真族(任一命中即生效):`spawn_spell`。」;`:64`/`:65`/`:157` 注释改写并入 T3 判据面。 -- [ ] `game/tests/unit/data_config/effect_kind_retired_test.gd`:按既有形态新增两枚探针——`PROBE_SPIKE_MODE`(注入 `{"kind": "modify_ticket", "mode": "spike"}` 行的夹具目录)与 `PROBE_TRADE_KEY`(注入带 `trade_hit` 键的 do 字典)。各建一个 `user://` 夹具目录常量,沿用 `_inject(dir, probe)` 与 `_assert_rejected` 既有函数,不新建套件文件。 -- [ ] 断言名与尾缀:`_assert_rejected` 内新增两条检查——`"词表闭面:kind=spike 出域拒绝"` 与 `"do 键闭面:trade_hit 出域拒绝"`;该套件已在 `game/tests/run_all.gd:195` 负面清单在册(`expect=1`、`negative=true`),本步只加探针,不动在册条目与退出码判据。 -- [ ] 独立验证先行:只跑该套件(`godot --headless --path game --script res://tests/unit/data_config/effect_kind_retired_test.gd`)出绿,再并入全套。 - -### T3 注释面退役名改写(0.5–1 工作日,可切 T3a/T3b) - -取用:3.3 表逐行清单。产出:注释面 grep 零命中读数(对照表 R4)。 - -- [ ] 名单唯一来源=`design/content-archive/道具退役素材库.md` 两张表的「原名」列,现场解析;禁把 40 个名字复制成第二份清单文件(第 12 节唯一面)。 -- [ ] 逐处改写口径=「这段代码做什么、按什么序、取值从哪来」,内容名换成机制名。示例(禁照抄字句,只照抄口径): - - `:285` 「第一拍矿工重锤命中交换」→ 「命中判定减成换攻击增伤的交换位(现役零注册,随本批摘除后此句消亡)」——摘除后该注释整段随分支删除,改写只适用于 T1 未摘的 11 个文件行。 - - `reward_roller.gd:1` 「偏运硬币/board_skew 两路投影」→ 「订阅加权与 board_skew 两路投影」。 - - `grant_segment.gd:232` 「完美布防=本轮封印批全中」→ 「封印全中倍率=本轮 hit>0 且 miss==0 时」。 -- [ ] 判据 grep:改写后重跑 3.3 那条扫描,注释面命中数必须为 0;读数贴进实现报告。 -- [ ] 不碰第 5 节点名保留的三行夹具串。 - -### T4 玩家可见文案与死匹配(0.25 工作日) - -取用:T0 采到的改前明细行读数。产出:新标签文本(R5/R6 引用)。 - -- [ ] `reward_grant.gd:226` 标签 → `"挖掘惩罚减半(%d→%d)"`;同处归因串 `"effect_id": "nest_counter"` → `"nest_halve"`(依代码规范用语纪律清零退役名;该串全仓仅此一处、不进存档不读表,改动面=本行+测试匹配串随动)。 -- [ ] 同文件 `:183` 与 `:221` 注释随 T3 口径改写;`:189` reason → `"中立移除:3×3 区域内无可移除危险格(须未揭示且未封印)"`。 -- [ ] `game/core/settlement/grant_segment.gd:166` 明细标签 `"惊巢反制·反弹"` → `"反弹伤害"`,同处归因串 `"attune_nest_counter"` → `"attune_nest_halve"`;`channel == "reflect"` 判据与开单参数字典逐字不动(契约 3)。 -- [ ] `meta_progression_forge_test.gd`:`:694` 匹配串、`:697` 标签文案、`:699`/`:700` 期望串四处随动到新标签;判据仍绑 `step_row` 采集的 `display_name`,`nc_hit != ""` 与等值断言的强度不得降低。 -- [ ] `level_autoplay.gd:421` 恒 false 的 `contains("雷区图谱")` 条件与该分支一并删,`:415` 注释随删。 -- [ ] 窄测:`meta_progression_forge_test`、`round_flow_skill_pool_test`、`damage_controller_test`、`table_structure_guard_test` 四套跑绿。 - -### T5 产出物落位卡(0.25 工作日) - -- [ ] 轻闸全绿后、合并前写齐 `reports/<迭代号>-实现报告-<日期>.md`:各 T 卡状态、T0/T2/T3/T4 四条读数、契约 1 三条 grep、改前改后对拍逐位一致结论、实机目检总清单(本批目检项:结算明细行减半标签一眼可读、无退役件名突兀感——登记不阻塞)、遗留读数义务(零现役通道与零订阅事件词两件,已另登待办条目,见第 5 节)、实现决定记录。 -- [ ] `reports/<迭代号>-反哺点-<日期>.md`:第 6 节每条都要有落点,含「明细标签是否入表」这条规范裁决点。 -- [ ] 里程碑步出口跑一次全套闸(`tools/run_all_gates.sh --code`);波内出红只做出红最小单元最窄确认。 - -## 8 验证闸(改动—验收对照表) - -| # | 需求点(判据出处行锚) | 改动(文件:行) | 验收测试(名称+落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 能力行 | -|---|---|---|---|---|---|---|---| -| R1 | 摘四段不可达改写分支(锚 `design/engineering/code-conventions.md:15`) | `damage_controller.gd:297-302/305-309/310-313/351-353/368-372/320-323/623` | `damage_controller_test.gd:_run_recipe_sequence`;`roll_multiplier_test.gd:_smoke_armor_roll`;`round_flow_skill_pool_test.gd:_smoke_full_battle`+dump_battle 六组逐字节比对 | 单元+集成 | `BattleOrchestrator.begin_battle()` → 主角先手开单兑付全链 | `step_row` 明细行文本与 `outcome` 取值逐位=T0 基线;静态判据=契约 1 三条 grep 零注册+摘后键集 grep 零 `spike_ready`。**观测面局限申报**:零现役注册行使分支不可达,无法从真实入口造出命中;禁人为注入表行取证(全项目禁变异类测试) | 既有:伤害单据编排与回放;既有:伤害公式与掷点乘区;既有:轮内流与结算五段全序 | -| R2 | `EFFECT_KINDS` 摘 `spike`(锚 `design/engineering/code-conventions.md:15`) | `effect_engine.gd:30` | `effect_kind_retired_test.gd:_assert_rejected`(新断言「词表闭面:kind=spike 出域拒绝」,尾缀 `expect=1 negative=true`) | 单元(负面,独立进程) | `ConfigDB.load_tables()` → `EffectEngine.validate_trigger_row` | 出域错误载荷含 `effect_kinds 出域` 文案+进程退出码 1 | 既有:加载期违规表拒绝:跨表悬空/计数器前缀/真视窄域/效果类型出域 | -| R3 | `DO_KEYS` 摘 `trade_hit`/`trade_atk`(锚 `design/engineering/code-conventions.md:15`) | `effect_engine.gd:82` | `effect_kind_retired_test.gd:_assert_rejected`(新断言「do 键闭面:trade_hit 出域拒绝」) | 单元(负面,独立进程) | `ConfigDB.load_tables()` → `EffectEngine.validate_trigger_row` | 键出域拒绝错误载荷含 `trade_hit`+进程退出码 1 | 既有:加载期违规表拒绝:跨表悬空/计数器前缀/真视窄域/效果类型出域 | -| R4 | 注释面退役名清零 13 文件 42 行(锚 `design/engineering/code-conventions.md:26`) | 3.3 表全部注释行号 | 静态判据:3.3 那条扫描命令注释面命中 0,读数贴实现报告;`--code` 域内静态扫描与 S11 段不变红;宿主既有套件逐位不变 | 静态(闸件扫描) | `BattleOrchestrator.begin_battle()` 链上宿主套件(改写面不进运行期) | 逐文件 grep 读数(文件名+行号+命中 0)+宿主套件回归 | 既有:换图系刷新链路与效果词表接线;既有:伤害单据编排与回放 | -| R5 | 减半明细行去退役件名(锚 `design/engineering/doc-conventions.md:47`) | `reward_grant.gd:226` | `meta_progression_forge_test.gd:_smoke_tree_rules`(换代断言串,落点 694/697/699/700) | 集成(信号采集) | 锻造锋线满级特效 → `BattleOrchestrator.begin_battle()` → `mine_hit` 减半路径 → `step_row` 信号 | `step_row` 行 `display_name` == 「挖掘惩罚减半(6→3)」,且 `power` 减半数值与发射次数逐字不变;`grep -r nest_counter game/` 零命中 | 既有:天赋树购买与六键合成 | -| R6 | 空集拒绝 reason 去退役件名(锚 `design/engineering/test-conventions.md:116`) | `reward_grant.gd:189` | 静态判据:`grep -c 爆破工兵包 game/core/reward_grant.gd` == 0;`table_structure_guard_test.gd` 与 `extended_row_violation_test.gd:_init` 跑绿 | 静态+单元 | `ConfigDB.load_tables()` → `_cell_removal_open` 所在兑付链(零现役行不可达) | grep 零命中读数+通道校验面绿。**观测面局限申报**:`cell_removal` 零现役行,reason 不可达,禁造行取证 | 既有:加载期违规表拒绝:跨表悬空/计数器前缀/真视窄域/效果类型出域 | -| R7 | 反弹明细标签去退役件名(锚 `design/engineering/doc-conventions.md:47`) | `grant_segment.gd:166` | 静态判据:`grep -rc "惊巢反制·反弹" game/` == 0 且 `grep -rc "attune_nest_counter" game/` == 0;`round_flow_skill_pool_test.gd:_smoke_full_battle` 跑绿 | 静态+集成 | `BattleOrchestrator.begin_battle()` → `mine_hit` 订阅施加链(`channel="reflect"` 二十一张表零命中,今天不可达) | grep 零命中+发射面零变化回归。**观测面局限申报**:同上,禁造行取证 | 既有:轮内流与结算五段全序 | -| R8 | sim 恒 false 匹配删除(锚 `design/engineering/test-conventions.md:116`) | `level_autoplay.gd:415/421` | 静态判据:`game/sim/` 全域 grep `雷区图谱` 零命中;`--code` 域内驱动件相关套件跑绿 | 静态+单元 | `game/sim/level_autoplay.gd` 驱动链(测量面,不进游戏运行) | grep 零命中+套件绿。**观测面局限申报**:恒 false 条件不可达,禁造行取证 | 既有:轮内流与结算五段全序 | - -测试方式两分法:R1/R2/R3/R5 归调试接口与真实入口断言;R6/R7/R8 三处的改动点今天不可达(零现役注册行驱动),一律走静态判据+观测面局限申报,禁造行取证,也不设目检;R5 的文本观感归用户目检——目检项「结算明细减半标签文本可读」写进实现报告总清单,登记不阻塞,不作合回前置。 - -闸口径:本批改动含 `game/` 下非 `.md` 行为载体 → 按 `tools/run_all_gates.sh --code` 选档,所选域全绿才合并;卡内不写死闸件名单。 - -回归措辞:里程碑步(T 段)出口跑一次全套闸;波内修复只做出红最小单元的最窄确认;新增探针先行孤立验证再并入全套。 - -测试削弱申报:唯一允许改的旧断言=`meta_progression_forge_test.gd:694/697/699/700` 四处期望串,理由是 R5 文案换代要求名实相符;断言强度(非空判定+等值判定+不含旧词)一条不降,阈值与扫描面不放宽。除此之外任何断言删除、阈值放宽、扫描面缩小都属未申报改动,独立验收审计判红。 - -独立验收审计:入口=合回前由未参与实现的会话或子代理执行,报告落 `reports/<迭代号>-验收审计-<日期>.md`;本档验收档=中,按 test-conventions §8.7 中档梯度核(R1/R2/R3/R5 四条必须逐条核到判据绑定面)。 - -## 9 异常分级 - -- L1 自救:注释措辞不通、局部变量名残留、grep 命令写法——就地改,不动判据。 -- L2 记录继续:T1 单元侧适配出现既有断言装配报错(3.6 已预判零命中),按申报范围改装配并记入实现报告「实现决定记录」。 -- L3 停机呈报并写报告: - 1. 契约 1 任一条 grep 读数非零(分支可达)=上游事实与卡片互斥,停卡退回写卡段。 - 2. 摘词后加载出现非预期致命(现役行被误判出域)或现役套件出红且原因落在本批摘除面。 - 3. 机检预算与卡片契约互斥(例:收窄后某函数键数低于登记值触发基线红/留空壳才能凑绿)=停卡退回写卡段,禁自造第三条路。 - 4. 按纪律无法干净摘除、必须保留中间壳函数才能过闸=停卡呈报走豁免/还债通道,禁硬拆凑绿。 - 5. R5 断言换代后仍红且原因在数值或发射序(说明文案改动越界)。 - -## 10 代拍板白名单 - -允许实现期自行拍板:注释改写的具体中文措辞(守契约 2 即可);负面探针常量与夹具目录命名(沿用既有形态);grep/扫描命令写法;是否把 T3 与 T4 并成一次提交;测试文件内局部变量改名;实现报告小节标题措辞。 -超出白名单=记入实现报告「实现决定记录」,不得静默。 - -## 11 待裁点 - -零待裁点,开工门=U0。(机制删留与注释口径已由用户在 2026-11-05 会话裁毕;GDD 四处错陈述已在写卡段前置文档段落文 v0.13;「明细标签是否入表」与三件遗留读数不属本卡执行面,已列第 5/6 节去向。) - -## 12 落位与可复用性预检 - -- 复用面:本批零新增文件。负域探针复用 `game/tests/unit/data_config/effect_kind_retired_test.gd` 既有 `_inject`/`_assert_rejected` 骨架——它正是「退役词出域即加载致命」的现成载体,新建套件=重复落点。注释改写复用既有文件与函数,无新 helper。 -- 唯一面:退役内容名清单的唯一来源=`design/content-archive/道具退役素材库.md`,判据脚本现场解析,禁在 `game/` 或 `tools/` 落第二份名单;摘词只改 `effect_engine.gd` 两处常量,`treasure_item_table.gd:20` 的注释属说明面(随 T3 改写),不另立词表。 -- 分层与交互面:`game/core/damage_controller.gd` 与 `game/core/effect_engine.gd` 的交互仍走既有 `_fx_hits` 查询口与 `validate_trigger_row` 校验口,本批只删消费环、不改接口;`reward_grant.gd` 文案改动不动 `emit_cb` 契约;`game/sim/` 驱动件不回连产品码。Core 零 UI、数值只经 ConfigDB、随机只经 RngService 三条红线本批一律不绕(本批不新增数值与随机消费)。 - -## 13 禁为机检文本保留无效成员 - -本卡为清退类:禁止为迎合机检消息文本或计数保留/新增任何无效成员、空壳函数、无用别名或 ctx 冗余键——`spike_ready`、`ar_hits`、`shift`、`trade_hit` 局部变量与 `_discount_shift_up` 一律随分支删净。机检键与本卡契约互斥时,实现段停卡呈报,不得自造第三条路。 - -## 14 实现规范预算 - -本批无新增文件,新件三查不适用;执行的是存量触面一查: - -- `game/core/damage_controller.gd`:T1 摘除使 `_ctx_rewrites` 与 `_hero_attack_base_value` 的分支数、函数长度下降;`_discount_shift_up` 整函数消亡。预期 S13 键只降不升,T0 抄录现登记值、T5 抄录摘后读数两张对照。 -- `game/core/effect_engine.gd`:两处常量字面量变短,方法数与成员变量数不变。 -- `game/core/reward_grant.gd`、`game/sim/level_autoplay.gd`:只删文本与一条恒 false 条件,分支数下降或持平。 -- 指标例外通道:本卡不申报任何例外——若摘除后仍有键超默认,属存量在册键,沿用 `tools/gdstyle_baseline.txt` 现登记,不新增。 - -## 15 拆分边界纪律 - -本卡是清退类,不产生拆出单元;条款照写以防实现期顺手重组: - -1. 若实现中确需拆出新私有函数或新段件,每个单元必须能写出一句职责陈述与单一变化原因,并逐件写进实现报告;写不出=凑计数袋,回炉。 -2. 「过阈值」只作发现信号,禁作为本批拆分或收拢的动机——本批唯一动机是零现役注册分支的清退。 -3. 按纪律无法干净摘除时停卡呈报走豁免/还债通道,禁硬拆凑绿(进第 9 节 L3 第 4 条)。 diff --git a/roadmap.md b/roadmap.md index ad76e8a7..91cf5666 100644 --- a/roadmap.md +++ b/roadmap.md @@ -5,8 +5,8 @@ ## 当前位(只回答「现在该做什么」;三行内随批次换代,禁止在此扩写成状态快照) - **阶段**:M3——Alpha 版本的完整实现(2026-10-09 立规:未明令宣布完成前停留 M3,迭代只排 M3 需求,M4 不排期;根目录 stage.txt 为旧阶段开关仅存档)。 -- **迭代位**:迭代 1–52 全部已完成;各迭代收口与批后收尾沿革查 git 提交信息与 `reports/`。 -- **下一步**:无已编迭代在飞——待编池:改写族孤儿(待处置方向裁决)、二三关棋盘观感(待设计+待美术)、EC8 热重载实装(待用户另立条目);测试面真输入化分期与存量断言逐条精锚(待执行,归下批编排)。 +- **迭代位**:迭代 1–53 全部已完成;各迭代收口与批后收尾沿革查 git 提交信息与 `reports/`。 +- **下一步**:无已编迭代在飞——待编池:效果通道与事件词零现役普查(待写卡)、结算明细文案入表(待文档设计)、二三关棋盘观感(待设计+待美术)、测试面真输入化分期与存量断言逐条精锚(待执行)、QA 覆盖总览面板与自动试玩扩展(待执行);EC8 热重载实装待用户另立条目。 ## 里程碑时间线 @@ -86,7 +86,7 @@ | 迭代 50 | 已完成(2026-11-03) | 已退役(git 历史可考) | 2026-11-03 | ≈12.875 个工作日〔用户特批 2026-11-03 超 10 个工作日〕 | 已退役(git 历史可考) | | 迭代 51 | 已完成(2026-11-04) | 已退役(git 历史可考) | 2026-11-04 | ≈6.625 个工作日 | 已退役(git 历史可考) | | 迭代 52 | 已完成(2026-11-05) | 已退役(git 历史可考) | 2026-11-04 | ≈9.275 个工作日 | 已退役(git 历史可考) | -| 迭代 53 | 待开工 | reports/需求卡片-卷轴视口批-2026-11-05.md
reports/需求卡片-改写族孤儿机制与退役名清退批-2026-11-05.md | 2026-11-05 | ≈6.0 个工作日 | — | +| 迭代 53 | 已完成(2026-11-06) | 已退役(git 历史可考) | 2026-11-05 | ≈6.0 个工作日 | 已退役(git 历史可考) | - 历史行的「—」= 该列事实不可考:迭代 1~8 早于需求卡片制;编排时点与预计周期不再回溯,细节查 git 提交史。 - 「已退役(git 历史可考)」= 需求卡片或实现报告曾存在,已按「证据只留最新」退役;已完成迭代的实现报告列只填 reports/ 现存路径或本保留字,不填「—」。 diff --git a/tools/capability_audit.py b/tools/capability_audit.py index 64dcb891..6cb4b237 100644 --- a/tools/capability_audit.py +++ b/tools/capability_audit.py @@ -132,6 +132,16 @@ def product_funcs(): return names +def probe_funcs(): + # 真实入口锚面补探针共用面(capability E 规则用):真输入合成沿(probe_ui_util 等探针 + # 共用件)写进能力行真实入口格后,卡片退役其凭证只能锚代码面——探针件定义与 D 规则 + # 同口径认锚(仍要求 func 真实存在,不属放宽)。 + names = set() + for p in (REPO / "game" / "probe").rglob("*.gd"): + names.update(re.findall(r"^(?:static\s+)?func\s+([A-Za-z_]\w*)\s*\(", read(p), re.M)) + return names + + def cards_corpus(): """reports/需求卡片*.md 全文拼接(双通道的「卡在」锚面);无卡返回空串。""" if not CARDS_DIR.is_dir(): @@ -266,7 +276,7 @@ def check_entry(no, name, cell, funcs, cards): for _, method in tokens: if method not in funcs and not (cards and method in cards): red(README.relative_to(REPO), no, "真实入口", - "「%s」方法 %s() 无凭证——产品码与 sim 测量件无定义且需求卡片未载" % (name[:16], method)) + "「%s」方法 %s() 无凭证——产品码、sim 测量件与探针件均无定义且需求卡片未载" % (name[:16], method)) def check_coverage(cov_tables, names): @@ -333,7 +343,7 @@ def main(): suites = manifest() if not suites: selfcheck.append("run_all.gd manifest 零套件") - funcs = product_funcs() + funcs = product_funcs() | probe_funcs() if not funcs: selfcheck.append("产品码零 `func`") names, referenced = check_capability_rows(cap_tables, suites, funcs, cards_corpus()) diff --git a/tools/glow_discrimination_sheet.py b/tools/glow_discrimination_sheet.py new file mode 100644 index 00000000..d80d8a1f --- /dev/null +++ b/tools/glow_discrimination_sheet.py @@ -0,0 +1,444 @@ +# 三关色相 × 五组微光互辨抽测——S1 取铺底值(机检 D)+ S2 出并列样图(机检 A/B/C)。 +# 执行依据:reports/提案-三关色相互辨抽测方案-2026-10-29.md(抽测任务=待办清单 +# 「三关色相 × 五组微光互辨抽测」条)。产物:并列样图(artwork/,不入 git)+逐对读数表(reports/)。 +# 口径出处(冲突时以 owner 为准): +# 微光色值与呈现参数 = game/presentation/views/ui_kit.gd TG_GLOW_*/TYPE_GLOW_*(v2 冻结); +# 合成公式 = game/presentation/views/cell_type_glow.gdshader(blend_mix 距离场柔光); +# 带域/格心取样 = game/probe/type_glow_shot_probe.gd(de<格边×0.125 / 内缩 20%); +# 硬指标门 = art-bible §4.1(L≥0.30 线性域 / 饱和≤0.55 / 避 300°–345°); +# 既有色板比对池 = game/tests/golden/palette_baseline/ 两张条带并集(palette_check.py 同源)。 +# 方案 §2.3 机检 C 口径订正:模型 0.49/0.51 是反解近似(ui_kit.gd 头注「模型域与实测的差 +# = shader 距离场权重非扁平混合」),比较对象改为 shader 精确合成模型(逐像素复刻),判据线 +# ±2/255 沿用在役值。 +# 方案 §7 表二行数笔误订正:表二实为 90 行(3 关 × 3 底型 × 10 组对),非 30 行。 +# +# 用法: +# python tools/glow_discrimination_sheet.py --art [--no-sheet] +# 输出: +# /glow-discrimination-sheet-2026-11-05.png(样图:正图段+灰度副本段+图例) +# reports/读数-三关色相互辨抽测-2026-11-05.md(逐对表 45 行+组间表 90 行+机检汇总) +import argparse +import math +import os +import sys + +from PIL import Image + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# ── ui_kit.gd 冻结常量(逐字转写,禁改值)── +GLOWS = [ + ("BUFF", (247, 223, 67)), # 通用增益·芥末黄 + ("HEAL", (104, 208, 66)), # 治疗·黄绿 + ("PIERCE", (48, 166, 166)), # 穿刺·青 + ("MAGIC", (123, 91, 214)), # 魔法·紫罗兰 + ("AOE", (166, 29, 55)), # 群伤·绯红 +] +GLOW_ALPHA = 170.0 / 255.0 +BAND_FRAC = 0.125 +SOFT = 0.55 +CORNER = 2.0 + +CELL = 45 # D 区实比格边 +BAND = CELL * BAND_FRAC # 5.625 +CORE_PAD = int(CELL * 0.20) # 格心内缩(探针 CORE_PAD_FRAC=0.20) + +# ── 三关铺底代表值 = 色表墙顶受光家族最亮槽 H2(tools/recolor_terrain_tiles.py PALETTES)── +H2_VALUES = { + "dungeon": (181, 155, 119), + "underdark": (186, 193, 210), + "deep_underdark": (150, 158, 186), +} +BOARD_TYPES = ["dungeon", "underdark", "deep_underdark"] +BASE_VARIANTS = [ # (底型名,瓦片件名后缀)——素砖 + 两种装饰变体(高墙瓦片词表在册件) + ("素砖", "terrain_wall_center"), + ("裂纹", "terrain_wall_center_crack"), + ("碎石斑", "terrain_wall_center_speckle"), +] + +TILE_DIR = os.path.join(REPO, "game", "assets", "art", "board") +BASELINE_DIR = os.path.join(REPO, "game", "tests", "golden", "palette_baseline") + +CHECKS = [] + + +def check(cid, name, ok, detail=""): + CHECKS.append((cid, name, bool(ok), detail)) + print("[%s] %s %s — %s%s" % ("PASS" if ok else "FAIL", cid, name, "✓" if ok else "✗", + (";" + detail) if detail else "")) + + +# ── 色彩数学 ── +def to_linear(v): + c = v / 255.0 + return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 + + +def rel_lum(rgb): + return 0.2126 * to_linear(rgb[0]) + 0.7152 * to_linear(rgb[1]) + 0.0722 * to_linear(rgb[2]) + + +def gray709(rgb): + return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2] + + +def hsv(rgb): + r, g, b = [v / 255.0 for v in rgb] + mx, mn = max(r, g, b), min(r, g, b) + d = mx - mn + if d == 0: + h = 0.0 + elif mx == r: + h = 60 * ((g - b) / d % 6) + elif mx == g: + h = 60 * ((b - r) / d + 2) + else: + h = 60 * ((r - g) / d + 4) + s = 0.0 if mx == 0 else d / mx + return h % 360, s + + +def wcag(c1, c2): + l1, l2 = rel_lum(c1), rel_lum(c2) + return (max(l1, l2) + 0.05) / (min(l1, l2) + 0.05) + + +def smoothstep(e0, e1, x): + t = max(0.0, min(1.0, (x - e0) / (e1 - e0))) + return t * t * (3.0 - 2.0 * t) + + +def glow_weight(px, py): + """shader 复刻:像素中心 (px,py) 的微光权重 = glow_alpha × a(d) × mask。""" + h = CELL / 2.0 + d = min(px, py, CELL - px, CELL - py) + r = min(CORNER, CELL * 0.25) + qx = abs(px - h) - (h - r) + qy = abs(py - h) - (h - r) + outside = math.hypot(max(qx, 0.0), max(qy, 0.0)) + min(max(qx, qy), 0.0) - r + mask = 1.0 - smoothstep(-1.0, 0.5, outside) + a = 0.0 + if d < BAND: + a = 1.0 - smoothstep(SOFT, 1.0, d / BAND) + return GLOW_ALPHA * a * mask + + +# ── 素材装载 ── +def load_tiles(): + tiles = {} + for bt in BOARD_TYPES: + for vn, stem in BASE_VARIANTS: + path = os.path.join(TILE_DIR, "%s_%s.png" % (stem, bt)) + if not os.path.exists(path): + print("[阻塞] 素材缺位:%s" % path, file=sys.stderr) + sys.exit(2) + img = Image.open(path).convert("RGB") + if img.size != (CELL, CELL): + img = img.resize((CELL, CELL), Image.NEAREST) + tiles[(bt, vn)] = img.load() + return tiles + + +# ── 带域/格心取样(探针口径)── +def band_pixels(): + """带域像素集(整数格坐标 de < 格边×0.125,type_glow_shot_probe 口径)。""" + return [(x, y) for x in range(CELL) for y in range(CELL) + if min(x, y, CELL - 1 - x, CELL - 1 - y) < BAND] + + +def core_pixels(): + """格心像素集(内缩 20%)。""" + return [(x, y) for x in range(CELL) for y in range(CELL) + if min(x, y, CELL - 1 - x, CELL - 1 - y) >= CELL * 0.20] + + +BAND_PX = band_pixels() +CORE_PX = core_pixels() + + +def mean_rgb(px, pts): + n = len(pts) + return tuple(round(sum(px[x, y][i] for x, y in pts) / n) for i in range(3)) + + +def model_band_mean(base_px, glow_rgb): + """shader 精确合成模型:逐像素 blend_mix 理论带内均值(独立实现路径)。""" + n = len(BAND_PX) + acc = [0.0, 0.0, 0.0] + for x, y in BAND_PX: + w = glow_weight(x + 0.5, y + 0.5) + b = base_px[x, y] + for i in range(3): + acc[i] += w * glow_rgb[i] + (1.0 - w) * b[i] + return tuple(round(v / n) for v in acc) + + +# ── 机检 D:铺底代表值五门 ── +def load_baseline_palette(): + cols = set() + for fn in ["ui_tokens.png", "art_snapshot.png"]: + p = os.path.join(BASELINE_DIR, fn) + img = Image.open(p).convert("RGB") + for x in range(img.width): + for y in range(img.height): + cols.add(img.getpixel((x, y))) + return sorted(cols) + + +def gate_d(tiles): + baseline = load_baseline_palette() + rows = [] + all_pass = True + for bt in BOARD_TYPES: + rgb = H2_VALUES[bt] + h, s = hsv(rgb) + lin = rel_lum(rgb) + l_ok = lin >= 0.30 + s_ok = s <= 0.55 + hue_ok = not (300.0 <= h <= 345.0) + # 近色门:三值=在役素材色(已在基线池内),palette_check 双门②只判新增色——报读数不判红 + near = [b for b in baseline if all(abs(rgb[i] - b[i]) < 8 for i in range(3))] + ok = l_ok and s_ok and hue_ok + all_pass = all_pass and ok + rows.append((bt, rgb, lin, s, h, len(near), ok)) + check("D-" + bt, "铺底代表值门(L/饱和/禁入带;近色只报读数)", ok, + "RGB=%s L=%.3f 饱和=%.3f 色相=%.1f° 近色对=%d(在役素材色,非新增)%s%s%s" + % (rgb, lin, s, h, len(near), + "" if l_ok else " [L红]", "" if s_ok else " [饱和红]", + "" if hue_ok else " [禁入带红]")) + # 语义检查=设计面人工核对(palette_check 同口径),打印读数不判红 + print(" [语义] %s=%s 环境域(不占红/金/蓝/紫赤/绿语义段)——人工核对" % (bt, rgb)) + # 渲染面带域均值 = 读数不判红(方案机检 D 只判槽值;本读数供观感定性参考) + for bt in BOARD_TYPES: + for vn, _ in BASE_VARIANTS: + m = mean_rgb(tiles[(bt, vn)], BAND_PX) + lin = rel_lum(m) + print(" [读数] 渲染面带域均值 %s/%s RGB=%s L=%.3f(不判红)" % (bt, vn, m, lin)) + return all_pass, rows + + +# ── 样图绘制 ── +PAGE_W, PAGE_H = 640, 1120 +MARGIN, HEAD, BLOCK_TITLE, GAP, LEGEND = 10, 60, 20, 30, 80 + + +def draw_sheet(out_path): + import argparse # noqa: F401 + from PIL import ImageDraw, ImageFont + img = Image.new("RGB", (PAGE_W, PAGE_H), (255, 255, 255)) + d = ImageDraw.Draw(img) + try: + f_zh = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 13) + f_zh_s = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 11) + except OSError: + f_zh = ImageFont.load_default() + f_zh_s = f_zh + + blocks = [] + y = MARGIN + HEAD + for bt in BOARD_TYPES: + d.text((MARGIN + 4, y + 3), "%s —— 列0=基准格,列1–5=五组微光(阶梯序)" % bt, fill=(0, 0, 0), font=f_zh) + y += BLOCK_TITLE + blocks.append((bt, y)) + y += 3 * CELL + + tiles = load_tiles() + # 画正图段 54 格 + cell_pngs = {} + for bt, y0 in blocks: + for ri, (vn, _) in enumerate(BASE_VARIANTS): + for ci, gname in enumerate(["BASE"] + [g[0] for g in GLOWS]): + base = tiles[(bt, vn)] + cell = Image.new("RGB", (CELL, CELL)) + cp = cell.load() + if gname == "BASE": + for x in range(CELL): + for yy in range(CELL): + cp[x, yy] = base[x, yy] + else: + glow = dict(GLOWS)[gname] + for x in range(CELL): + for yy in range(CELL): + w = glow_weight(x + 0.5, yy + 0.5) + b = base[x, yy] + cp[x, yy] = tuple(round(w * glow[i] + (1 - w) * b[i]) for i in range(3)) + gx = MARGIN + 170 + ci * CELL + gy = y0 + ri * CELL + img.paste(cell, (gx, gy)) + cell_pngs[(bt, vn, gname, "main")] = (gx, gy) + # 行标注 + for ri, (vn, _) in enumerate(BASE_VARIANTS): + d.text((MARGIN, y0 + ri * CELL + 16), vn, fill=(0, 0, 0), font=f_zh_s) + + # 灰度副本段(等幅面,正图段整段 Rec.709 去色) + main_top = MARGIN + HEAD + main_bot = y + gray_top = main_bot + GAP + for xx in range(MARGIN, PAGE_W - MARGIN): + for yy in range(main_top, main_bot): + r, g, b = img.getpixel((xx, yy)) + v = int(round(gray709((r, g, b)))) + img.putpixel((xx, gray_top + (yy - main_top)), (v, v, v)) + d.text((MARGIN + 4, gray_top - 2), "灰度副本段(Rec.709 去色——色弱通道目检面)", fill=(80, 80, 80), font=f_zh_s) + + # 图例段 + leg_y = gray_top + (main_bot - main_top) + 10 + d.text((MARGIN + 4, leg_y), "图例(五组冻结色值 ui_kit.gd TG_GLOW_*;阶梯序=合成空间亮度递减)", fill=(0, 0, 0), font=f_zh_s) + for i, (gn, rgb) in enumerate(GLOWS): + x0 = MARGIN + 170 + i * (CELL + 4) + d.rectangle([x0, leg_y + 18, x0 + CELL, leg_y + 18 + 28], fill=rgb, outline=(0, 0, 0)) + d.text((x0, leg_y + 48), gn, fill=(0, 0, 0), font=f_zh_s) + + img.save(out_path) + return img, cell_pngs, tiles, blocks, gray_top, main_top, main_bot + + +# ── 机检 A/B/C + 灰度/净空机检 ── +def run_checks(img, cell_pngs, tiles, blocks, gray_top, main_top, main_bot): + sheet_px = img.load() + table1 = [] # 45 行逐对表 + table2 = [] # 90 行组间表 + band_means = {} + fail_b, fail_c = [], [] + + for bt, y0 in blocks: + for vn, _ in BASE_VARIANTS: + # 列 0 基准格带域均值 = 该底型的模型底值 + gx0, gy0 = cell_pngs[(bt, vn, "BASE", "main")] + band_base = mean_rgb(tiles[(bt, vn)], BAND_PX) + for gi, (gname, glow) in enumerate(GLOWS): + gx, gy = cell_pngs[(bt, vn, gname, "main")] + px = tiles[(bt, vn)] + # 样图带内均值(样图渲染面读数) + n = len(BAND_PX) + acc = [0.0, 0.0, 0.0] + for x, yy in BAND_PX: + p = sheet_px[gx + x, gy + yy] + for i in range(3): + acc[i] += p[i] + band_mean = tuple(round(v / n) for v in acc) + core_mean = mean_rgb(px, CORE_PX) + model = model_band_mean(px, glow) + diff_model = max(abs(band_mean[i] - model[i]) for i in range(3)) + ch_diff = [abs(band_mean[i] - core_mean[i]) for i in range(3)] + b_ok = max(ch_diff) >= 8 + c_ok = diff_model <= 2 + if not b_ok: + fail_b.append("%s/%s/%s" % (bt, vn, gname)) + if not c_ok: + fail_c.append("%s/%s/%s" % (bt, vn, gname)) + hue_gap = abs(hsv(H2_VALUES[bt])[0] - hsv(glow)[0]) + hue_gap = min(hue_gap, 360 - hue_gap) + table1.append((bt, vn, gname, band_mean, core_mean, model, + diff_model, ch_diff, hue_gap, b_ok and c_ok)) + band_means[(bt, vn, gname)] = band_mean + + # 机检 A:组间表 90 行 + for bt, y0 in blocks: + for vn, _ in BASE_VARIANTS: + for i in range(len(GLOWS)): + for j in range(i + 1, len(GLOWS)): + a, b = band_means[(bt, vn, GLOWS[i][0])], band_means[(bt, vn, GLOWS[j][0])] + cr = wcag(a, b) + ok = cr >= 1.20 + table2.append((bt, vn, "%s/%s" % (GLOWS[i][0], GLOWS[j][0]), + cr, gray709(a), gray709(b), ok)) + stair_ok = True + for bt, y0 in blocks: + for vn, _ in BASE_VARIANTS: + seq = [gray709(band_means[(bt, vn, GLOWS[k][0])]) for k in range(len(GLOWS))] + mono = all(seq[k] > seq[k + 1] for k in range(len(seq) - 1)) + stair_ok = stair_ok and mono + check("A-stair-%s-%s" % (bt, vn[:4]), "明度阶梯单调递减", mono, + "→".join("%.3f" % v for v in seq)) + min_cr = min(t[3] for t in table2) + worst = min(table2, key=lambda t: t[3]) + check("A-CR", "组间合成对比最弱对 ≥1.20(90 对)", min_cr >= 1.20, + "最弱 %s/%s CR=%.3f" % (worst[0], worst[2], min_cr)) + check("B", "格底-微光可辨底线(45 行三通道差不全<8)", len(fail_b) == 0, + "违例 %d 行:%s" % (len(fail_b), ", ".join(fail_b[:6]) or "无")) + check("C", "绘制参数校准(45 行模型差≤±2/255)", len(fail_c) == 0, + "违例 %d 行:%s" % (len(fail_c), ", ".join(fail_c[:6]) or "无")) + + # 灰度段零彩色像素 + color_hit = 0 + for xx in range(MARGIN, PAGE_W - MARGIN): + for yy in range(gray_top + BLOCK_TITLE, gray_top + (main_bot - main_top)): + r, g, b = sheet_px[xx, yy] + if not (r == g == b): + color_hit += 1 + check("GRAY", "灰度副本段零彩色像素", color_hit == 0, "命中 %d" % color_hit) + + # 四缘净空(A5 同款) + edge_hit = 0 + for xx in range(PAGE_W): + for yy in range(PAGE_H): + if xx < MARGIN or xx >= PAGE_W - MARGIN or yy < MARGIN or yy >= PAGE_H - MARGIN: + if sheet_px[xx, yy] != (255, 255, 255): + edge_hit += 1 + check("EDGE", "四缘 10px 带内零内容像素", edge_hit == 0, "命中 %d" % edge_hit) + return table1, table2 + + +def write_report(table1, table2, d_rows): + rep = os.path.join(REPO, "reports", "读数-三关色相互辨抽测-2026-11-05.md") + lines = [ + "# 读数:三关色相 × 五组微光互辨抽测(2026-11-05)", + "", + "> 接收方:待办清单「三关色相 × 五组微光互辨抽测——三关图类型色板终值冻结的前置抽测」条。", + "> 执行依据:reports/提案-三关色相互辨抽测方案-2026-10-29.md(S1/S2 步)。", + "> 样图:artwork/mockups/glow-discrimination-sheet-2026-11-05.png(不入 git)。", + "", + "## 机检汇总", + "", + "| 项 | 判定 | 说明 |", + "|---|---|---|", + ] + for cid, name, ok, detail in CHECKS: + lines.append("| %s | %s | %s:%s |" % (cid, "PASS" if ok else "FAIL", name, detail)) + lines += ["", "## S1 机检 D:铺底代表值(H2 槽)五门", "", + "| 关 | RGB | L(线性域) | 饱和 | 色相 | 近色对 | 判定 |", "|---|---|---|---|---|---|---|"] + for bt, rgb, lin, s, h, near, ok in d_rows: + lines.append("| %s | %s | %.3f | %.3f | %.1f° | %d | %s |" % (bt, rgb, lin, s, h, near, "过" if ok else "红")) + lines += ["", "## 表一 逐对表(45 行)", "", + "| 关 | 底型 | 微光组 | 带内合成均值 | 格心素底均值 | 模型带内均值 | 模型差 | 三通道差 | 色相差° | 判定 |", + "|---|---|---|---|---|---|---|---|---|---|"] + for bt, vn, gn, bm, cm, md, dm, cd, hg, ok in table1: + lines.append("| %s | %s | %s | %s | %s | %s | %d | %s | %.0f | %s |" + % (bt, vn, gn, bm, cm, md, dm, cd, hg, "过" if ok else "红")) + lines += ["", "## 表二 组间表(90 行)", "", + "| 关 | 底型 | 组对 | 合成对比 | 灰度亮度a | 灰度亮度b | 判定 |", "|---|---|---|---|---|---|---|"] + for bt, vn, pair, cr, ga, gb, ok in table2: + lines.append("| %s | %s | %s | %.3f | %.3f | %.3f | %s |" % (bt, vn, pair, cr, ga, gb, "过" if ok else "红")) + n_fail = sum(1 for c in CHECKS if not c[2]) + lines += ["", "---", "", "机检 %d 项,FAIL %d 项。" % (len(CHECKS), n_fail), + "", "目检(S3)归用户:四条判据见方案 §2.2(逐对指认 45/45/灰度副本 45/45/相邻并列 36 对可辨/装饰不抢读)。", ""] + with open(rep, "w", encoding="utf-8") as f: + f.write("\n".join(lines)) + print("[报告] %s" % rep) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--art", default=os.path.join(REPO, "artwork", "mockups")) + ap.add_argument("--no-sheet", action="store_true") + args = ap.parse_args() + tiles = load_tiles() + print("== S1 机检 D:三关铺底代表值 ==") + d_ok, d_rows = gate_d(tiles) + if args.no_sheet: + return 0 if d_ok else 1 + os.makedirs(args.art, exist_ok=True) + out = os.path.join(args.art, "glow-discrimination-sheet-2026-11-05.png") + print("== S2 出并列样图 ==") + img, cell_pngs, tiles2, blocks, gray_top, main_top, main_bot = draw_sheet(out) + print("[样图] %s" % out) + t1, t2 = run_checks(img, cell_pngs, tiles2, blocks, gray_top, main_top, main_bot) + write_report(t1, t2, d_rows) + n_fail = sum(1 for c in CHECKS if not c[2]) + print("== 抽测机检完成:%d 项,FAIL %d ==" % (len(CHECKS), n_fail)) + return 0 if n_fail == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/golden_baseline_refresh.sh b/tools/golden_baseline_refresh.sh index 422d5a4f..7aba1e93 100755 --- a/tools/golden_baseline_refresh.sh +++ b/tools/golden_baseline_refresh.sh @@ -5,6 +5,8 @@ # 缺位时先拷主区素材+导入边车再跑)。 # 模式: # --menu:shot_probe 启动态菜单整屏帧 → game/tests/golden/menu_boot.png。 +# --lmap:level_map_shot_probe 关卡视口两 Shot(入口停点+Boss 列锚定——2026-11-05 用户裁 +# 两 Shot 面)→ game/tests/golden//lmap_entry.png + lmap_boss.png。 # 全屏基线可行性前置已验证:菜单连续帧差 0.0000%/max_diff=0(光柱为 # 程序生成静态贴片、零动画着色器,全屏基线不抖动);采集确定性=探针隔离档(无断点+出厂 # 设置 1280×800),与 render_gate 的 shot_menu 对比段同源同帧。 @@ -21,6 +23,7 @@ MODE="board" for a in "$@"; do case "$a" in --menu) MODE="menu" ;; + --lmap) MODE="lmap" ;; *) if [ -z "$GAME" ]; then GAME="$a"; fi ;; esac done @@ -53,6 +56,34 @@ if [ "$MODE" = "menu" ]; then exit 0 fi +if [ "$MODE" = "lmap" ]; then + # 关卡视口两 Shot 面(卷轴视口批·2026-11-05 用户裁:只对拍关卡最开始与关卡 Boss 节点, + # 中间过程不对拍)——入口停点 + 末列 Boss 列锚定,两基线各存平台子目录。 + for a in assets/fonts/ui_font_fusion_pixel_zh_hans.ttf \ + assets/art/ui/button_stone.png \ + assets/art/ui/node_battle.png assets/art/ui/node_reward.png \ + assets/art/ui/node_rest.png assets/art/ui/node_boss.png \ + assets/art/ui/node_shop.png; do + if [ ! -f "$GAME/$a" ]; then + echo "[golden_refresh] 拒跑 — 关卡屏素材缺位: ${a}(S3 停机点口径:基线禁锚字形回退态,先补素材+导入边车)" + exit 1 + fi + done + gate_run_timeout 420 "$TMP/run.log" "$G" --path "$GAME_WIN" --quit-after 3000 res://probe/level_map_shot_probe.tscn -- "$TMP/shots" + st=$? + grep -E "\[level_map_shot_probe\]" "$TMP/run.log" | tail -3 + # 判定锚工件(同 menu——quit-after 与 quit(0) 竞速的 124 非真红):PASS 签名 + 双图非零。 + if ! grep -q "PASS — 两 Shot 落盘" "$TMP/run.log" || [ ! -s "$TMP/shots/lmap_entry.png" ] || [ ! -s "$TMP/shots/lmap_boss.png" ]; then + echo "[golden_refresh] FAIL — 关卡视口截图缺失或探针自检红(st=$st,见上)" + exit 1 + fi + mkdir -p "$REPO/game/tests/golden/$PLAT" + cp "$TMP/shots/lmap_entry.png" "$REPO/game/tests/golden/$PLAT/lmap_entry.png" || exit 1 + cp "$TMP/shots/lmap_boss.png" "$REPO/game/tests/golden/$PLAT/lmap_boss.png" || exit 1 + echo "[golden_refresh] 完成 — 关卡视口两 Shot 基线已代入 game/tests/golden/$PLAT/(提交说明须写明触发原因)" + exit 0 +fi + for a in assets/fonts/ui_font_fusion_pixel_zh_hans.ttf \ assets/art/board/seal_mark.png \ assets/art/character/character_hero_idle.png \ diff --git a/tools/probe_gate.sh b/tools/probe_gate.sh index d47b7af8..b54848ae 100755 --- a/tools/probe_gate.sh +++ b/tools/probe_gate.sh @@ -134,6 +134,12 @@ st=$? grep -E "PASS|FAIL|问题" "$TMP/uip.log" | head -12 verdict "ui_input_probe" "$TMP/uip.log" "$st" || _rc=1 +echo "======== level_map_viewport_probe(卷轴视口批 R1-R8) ========" +run_suite 120 "$TMP/lvp.log" res://probe/level_map_viewport_probe.tscn +st=$? +grep -E "PASS|FAIL" "$TMP/lvp.log" | head -12 +verdict "level_map_viewport_probe" "$TMP/lvp.log" "$st" || _rc=1 + echo "======== ui_struct_probe ========" run_suite 120 "$TMP/stp.log" res://probe/ui_struct_probe.tscn st=$? diff --git a/tools/render_gate.sh b/tools/render_gate.sh index 8e1a7b8a..d8bfb16f 100755 --- a/tools/render_gate.sh +++ b/tools/render_gate.sh @@ -165,6 +165,38 @@ else fi fi + +echo "======== 关卡视口 golden 逐像素(卷轴视口批——两 Shot 面:入口停点+Boss 列锚定)========" +LM="$TMP/lmapshots" +gate_run_timeout 360 "$TMP/lm.log" "$G" --path "$GAME_WIN" --quit-after 3000 res://probe/level_map_shot_probe.tscn -- "$LM" +st=$?; kill_orphans +grep -E "\[level_map_shot_probe\]" "$TMP/lm.log" | tail -3 +# 判定锚工件(同 fx/board——quit-after 竞速 124 非真红):PASS 签名 + 双图非零。 +if ! grep -q "PASS — 两 Shot 落盘" "$TMP/lm.log"; then + echo "[render_gate] FAIL — level_map_shot_probe 未干净完成(exit=${st})"; _rc=1 +fi +for b in lmap_entry.png lmap_boss.png; do + if [ ! -s "$LM/$b" ]; then echo "[render_gate] FAIL — 取证缺失/零字节: $b"; _rc=1; fi +done +scan_log "level_map_shot_probe" "$TMP/lm.log" +if [ "$_rc" -eq 0 ]; then + for b in lmap_entry.png lmap_boss.png; do + if [ ! -s "$GOLDEN/$b" ]; then + # 两 Shot 面(2026-11-05 用户裁)各平台独立冻结——本平台缺基线=未冻结态记 SKIP 不判红 + #(windows 先锚;他平台冻结义务归收尾呈报),已锚平台照常逐像素严判 + echo "[render_gate] SKIP — $GOLDEN/$b 基线缺失(本平台未冻结——跑 tools/golden_baseline_refresh.sh --lmap)" + else + LMG="$TMP/lmgolden" + mkdir -p "$LMG" + cp "$LM/$b" "$LMG/$b" + python3 tools/golden_compare.py "$REPO/game/tests/golden" "$LMG" "$b" | sed 's/^/[render_gate] /' + if [ "${PIPESTATUS[0]}" -ne 0 ]; then + echo "[render_gate] FAIL — 关卡视口 golden 超容差(S2 停机点口径:视口呈现换代=显式重制基线+呈报;超预期面=L3)"; _rc=1 + fi + fi + done +fi + echo "======== 残留检查(批末闸) ========" left="$(orphans | wc -l | tr -d ' ')" total="$(gate_godot_count_all)" diff --git a/体验验证清单.md b/体验验证清单.md index b36f05a3..d066d596 100644 --- a/体验验证清单.md +++ b/体验验证清单.md @@ -508,3 +508,14 @@ | 1 | 三关封印格观感 | 环+十字+环外压暗光圈呈形正确、与格底可辨;像素判据已在机检承载,本项为观感复核 | | 2 | 战末金蜡封徽记观感 | 贴图路径未动(零预期变化)——观感确认项 | | 3 | 封印格与变异纹带同格观感 | 两层共存可辨、不脏面(零代码触面——观感确认项) | +## 52 卷轴视口与结算文案换代目检(迭代 53) + +> 关卡画面视口化(卷轴视口)与结算文案换代(减半标签)后,机检已覆盖行为与像素面(探针 26 断言+golden 两 Shot 逐像素+明细行等值断言),观感面按登记不阻塞立规不拦合并。 + +| # | 目检项 | 判据 | +|---|---|---| +| 1 | 深图卷轴视口实跑观感 | 深度 13+ 图拖拽平滑可达首末列;停点推进跟随方向感自然、当前节点居中锚即时;拖拽不误触选路 | +| 2 | 指引件观感 | 深图两侧渐隐带与顶部平移提示 chip 可辨、不碍选路;浅图两件均不出现 | +| 3 | 结算明细减半标签 | 「挖掘惩罚减半(6→3)」一眼可读,无退役件名突兀感 | + +- **结论回流**:观感问题归待办清单登记新条目;数值面不在目检现场改数。 diff --git a/待办清单.md b/待办清单.md index 4e6d90fb..d12e6d62 100644 --- a/待办清单.md +++ b/待办清单.md @@ -20,14 +20,15 @@ **状态**:待文档设计 -#### 超预算图策略升级路径——等比缩放与滚动视口两案待裁(2026-10-24 登记,来源=批次移交) +#### 结算明细文案入表——玩家可见明细文案归属改数据表承载(2026-11-06 登记,来源=批次移交) -**实况**:2026-11-05 用户裁滚动视口案落地(等比缩放案否决——像素纪律):关卡画面卷轴视口,跟随+拖拽+滚轮、平滑滚动(减动效瞬切)、当前节点居中锚;「图包络 ≤ 屏预算」约束退役,画布随图生长、规模归参数档。设计闭合凭据:design/gdd/ux/level-map.md v2.0(布局决策 5+视口效果图内嵌,2026-11-05 人审通过);design/gdd/systems/冒险循环/关卡节点图生成.md v1.5(参数契约节+图规模无呈现侧上限句)。需求卡片=reports/需求卡片-卷轴视口批-2026-11-05.md(grilling 复核裁决全落,验收档中)。 -- 相关文档:关卡节点图参数取值域已归数据 schema 层;schema 头注「域并单点=调参表」指针反转随实现批顺带(卡片随动项);需求卡片 reports/需求卡片-卷轴视口批-2026-11-05.md(grilling 复核裁决全落,验收档中)。 +**实况**:迭代 53 反哺点 4 归属裁决在案=文案入数据表行(2026-11-06 用户裁);规范句已落 [战斗结算](design/gdd/systems/战斗循环/战斗结算.md) v1.8 明细流水节(显示名等玩家可见文案=数据面字段,代码读表拼行、禁代码写死文案字串);现行代码硬编码面=结算明细扩展行标签文案(如减半标签,reward_grant.gd:226 一族)。 -**改动面**:文档 +**缺口**:还差表结构设计讨论——承载表与字段形态、基础行文案是否同批入表、读表拼行接线面。 -**状态**:文档设计已闭合+已写卡+已编迭代 53 +**改动面**:文档+游戏代码 + +**状态**:待文档设计 #### UI 贴图重构批——元进度件仍是程序化占位(2026-10-13 登记,来源=审计发现) @@ -234,21 +235,13 @@ ### 文档与结构 -#### damage_controller 按序改写族——内容孤儿机制与退役名注释层(2026-11-04 登记,来源=会话提及) - -**实况**:damage_controller.gd 按序改写族通用机制(trade_hit/trade_atk 交换、armor_rewrite 免掷、spike 满层必中与倍乘、discount_shift 轻甲改善——注释叙事 :285-290、改写执行 :291-325、增伤与倍乘 :350-353 与 :368-372)现役内容面仅淬毒刃纹一行 modify_ticket(mode=clamp_bonus,feats.tres:853-857):其余子分支零表行注册(全仓 grep 证)——「框架先行、内容未接」同族债。五拍注释全以旧 27 专长名叙事(矿工重锤/专注/厚积薄发/猎弱本能,道具退役素材库.md:43-47 退役在案);charge_layers 四处已归 EffectPort 蓄力层删键批(实现提交 ace15701)。2026-11-05 会话裁决=孤儿机制与退役名一律清退(口径「只要有问题的都要改」);同轮核出 spike 的 GDD 承接句站不住(候选条写「零新代码——spike 在册」,实为词面在册、全仓零派发点;长枪条同类——attack_segments 已随架构审查甲案退役),据此订正该节四处陈述。全仓扫=40 退役名命中 53 行/13 文件(注释面 42、字面量 11),逐处清单见卡片。关联文档:设计定稿=design/gdd/systems/跨局循环/锻造工坊.md v0.13(2026-11-05);需求卡片=reports/需求卡片-改写族孤儿机制与退役名清退批-2026-11-05.md - -**改动面**:游戏代码 - -**状态**:文档设计已闭合+已写卡+已编迭代 53 - #### 效果通道与事件词的零现役普查(2026-11-05 登记,来源=审计发现) -**实况**:证据=四条 grep 读数:`cell_removal` 通道在册(`game/data/schemas/treasure_item_table.gd:26`;消费点 `game/core/reward_grant.gd:160-161`)而 `game/data/` 全域零数据行;`mine_hit_halve` 同形态(仅 schema 词表与注记在位);`open_ticket` 的 `channel="reflect"` 行族零表行(21 张 `.tres` 逐张 grep);`armor_roll` 在 `game/core/effect_engine.gd:13` 事件词表在册、发射点 `game/core/damage_controller.gd:296`,而 `game/data/tables/feats.tres` 零订阅行。形态与架构审查甲案已清的 overflow/attack_segments 死通道同族(判据出处=`design/engineering/code-conventions.md:15`、`design/engineering/test-conventions.md:116`)。本批(需求卡片-改写族孤儿机制与退役名清退批)只清其文案与归因串,通道与事件词本体去留待普查批整体裁。 +**实况**:普查报告 reports/普查-效果通道与事件词零现役-2026-11-06.md——结论=内容层零孤儿、四词表+秘宝表 schema 旧词层全部属预留埋点(词面全保留零清退项;架构原则=词表先行内容后接)。叙述脱节 5 处已按「预留、未设计入表」口径订正(2026-11-06 用户令直改,冒烟 15 项 0 失败三闸全过)。唯一遗留待裁=秘宝表 schema 旧词面 9 个「迁移完成」收尾(做不做、何时做),待用户裁。 **改动面**:游戏代码 -**状态**:待写卡 +**状态**:待执行 #### 测试面真输入化分期与存量断言逐条精锚(2026-11-05 登记,来源=批次移交) @@ -260,8 +253,24 @@ #### QA 覆盖总览面板与自动试玩扩展——自动发现测试验证缺漏(2026-11-05 登记,来源=会话提及) -**实况**:方案全文=`D:\code\qa-harness-discussion\QA-Harness讨论结果-中间报告.md`(三批定案,全部三关试点):①探针存量 emit 真输入化(~40 处站点,承接上条)②B 覆盖总览面板+本体枚举器(六源回执明细化+能力清单提取+HTML 双击即开+游戏内容↔测试验证对账)③C 自动试玩扩展(auto_player 扩全动线全功能,异常上报含机器自填归因)。裁决在案勿重提:A 执行覆盖矩阵不做、D 缺口闸不做、变异测试禁入、归因登记不立独立制度。 +**实况**:方案全文=`D:\code\qa-harness-discussion\QA-Harness讨论结果-中间报告.md`(三批定案,全部三关试点);证据=真实入口缺口在册:能力行「营地七物件命中区点选与分界面可达」残险行 game/tests/README.md:158(emit 直发标「接线验证」、真输入承载待建)+test-conventions §8.3 真实入口判定口径条款(2026-11-05 收窄)。①探针存量 emit 真输入化(~40 处站点,承接上条)②B 覆盖总览面板+本体枚举器(六源回执明细化+能力清单提取+HTML 双击即开+游戏内容↔测试验证对账)③C 自动试玩扩展(auto_player 扩全动线全功能,异常上报含机器自填归因)。裁决在案勿重提:A 执行覆盖矩阵不做、D 缺口闸不做、变异测试禁入、归因登记不立独立制度。 **改动面**:游戏代码+工具代码+文档 **状态**:待执行 + +#### 闸机超时守卫缺陷修复——gate_run_timeout 标志文件恒真(2026-11-06 登记,来源=批次移交) + +**实况**:tools/gate_platform.sh 的 gate_run_timeout 用 mktemp 预建超时标志文件,判据 `[ -f ]` 建时即真、超时态无法与正常完成区分(Windows Git Bash 实测直接调用 4 秒返回 124;远端各闸以工件锚定未受影响)。连带 tools/golden_baseline_refresh.sh 的 guard 子Shell sleep 420 未被 kill 短路(重采集等待 7 分钟症状)。修法=kf 先赋路径+rm -f 后置守卫(probe_gate.sh:97 run_suite 同款正确写法)。证据=tools/gate_platform.sh:37/:48+迭代53实现报告遗留呈报 1(git 历史可考)。 + +**改动面**:工具代码 + +**状态**:待执行 + +#### 关卡视口两 Shot 基线 mac 侧冻结(2026-11-06 登记,来源=批次移交) + +**实况**:golden/windows/ 已锚 lmap_entry/lmap_boss(提交 6394abf7);mac 子目录无该两 Shot——渲染闸对 mac 缺基线记 SKIP 不判红,两平台同盯缺一。义务=Mac 侧跑 tools/golden_baseline_refresh.sh --lmap 冻结 mac 基线并呈报确定性读数。证据=game/tests/golden/README.md 现持清单行与时效判定纪律行+迭代53实现报告遗留 2(git 历史可考)。 + +**改动面**:游戏代码 + +**状态**:待执行