Merge branch 'master' into qa-harness

# Conflicts:
#	game/probe/probe_ui_util.gd
This commit is contained in:
DKChan committed 2026-11-06 01:52:17 +08:00
commit 886368684c
49 files changed
+1777 -584

No files matched your search

+1 -1
View File
@@ -79,7 +79,7 @@
### 8.2 四层与真实入口
- **单元**:不依赖场景树和真实宿主的纯逻辑;测试从领域类的对外方法进出,断言钉对外可观测面(返回值/信号/查询接口/落盘内容)。内部字段直查=受控例外:仅当该行为没有公开可观测面可钉时允许,对照表该行注明理由;存量不回头清扫,被批次触碰时顺带换代。
- **单元**:不依赖场景树和真实宿主的纯逻辑;测试从领域类的对外方法进出,断言钉对外可观测面(返回值/信号/查询接口/落盘内容)。内部字段直查=受控例外:仅当该行为没有公开可观测面可钉时允许,对照表该行注明理由;存量不回头清扫,被批次触碰时顺带换代。探针直读视口承载子件结构(画布取视口首子件、渐隐带两片与平移提示子件按名取)属此类——断言对象即承载结构本身,无公开可观测面可钉,理由随对照表行申报。
- **接口**:模块边界的公开契约——数据表加载、存档与断点、事件载荷、命令接口;测试从公开入口进出。接口级断言随单元套件落(`unit/contracts/`),不另设承载目录。
- **集成**:跨宿主的接线、持久、生命周期——例如 LevelRunner 把 level_counters 传给 BattleOrchestrator、两场战斗共享同一个桶、断点续玩;测试必须实例化真实宿主链,禁止用裸 EffectEngine 代替。
- **探针/golden**:玩家可达性与观感;真实实例化场景、驱动真实输入。
-6
View File
@@ -47,12 +47,6 @@
| 专名 | 是什么 | 出处条目 |
|---|---|---|
| 显影尘 | 信息类秘宝 | aux_items |
| 工兵令 | 加行动点的秘宝 | aux_items |
| 探宝罗盘 | 预读奖励身份的秘宝 | aux_items |
| 探巢摆钟 | 探知巢穴位置的秘宝 | aux_items |
| 地穴全图卷 | 全图级稀有秘宝 | aux_items |
| 爆破工兵包 | 行动点 + 探巢的稀有秘宝 | aux_items |
| 地精 / 熊地精 / 食人魔酋长 | 首发三敌种(末者为 Boss) | enemy |
| 第一层 · 地精洞窟 | 首关层名 | level_table |
| 封印 | 正式动作名(全库统一,非比喻、非呈现层别名)——对未开格实施封印,印记=封印印记 | terminology |
+1 -1
View File
@@ -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 旧六名扫描零命中)。
## 增益与状态
@@ -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=传说;任何概率提升禁止改变本档保底承诺(保底=承诺非概率)。
@@ -1,7 +1,7 @@
# 战斗结算
> 一句话:轮末结算的编排与逐行兑现
> 状态:已定稿;现行为 v1.7
> 状态:已定稿;现行为 v1.8
## 设计目的
@@ -84,7 +84,7 @@
| 判定行 | 胜 / 负 / 继续各一行;终局判定行同时是胜负画面的数据锚 |
| 高光行 | 完美封印成立时一行,只记实际有效兑付 |
每行还携带公共字段:来源类别、来源格、轮次、显示名、数值,以及可空的高光标记(用于强调封印标中、清图奖励这类值得高亮的结果)。奖励直伤行的数值字段沿用 `effectiveness` 键名、值为整数点数——键名与量纲是数据导出与分析工具的兼容契约,改动须连动分析工具面。没生效也要留痕:两类"未生效"行如实记录合法例外,呈现时聚合计数,不逐行刷屏。
每行还携带公共字段:来源类别、来源格、轮次、显示名、数值,以及可空的高光标记(用于强调封印标中、清图奖励这类值得高亮的结果)。**显示名等玩家可见文案是数据面字段:文案值登记在数据表行,代码按来源类别与效果身份读表拼行,不在代码里写死文案字串**(2026-11-06 用户裁——文案归属数据表承载;承载表与字段形态归表结构设计段定,设计闭合后由实装批落表)。奖励直伤行的数值字段沿用 `effectiveness` 键名、值为整数点数——键名与量纲是数据导出与分析工具的兼容契约,改动须连动分析工具面。没生效也要留痕:两类"未生效"行如实记录合法例外,呈现时聚合计数,不逐行刷屏。
任何一行找不到来源即断言失败——宁可返回错误,也不让"合并到看不出来源"的呈现混过数据层。但有些行天然没有棋盘来源,断言为此留了两层豁免,豁免集合是封闭清单,改动同样走本文修订。
+2 -2
View File
@@ -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` 首例;有效误标=误标总数 − 豁免数,豁免不改写误标账 |
+1 -1
View File
@@ -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:
+2 -2
View File
@@ -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
+19 -66
View File
@@ -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 敌的账)
+6 -6
View File
@@ -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、
+6 -6
View File
@@ -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)
+4 -4
View File
@@ -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:
+11 -11
View File
@@ -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"):
+3 -2
View File
@@ -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]})
+2 -2
View File
@@ -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"):
+122 -20
View File
@@ -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)
@@ -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
@@ -0,0 +1 @@
uid://dgin6f6214cbg
+91
View File
@@ -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 -- <shots_dir>
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)
+1
View File
@@ -0,0 +1 @@
uid://j4ax48si4nq5
+6
View File
@@ -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")
+389
View File
@@ -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)
@@ -0,0 +1 @@
uid://cprbakccv06re
+6
View File
@@ -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")
+42
View File
@@ -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://<tag>_slot_1.json)。返回 false=新档钮缺失(驱动中断)。
+16
View File
@@ -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():
+1 -1
View File
@@ -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
+2 -4
View File
@@ -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":
+1 -1
View File
@@ -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`(写口尾刷新段) |
+8 -4
View File
@@ -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% 阈值):基线**留在位**,不得顺手重制;
Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

@@ -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)
@@ -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 携带)",
@@ -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)
@@ -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)
@@ -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()
@@ -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 走独立系统,不入本普查面。
@@ -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 对可辨/装饰不抢读)。
@@ -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<N>-实现报告-<日期>.md`(各卡状态+实机目检总清单+实现决定记录)+ `reports/iter<N>-反哺点-<日期>.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<N>-验收审计-<日期>.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 用户已裁决;验收档「中」随批卡确认)。
@@ -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 条)。
+3 -3
View File
@@ -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<br>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/ 现存路径或本保留字,不填「—」。
+12 -2
View File
@@ -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())
+444
View File
@@ -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 <artwork/mockups 目录> [--no-sheet]
# 输出:
# <art>/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())
+31
View File
@@ -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/<plat>/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 \
+6
View File
@@ -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=$?
+32
View File
@@ -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)"
+11
View File
@@ -508,3 +508,14 @@
| 1 | 三关封印格观感 | 环+十字+环外压暗光圈呈形正确、与格底可辨;像素判据已在机检承载,本项为观感复核 |
| 2 | 战末金蜡封徽记观感 | 贴图路径未动(零预期变化)——观感确认项 |
| 3 | 封印格与变异纹带同格观感 | 两层共存可辨、不脏面(零代码触面——观感确认项) |
## 52 卷轴视口与结算文案换代目检(迭代 53)
> 关卡画面视口化(卷轴视口)与结算文案换代(减半标签)后,机检已覆盖行为与像素面(探针 26 断言+golden 两 Shot 逐像素+明细行等值断言),观感面按登记不阻塞立规不拦合并。
| # | 目检项 | 判据 |
|---|---|---|
| 1 | 深图卷轴视口实跑观感 | 深度 13+ 图拖拽平滑可达首末列;停点推进跟随方向感自然、当前节点居中锚即时;拖拽不误触选路 |
| 2 | 指引件观感 | 深图两侧渐隐带与顶部平移提示 chip 可辨、不碍选路;浅图两件均不出现 |
| 3 | 结算明细减半标签 | 「挖掘惩罚减半(6→3)」一眼可读,无退役件名突兀感 |
- **结论回流**:观感问题归待办清单登记新条目;数值面不在目检现场改数。
+25 -16
View File
@@ -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 历史可考)。
**改动面**:游戏代码
**状态**:待执行