From 11beb4fc59872bb7638798969c0bbc0a2c3c0007 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 2 Nov 2026 07:30:33 +0800 Subject: [PATCH 1/4] =?UTF-8?q?iter49-=E5=8D=A11=EF=BC=9A=E6=95=88?= =?UTF-8?q?=E6=9E=9C=E7=AB=AF=E5=8F=A3=E4=B8=8E=E6=8E=B7=E6=A0=BC=E5=88=86?= =?UTF-8?q?=E4=BB=B6=E6=89=B9(=E8=BD=B4=E6=88=8A=E4=B8=A4=E4=BB=B6?= =?UTF-8?q?=E5=BB=BA=E4=BB=B6):EffectPort=E5=85=ADCallable=E8=B7=AF?= =?UTF-8?q?=E7=94=B1+RewardRoller=E6=8E=B7=E6=A0=BC=E9=9D=A2=E8=BF=81?= =?UTF-8?q?=E5=87=BA+=E4=B8=A4=E4=B8=9A=E5=8A=A1=E7=B1=BB=E9=9B=B6?= =?UTF-8?q?=E5=BC=95=E6=93=8E=E7=9B=B4=E8=BF=9E+effect=5Fport=5Ftest=20V1-?= =?UTF-8?q?V3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/engineering/test-conventions.md | 2 +- design/gdd/systems/跨局循环/锻造工坊.md | 16 +- game/core/action_economy.gd | 30 +-- game/core/battle_fx_host.gd | 57 +++- game/core/battle_orchestrator.gd | 15 +- game/core/board_installer.gd | 4 +- game/core/damage_controller.gd | 77 ++---- game/core/effect_port.gd | 71 +++++ game/core/effect_port.gd.uid | 1 + game/core/reward_economy.gd | 244 ++--------------- game/core/reward_roller.gd | 245 ++++++++++++++++++ game/core/reward_roller.gd.uid | 1 + game/core/settlement/combat_segment.gd | 9 +- game/presentation/main.gd | 10 + game/presentation/views/ui_kit.gd | 2 +- .../golden/palette_baseline/art_snapshot.png | Bin 2572 -> 0 bytes .../golden/palette_baseline/ui_tokens.png | Bin 337 -> 0 bytes game/tests/run_all.gd | 1 + game/tests/static_scan.sh | 6 +- .../battle_settlement/effect_port_test.gd | 129 +++++++++ .../battle_settlement/effect_port_test.gd.uid | 1 + .../variant_placement_test.gd | 36 +-- reports/工程改动报告-工程清理批-2026-11-02.md | 75 ------ tools/capability_audit.py | 1 + tools/extract_checks.py | 43 ++- tools/gen_palette_baseline.py | 105 -------- tools/palette_check.py | 13 +- tools/path_check.py | 2 +- tools/render_sample_sheet_v3.py | 12 - tools/run_all_gates.sh | 8 - 待办清单.md | 67 ++++- 31 files changed, 697 insertions(+), 586 deletions(-) create mode 100644 game/core/effect_port.gd create mode 100644 game/core/effect_port.gd.uid create mode 100644 game/core/reward_roller.gd create mode 100644 game/core/reward_roller.gd.uid delete mode 100644 game/tests/golden/palette_baseline/art_snapshot.png delete mode 100644 game/tests/golden/palette_baseline/ui_tokens.png create mode 100644 game/tests/unit/battle_settlement/effect_port_test.gd create mode 100644 game/tests/unit/battle_settlement/effect_port_test.gd.uid delete mode 100644 reports/工程改动报告-工程清理批-2026-11-02.md delete mode 100644 tools/gen_palette_baseline.py diff --git a/design/engineering/test-conventions.md b/design/engineering/test-conventions.md index 039058c1..09c7c39e 100644 --- a/design/engineering/test-conventions.md +++ b/design/engineering/test-conventions.md @@ -35,7 +35,7 @@ 需求卡片 §8「改动—验收对照表」每一条都要落成可执行的验证;本文定统一写法,卡里定每批的具体内容(测什么/注入哪里)。写卡期查不出执行载体的条目=不可测,当场呈报,禁拖到实现期。 1. **执行载体五选一,逐条声明**:单元测试(`tests/run_all`)/系统测试断言(`tests/system_test.gd`)/场景探针(`probe/` 下 `*_probe`)/MC 同种子逐项比对/人工核对(仅限无法自动化的纪律类需求点,如命名锁定、数值占位)。行为类条目以自动化载体为主。 -2. **粒度与对应关系**:每条清单条目至少一个自动断言,且断言必须从该条目的真实入口进入;多条条目可合用一个测试函数,但每条都要有自己单独可判红的判据(取证口径见 §8.4 唯一口径——禁变异/禁撤销)。断言失败消息末尾带编号尾缀〔卡短名 Vn〕(如〔秘宝批 V3〕)——断言与卡片清单可互相 grep 查找。`Kit.check_quiet` 静默收集形态(计数不入判定即打印、失败消息进 problems 汇总)的断言已入 `tools/extract_checks.py` 扫描面(2026-11-02 提取器改版:三通道同口径提取,静默通道条目 quiet 列记 `channel:check_quiet`),验收尾缀 grep 对该通道有效。 +2. **粒度与对应关系**:每条清单条目至少一个自动断言,且断言必须从该条目的真实入口进入;多条条目可合用一个测试函数,但每条都要有自己单独可判红的判据(取证口径见 §8.4 唯一口径——禁变异/禁撤销)。断言失败消息末尾带编号尾缀〔卡短名 Vn〕(如〔秘宝批 V3〕)——断言与卡片清单可互相 grep 查找。**工具面局限备案(2026-10-27 迭代 39 验收审计)**:`Kit.check_quiet` 静默收集形态(计数不入判定即打印、失败消息进 problems 汇总)的断言消息不进 `tools/extract_checks.py` 扫描面——验收尾缀 grep 对该通道失效;如需覆盖属提取器改版(工程批)。 3. **断言词汇按验证对象五选一**: | 词汇 | 适用场景 | diff --git a/design/gdd/systems/跨局循环/锻造工坊.md b/design/gdd/systems/跨局循环/锻造工坊.md index 8319cdb8..679e80ab 100644 --- a/design/gdd/systems/跨局循环/锻造工坊.md +++ b/design/gdd/systems/跨局循环/锻造工坊.md @@ -1,7 +1,7 @@ # 锻造工坊 > 一句话:营地的武器与护甲成长——武器锻造区三条路线、护甲锻造区三条路线各练各的,钥石消费,一次冒险各选一件,满级特效留位本期不做 -> 状态:**v0.11(2026-11-02 补验收标准节:九条结构面判据落文,规则与数值零变化)**——全部数值 provisional,终值归 Alpha 收官 M4 数值批〔未排期〕 +> 状态:**v0.10(2026-10-29 实现内容清理:删「现役代码事实」「实装边界」两节与正文代码指证,规则与数值零变化)**——全部数值 provisional,终值归 Alpha 收官 M4 数值批〔未排期〕 ## 设计目的 用户原案(playnote 2026-10-06):把现役"营地花钱买攻击 +1"改造成有等级、有成长、有满级特效的**武器升级路线**;新增长枪(穿刺)与法杖(魔法)实现武器间差异化;护甲同理——初始只有轻甲(升级只提 AC),最高等级后解锁重甲/魔法护甲,基础 AC 各异、各带满级特效。 @@ -209,17 +209,3 @@ ### 10 · 数值与验证 全部数值 provisional 归 Alpha 收官 M4 数值批;实装批必须带改前 / 改后同随机种子配对读数验证难度影响。 - -## 验收标准 - -本节判据全部绑结构与关系、不绑具体数值:价格、增量、等级上限等行值 provisional 归 M4 数值批,改值不推翻判据;判据失效只发生在结构关系本身被改时。 - -- **柜台解锁与默认值解耦**:天赋树的锻造工坊钥匙节点(见[天赋树](天赋树.md))点亮后本柜台开放、可购买;未点亮时柜台锁定、不存在任何购买途径;未点亮期间武器/甲默认值照常经参数面注入冒险,不存在「未解锁就读不到默认值」的窗口(2026-10-11 树侧钥匙裁)。 -- **路线解锁双通道并联**:长枪、法杖、重甲、魔法护甲四条路线的解锁条件全部在本系统内部、与关卡进度零绑定;每条路线同时开两条互不影响的通道——练满前一档、或支付一次性解锁价直购——任一通道成立即解锁(4b/4b-1 裁)。 -- **窗口内容**:营地四柜台并排呈现;本柜台分武器、护甲两区,各列三条路线行,行名=件名+锻造;出场行显示当前选中(武器+甲);满级特效无窗口入口、无文案(本期不做)。 -- **单选注入与树零攻防**:攻击加值唯一来源=当前选中武器路线、AC 加值唯一来源=当前选中甲路线;0 级=未练合法态、零加值;天赋树淬炼对攻击与 AC 零贡献;开局注入按「账号层基础值+树淬炼 flat+锻造加值+局内加算区」分层可加,锻造加值只取当前选中(2026-10-11 所有权切分裁)。 -- **切换语义**:营地内任意武器×甲组合切换免费、即时生效;换装不丢级不生效——旧选中等级保留、加值待命;一次冒险=一个武器类型+一个甲型;冒险开始后锁死、局内不存在任何换装途径(1a/1b/1c 裁)。 -- **定价结构**:每级价格随已购级数几何递增(第 N+1 级价格 > 第 N 级价格,基价与公比读表单点);路线解锁价一次性收取;钥石不足时购买被拒、余额与等级零变化;GP 全程不可用于本柜台(5b 裁)。 -- **存档闭合**:六条路线等级+当前武器/甲选中标记写入存档,读档恢复与写入前一致;存档域无特效域(6 裁连带)。 -- **祝福件负断言**:局内商店购买攻击/护甲祝福后,本柜台路线等级与选中态零变化(8a 裁连带——货架行名带「攻击」「护甲」字样,属性祝福与锻造升级是两条互不相通的通道)。 -- **甲型双轴可感**:切换甲型后基础 AC 与挨普通攻击的克制修正(轻甲 0/重甲 −1/魔法护甲 +1)双轴同时变化(3b 裁;现行敌方攻击全部按普通处理,断言条件当下即成立)。 diff --git a/game/core/action_economy.gd b/game/core/action_economy.gd index 8fc13919..8a12b50b 100644 --- a/game/core/action_economy.gd +++ b/game/core/action_economy.gd @@ -37,15 +37,15 @@ var _pending_ap_delta := 0 # 下轮行动点修正排队(act var _charge_layers := 0 var charge_layers: int: get: return _charge_layers -# 效果引擎(dig_paid/charge_full 发射面;round_end 转存决策面) -var _fx: EffectEngine = null -var _ctx: EffectContext = null +# 效果端口件(dig_paid/dig_stop/charge_full 发射面经端口路由宿主;组装期单点注入—— +# host _init bind_port;缺省 null=未绑零扰动,与原 _fx==null 守卫同语义) +var _port: EffectPort = null var _last_stop_ap := 0 # 停手侧剩余 AP 记账(转存决策在 round_end 发射点——grant_ap(transfer) 订阅定夺) -func set_fx(fx: EffectEngine, ctx: EffectContext) -> void: - _fx = fx - _ctx = ctx +# 效果端口件注入(host _init 组装期单点——先于 begin_battle 任何发射) +func bind_port(port: EffectPort) -> void: + _port = port func _init(db_ref: ConfigDb) -> void: @@ -116,16 +116,16 @@ func _apply_dig_paid_hits(base_cost: int) -> Dictionary: var cost := base_cost var dig_refund := 0 var rap_gain := 0 - if _fx != null: - for h in _fx.dispatch(_ctx, "dig_paid", {"ap_cost": cost, "ap_surplus": _ap - cost}): + if _port != null: + for h in _port.emit("dig_paid", {"ap_cost": cost, "ap_surplus": _ap - cost}): if String(h.get("kind", "")) == "counter_spend": var extra := int(h.get("value", 1)) if _ap >= cost + extra: cost += extra - EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), 1) + _port.counter_add(String(h.get("counter_id", "")), 1, false) elif String(h.get("kind", "")) == "counter_add": var set_v = h.get("set", null) - EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != 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) elif String(h.get("kind", "")) == "grant_ap": dig_refund += int(h.get("value", 0)) elif String(h.get("kind", "")) == "grant_reserve_ap": @@ -174,13 +174,13 @@ func request_stop() -> bool: return false # dig_stop 发射+施加(停手挂点〔弃图兑换〕:剩余 AP 随载荷外发,counter_add # value_event="ap_left" 取值面〔值=停手时刻真实余量〕;发射先于记账清零) - if _fx != null: - for h in _fx.dispatch(_ctx, "dig_stop", {"ap_left": _ap}): + if _port != null: + for h in _port.emit("dig_stop", {"ap_left": _ap}): if String(h.get("kind", "")) == "counter_add": var val := int(h.get("value", 0)) if String(h.get("value_event", "")) == "ap_left": val = _ap - EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), val, h.has("set")) + _port.counter_add(String(h.get("counter_id", "")), val, h.has("set")) _last_stop_ap = _ap _ap = 0 _finish_round("stopped", "same_board") @@ -201,8 +201,8 @@ func finish_round_stop(transfer: bool, transfer_pct: int = 100) -> void: if transfer: var prev_layers := _charge_layers _charge_layers = mini(CHARGE_LAYER_CAP, _charge_layers + _last_stop_ap) - if _fx != null and _charge_layers >= CHARGE_LAYER_CAP and prev_layers < CHARGE_LAYER_CAP: - _fx.dispatch(_ctx, "charge_full", {"layers": _charge_layers}) + if _port != null and _charge_layers >= CHARGE_LAYER_CAP and prev_layers < CHARGE_LAYER_CAP: + _port.emit("charge_full", {"layers": _charge_layers}) else: _rap += _last_stop_ap * maxi(1, transfer_pct) / 100 _last_stop_ap = 0 diff --git a/game/core/battle_fx_host.gd b/game/core/battle_fx_host.gd index a4a3bc42..10df992d 100644 --- a/game/core/battle_fx_host.gd +++ b/game/core/battle_fx_host.gd @@ -7,11 +7,16 @@ ## 留宿主,禁回调宿主私有族;揭示奖励兑付走 bind_grant 注入的兑付回调(RewardGrant 授予面—— ## snapshot 查询与 grant_routed 同面);对本件零反向强引用(禁 RefCounted 双向环——两件均随每战新建)。 ## 随机只经注入 rng 的 fx_reveal 封闭流(除该流外零随机;零 await——效果引擎全同步解释器)。 -## 类变量封闭面(14≤15):bundle 7 项(db/rng/minesweeper/action_economy/damage/emit_cb/facts_cb, +## 类变量封闭面(15≤15):bundle 7 项(db/rng/minesweeper/action_economy/damage/emit_cb/facts_cb, ## 缺一键 assert——引用束注入防 _init max-parameters 新键)+grant_cb+engine+counters_ctx+ ## ac 束(键集封闭 base/round/battle)+_fx_bonus_delta+_fx_new_resource_types+reveal 束 -## (键集封闭 queue/busy)。禁实现期增删键;禁为机检文本保留无效成员(原编排器 fx_counters_round/ +## (键集封闭 queue/busy)+facts_inject(键集封闭 dig_streak/rider_tier_bonus/rider_tier_crit—— +## 业务发射事实束运行态写面,combat_segment 每轮写 dig_streak/begin_battle 每战复位 rider 两键)。 +## 禁实现期增删键;禁为机检文本保留无效成员(原编排器 fx_counters_round/ ## battle 两零读者成员迁入即删——同物异名同字典,桶引用经 counters_ctx 承载)。 +## 效果端口件组建与转注(轴戊):_init 尾段建 EffectPort(六 Callable 绑本件对应方法)后对两业务类 +## bind_port——端口不落本件成员(单向接线,本件零回持);业务类发射面全经端口路由, +## 计数器账面(counters_ctx)唯一宿主侧。 class_name BattleFxHost extends RefCounted @@ -29,6 +34,7 @@ var ac := {"base": -1, "round": 0, "battle": 0} # ac 束(base=主角 AC 基 var _fx_bonus_delta := {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} # 本战 bonus_delta 累积(战末宿主 bonus_delta_drain 读后清零) var _fx_new_resource_types := {} # 本战已见资源类型集(地质学家 fact new_resource_type 判定——battle 域) var reveal := {"queue": [], "busy": false} # reveal 束(queue=揭示效果待兑队列〔见血开窍/斩首学者/地质学家/跳格采掘共用——授予段末统一兑付,防轮 facts 迭代中追加重入〕;busy=兑付中标记——宿主 _on_cell_revealed 缓冲旁路判定,防二次缓冲二次兑付) +var facts_inject := {"dig_streak": 0, "rider_tier_bonus": 0, "rider_tier_crit": false} # 业务发射事实束(键集封闭——原 damage.inject 运行态束迁宿主:dig_streak=combat_segment 每轮写;rider 两键=begin_battle 每战复位;charge_layers 不在束内=活读 action_economy) func _init(bundle: Dictionary) -> void: @@ -43,6 +49,18 @@ func _init(bundle: Dictionary) -> void: facts_cb = bundle.facts_cb engine = EffectEngine.new(db) counters_ctx = EffectContext.new() # round/battle 两桶即 ctx 自带字典——编排器旧 fx_counters 两成员同物异名零读者,不再另立 + # 效果端口件组建与转注(轴戊——组装期单点:先于任何发射,六 Callable 绑本件方法后对两业务类转注; + # 端口不落本件成员——单向接线,本件零回持;方法直引用形态同 bundle emit_cb/facts_cb 既有接线) + var port := EffectPort.new({ + "emit_cb": fx_emit_biz, + "counter_read_cb": counter_read, + "counter_add_cb": counter_add, + "fact_cb": biz_fact, + "reveal_cb": enqueue_reveal, + "ac_cb": apply_ac_bonus, + }) + bundle.damage.bind_port(port) + bundle.action_economy.bind_port(port) # 揭示兑付回调挂接(宿主 _init 建两件后接线——构造序禁环) @@ -55,6 +73,41 @@ func fx_emit(event: String, payload: Dictionary) -> Array: return engine.dispatch(counters_ctx, event, payload, _fx_fact) +# 业务类发射路由(效果端口件 emit_cb——damage/action_economy 发射面统一出口; +# 事实回调=biz_fact 闭面五键,battle_index/round_index/seals_count/nest_count 禁进业务发射面) +func fx_emit_biz(event: String, payload: Dictionary) -> Array: + return engine.dispatch(counters_ctx, event, payload, biz_fact) + + +# 计数器账面路由(效果端口件 counter_read_cb——value_counter 层值读;账面唯一宿主侧) +func counter_read(counter_id: String) -> int: + return EffectEngine.counter(counters_ctx, counter_id) + + +# 计数器账面路由(效果端口件 counter_add_cb——施加单点;账面唯一宿主侧) +func counter_add(counter_id: String, value: int, set_form: bool) -> void: + EffectEngine.counter_add(counters_ctx, counter_id, value, set_form) + + +# 业务发射事实面(闭面五键——三 when 事实 {charge_layers 活读/dig_streak 轮内槽/hp_ratio 主角 +# 血量比例算式逐字承 damage_controller 原式}∪ 两 rider 运行态读数 {facts_inject 现值}; +# 其余事实一律 null——battle_index/round_index/seals_count/nest_count 禁进,扩面=行为变化) +func biz_fact(name: String) -> Variant: + match name: + "charge_layers": + return action_economy.charge_layers + "dig_streak": + return int(facts_inject["dig_streak"]) + "hp_ratio": + var hu: Dictionary[Variant, Variant] = damage.units.hero_unit() + return int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1)))))) # 百分比 int(when lte int 截断口径) + "rider_tier_bonus": + return int(facts_inject["rider_tier_bonus"]) + "rider_tier_crit": + return bool(facts_inject["rider_tier_crit"]) + return null + + # 偏运权重查询(reward_roll 发射面——bias_rewards 词汇在本层收口,掷格域只见整数)。 # 三参形 dispatch(不带 facts:本事件订阅行 when 事实面不参与,命中集与缝拆前逐字同); # 发射时点由掷格件在开流之后、权重装配之前调用;零命中=0=原行集零扰动。 diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 026066a9..9b75544f 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -226,9 +226,8 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: "action_economy": action_economy, "status_effects": _status_effects, "units": _enemy_system, "emit_cb": _emit_row, "fatal_cb": _fatal, "attune_safe_seen_cb": _attune_safe_seen_read, "pool_effective_cb": _pool_effective}) _fx_host.bind_grant(_reward_grant.reveal_redeem) # 构造序禁环:两件建齐后接线(fx 件对授予面走注入回调) - _damage.set_fx(_fx_host.engine, _fx_host.counters_ctx) # 效果引擎注入(attack_commit/clamp_check/armor_roll/kill/damage_taken 发射面) - action_economy.set_fx(_fx_host.engine, _fx_host.counters_ctx) # dig_paid/charge_full 发射面 - _reward_economy.luck_bias_query = _fx_host.luck_bias_for # 偏运查询口注入(reward_roll 发射归 fx 宿主) + # 业务类发射面接线归 host._init 尾段组装期完成(效果端口件 bind_port 转注——先于 begin_battle 任何发射) + _reward_economy.roller.luck_bias_query = _fx_host.luck_bias_for # 偏运查询口注入(掷格件缝拆形态随件迁移——reward_roll 发射归 fx 宿主) minesweeper.cell_revealed.connect(_on_cell_revealed) minesweeper.seal_judged.connect(_on_seal_judged) minesweeper.round_end_board.connect(_on_round_end) @@ -340,13 +339,11 @@ func _begin_battle_fx_state(enc: Dictionary) -> void: var adv_bucket: Variant = enc.get("adv_bucket", null) _fx_host.mount_buckets(adv_bucket if adv_bucket is Dictionary else {}, enc.get("level_counters")) # 冒险桶宿主字典(直构兜底自有空字典);关卡桶 LevelRunner 每关 new 下传、非 Dictionary 即不挂 _reward_grant.bind_battle(_battle_hooks()) # 授予件战斗域回调束(充能消耗/真视计数/踩雷计数) - _damage.fx_calls["reveal"] = _fx_host.enqueue_reveal - _damage.fx_calls["ac"] = _fx_host.apply_ac_bonus var feat_ids: Array[Variant] = enc.get("feats", []) as Array # 流派档位质变不设(奇物.md v1「不设流派标签、 - # 不设档位机制」):rider_tier 两注入恒零,散件单体效果=唯一形态 - _damage.inject["rider_tier_bonus"] = 0 - _damage.inject["rider_tier_crit"] = false + # 不设档位机制」):rider_tier 两运行态复位恒零,散件单体效果=唯一形态 + _fx_host.facts_inject["rider_tier_bonus"] = 0 + _fx_host.facts_inject["rider_tier_crit"] = false _facts.feat_first_dig_used = false # 树 rule 键消费(Fork 激活侧效果=效果引擎合成 # 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1)。其余 rule 键 @@ -499,7 +496,7 @@ func _hits_of(hits: Array, kind: String) -> Array: # aux 解锁门控转发(LevelRunner 注入;E1/E3 封装:外部经公有面,禁直读 _reward_economy) func set_treasure_unlocked(ids: Dictionary) -> void: - _reward_economy.set_treasure_unlocked(ids) + _reward_economy.roller.set_treasure_unlocked(ids) # ---- 开轮摇号(共识点 B:技能选择前置到轮次开始,数值计算留结算段)---- diff --git a/game/core/board_installer.gd b/game/core/board_installer.gd index 96f7d921..f8b7c123 100644 --- a/game/core/board_installer.gd +++ b/game/core/board_installer.gd @@ -77,13 +77,13 @@ func install(during_round := false) -> bool: # region 布置 = 兜底回落合法态(防配置缺行静默裸奔——provisional)。 func _resolve_global_variant(board: BoardData) -> void: board.board_variants.clear() - reward_economy.board_skew = 0 + reward_economy.roller.board_skew = 0 var row: Dictionary[Variant, Variant] = db.combat.variation.global_variant_row(int(encounter.level_index), String(board.board_type), String(encounter.get("node_type", ""))) if row.is_empty(): return board.board_variants = [String(row.variant_id)] - reward_economy.board_skew = int(row.get("effect_value", 0)) + reward_economy.roller.board_skew = int(row.get("effect_value", 0)) # ---- 地形变异 布置段(棋盘设置顺序第 4 步)---- diff --git a/game/core/damage_controller.gd b/game/core/damage_controller.gd index 585eb080..3ebf3137 100644 --- a/game/core/damage_controller.gd +++ b/game/core/damage_controller.gd @@ -41,19 +41,9 @@ var _outcomes := {} # ticket_id → "redeemed"/"zero"/"voided"( var _seq := 0 # 流水号 var _halted := false # 主角倒下终止封印(死亡事件置位——flush 余单作废) var _hero_ac_override := -1 # 主角护甲等级合流入参(begin_battle 显式设定——-1 = 表值兜底,口径同战斗编排器) -# 效果引擎(attack_commit/clamp_check/armor_roll/kill/damage_taken -# 发射+施加面:dispatch 返回命中,本类按组合规则施加) -var _fx: EffectEngine = null -var _ctx: EffectContext = null -# 战斗段运行态事实束(编排器每轮/每战注入——键集封闭,缺键读缺省即零扰动): -# dig_streak/charge_layers——facts 取数面(attack_commit 载荷 dig_streak;猎巢人 per/整备 -# attack_bonus per/厚积薄发 when/铁壁 shield per 同读 charge_layers)——combat_segment 每轮写; -# rider_tier_bonus/rider_tier_crit——流派档位两运行态(magic 档一 rider+1/档二 rider 可暴击) -# ——begin_battle 每战写 -var inject := {"dig_streak": 0, "charge_layers": 0, "rider_tier_bonus": 0, "rider_tier_crit": false} -# 效果引擎入站回调束(编排器 begin_battle 同点注入):reveal=揭示效果回调(kill 发射点 reveal_tiles -# 订阅面,入队授予段末兑付)/ac=ac_bonus 回调(石肤 AC 累积单点同步) -var fx_calls := {"reveal": Callable(), "ac": Callable()} +# 效果端口件(组装期单点注入——host _init bind_port):发射/计数器/事实/揭示/AC 全经端口路由, +# 缺省 null=未绑(与原 _fx==null 零扰动守卫同语义——单测直构不接端口的夹具零扰动照跑) +var _port: EffectPort = null func _init(db_ref, rng_ref, units_ref, effects_ref) -> void: @@ -69,44 +59,33 @@ func set_hero_ac_override(v: int) -> void: _hero_ac_override = v -# 效果引擎注入(战斗编排器 _init——事件发射面接线) -func set_fx(fx: EffectEngine, ctx: EffectContext) -> void: - _fx = fx - _ctx = ctx +# 效果端口件注入(host _init 组装期单点——先于 begin_battle 任何发射) +func bind_port(port: EffectPort) -> void: + _port = port -# 事件发射薄面(发射+命中收集统一入口——订阅空=零循环零扰动;命中由各施加面按组合规则施加) +# 事件发射薄面(发射+命中收集统一入口——订阅空=零循环零扰动;命中由各施加面按组合规则施加; +# 路由经效果端口件,事实回调=biz_fact 闭面五键宿主侧) func _fx_hits(event: String, payload: Dictionary) -> Array: - if _fx == null: + if _port == null: return [] - return _fx.dispatch(_ctx, event, payload, _fx_fact) + return _port.emit(event, payload) -# 事件发射薄面(免施加观测位——enemy_hp_threshold 等现无订阅者的事件保留零扰动面) -# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数:charge_layers 编排器每轮注入。 -# hp_ratio 补入〔背水 when 面,敌技 damage_taken 载荷不带血比例时回落〕; -# fx_calls.reveal=揭示效果回调〔斩首学者 kill 订阅面——编排器注入,入队授予段末兑付〕) -func _fx_fact(fact: String) -> Variant: - match fact: - "charge_layers": - return int(inject["charge_layers"]) - "dig_streak": - return int(inject["dig_streak"]) - "hp_ratio": - var hu: Dictionary[Variant, Variant] = units.hero_unit() - return int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1)))))) # 百分比 int(when lte int 截断口径) - return null +# 未绑事实回落(缺省 null——int()/bool() 包裹后与原 inject 缺省值逐位同:int(null)=0/bool(null)=false) +func _fact_or(name: String) -> Variant: + return null if _port == null else _port.fact(name) -# per 取数(per 字段解析——载荷同名字段优先,回落补充事实面;add_ticket_rider/attack_bonus/ +# per 取数(per 字段解析——载荷同名字段优先,回落端口事实面;add_ticket_rider/attack_bonus/ # shield/open_ticket 的倍数承载:贡献值 = value × per 读数,cap 对乘积封顶) func _per_value(name: String, payload: Dictionary) -> int: if name == "": return 1 if payload.has(name): return int(payload[name]) - if _fx_fact(name) != null: - return int(_fx_fact(name)) + if _fact_or(name) != null: + return int(_fact_or(name)) return 0 @@ -169,7 +148,7 @@ func open_ticket(kind: String, source_class: String, display: String, spec: Dict var ac_payload := {"atk": int(t.base_value), "target": String(tgt.get("display_name", "")) if tgt != null else "", "target_id": String(tgt.get("enemy_id", "")) if tgt != null else "", - "matchup": matchup_o, "dig_streak": int(inject["dig_streak"]), "nest_count": _nest_count()} + "matchup": matchup_o, "dig_streak": int(_fact_or("dig_streak") or 0), "nest_count": _nest_count()} t.context["fx_attack_hits"] = _fx_hits("attack_commit", ac_payload) t.context["fx_attack_payload"] = ac_payload return t @@ -361,13 +340,13 @@ func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float: if String(h.get("kind", "")) == "attack_bonus": var contrib := int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ac_payload) if h.has("value_counter") and String(h.get("value_counter", "")) != "": - contrib = EffectEngine.counter(_ctx, String(h.get("value_counter", ""))) + contrib = _port.counter_read(String(h.get("value_counter", ""))) base_value += float(contrib) - # 计数器施加(attack_commit 命中行内 counter_add〔用后清零行〕,命中序=声明序) + # 计数器施加(attack_commit 命中行内 counter_add〔用后清零行〕,命中序=声明序;端口路由账面) for h in ac_hits: if String(h.get("kind", "")) == "counter_add": var set_v = h.get("set", null) - EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != 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) # 任意真族:厚积薄发满层 spike 倍乘(≥3 层——强制必中已在折扣拍承载) if ctx["spike_ready"]: for h in ac_hits: @@ -397,7 +376,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D # (奥术余烬「普攻命中后恒定结算」语义);猎巢人巢控光环(per nest_count)/矿工的执拗开格加成 # (per dig_streak、cap 封顶)同通道合计;档一质变 rider+1 / 档二(crit 时)rider 乘暴击倍率。 # rider 不改变下限语义(landed 含 rider 后不再过 floor)。 - var hero_rider := int(inject["rider_tier_bonus"]) + var hero_rider := int(_fact_or("rider_tier_bonus") or 0) # or 0=未绑缺省与 int(null) 缺省同值(int() 构造拒 null——端口缺省 null 面的空值合并) for h in ac_hits: if String(h.get("kind", "")) == "add_ticket_rider": var contrib := int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ctx["ac_payload"]) @@ -405,7 +384,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D contrib = mini(int(h.cap), contrib) hero_rider += contrib if hero_rider > 0: - if bool(ctx["crit"]) and bool(inject["rider_tier_crit"]): + if bool(ctx["crit"]) and bool(_fact_or("rider_tier_crit") or false): hero_rider = int(round(float(hero_rider) * float(db.combat.settlement.damage_roll.crit_multiplier))) landed += hero_rider payload["hero_rider"] = hero_rider @@ -421,9 +400,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D func _kill_overflow(t: Dictionary, target: Dictionary, landed: int) -> void: for h in _fx_hits("kill", {"target": String(target.display_name), "target_id": String(target.get("enemy_id", "")), "overflow": landed}): if String(h.get("kind", "")) == "reveal_tiles": - var reveal_cb: Callable = fx_calls["reveal"] - if reveal_cb.is_valid(): - reveal_cb.call() # 斩首学者——击杀揭示 1 格(编排器入队,授予段末兑付) + _port.reveal_effect() # 斩首学者——击杀揭示 1 格(端口路由宿主入队,授予段末兑付) continue if String(h.get("kind", "")) != "overflow": continue @@ -468,7 +445,7 @@ func _resolve_enemy_skill(t: Dictionary) -> void: var total: int = int(roll["total"]) # damage_taken 发射(kind=enemy_skill;铁壁整备减伤订阅面:shield 加法族合计 value×per(charge_layers), # 稳健拓荒树规则同面(rebuild 合成订阅);减后过下限保底。damage_reduce〔背水 hp_ratio when〕/ - # counter_add〔保险丝/石肤〕/ac_bonus〔石肤〕同面施加;hp_ratio fact 由 _fx_fact 补充事实面回落提供) + # counter_add〔保险丝/石肤〕/ac_bonus〔石肤〕同面施加;hp_ratio fact 由 damage_taken 载荷携带) var dt_res := _emit_hero_damage_taken(total, "enemy_skill", String(t.context.get("skill_id", "")), t.origin_cell, int(t.round_index)) total = int(dt_res["total"]) units.apply_damage(hero, total) @@ -525,12 +502,10 @@ func _apply_hero_damage_effects(hits: Array, total: int) -> Dictionary: "damage_reduce": total = maxi(0, total - int(h.get("value", 0))) "ac_bonus": - var ac_cb: Callable = fx_calls["ac"] - if ac_cb.is_valid(): - ac_cb.call(h) + _port.apply_ac(h) "counter_add": var set_v = h.get("set", null) - EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != 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) if shield > 0: total = maxi(int(db.combat.settlement.damage_roll.damage_floor), total - shield) return {"total": total, "shield": shield} diff --git a/game/core/effect_port.gd b/game/core/effect_port.gd new file mode 100644 index 00000000..c3b3c2e0 --- /dev/null +++ b/game/core/effect_port.gd @@ -0,0 +1,71 @@ +## 效果端口件:战斗业务类(DamageController/ActionEconomy)唯一的效果发射出口——收事件与载荷、 +## 交效果引擎结算、取回命中批。六 Callable 路由宿主(BattleFxHost.fx_emit_biz/counter_read/ +## counter_add/biz_fact/enqueue_reveal/apply_ac_bonus),经组装期 bind_port 单点注入。 +## 闭面红线:事实回调=biz_fact 闭面五键(charge_layers/dig_streak/hp_ratio/rider_tier_bonus/ +## rider_tier_crit),battle_index/round_index/seals_count/nest_count 禁进业务发射事实面。 +## 单向接线:本件只持宿主注入的 Callable,禁持 BattleFxHost 对象引用(宿主 bundle 已持两业务类 +## ——对象引用即成 host→damage→port→host 强引用环,两件均随每战新建=泄漏)。 +## 未绑缺省:emit 返回空批/计数器读 0/计数器施加与揭示与 AC 回调 no-op/事实答 null—— +## 与原业务类 _fx==null 零扰动守卫同语义(单测直构不接端口的既有夹具零扰动照跑)。 +class_name EffectPort +extends RefCounted + +var emit_cb: Callable # func(event: String, payload: Dictionary) -> Array——宿主 fx_emit_biz +var counter_read_cb: Callable # func(counter_id: String) -> int——宿主 counter_read +var counter_add_cb: Callable # func(counter_id: String, value: int, set_form: bool) -> void——宿主 counter_add +var fact_cb: Callable # func(name: String) -> Variant——宿主 biz_fact(伤害域闭面) +var reveal_cb: Callable # func() -> void——宿主 enqueue_reveal +var ac_cb: Callable # func(h: Dictionary) -> void——宿主 apply_ac_bonus + + +func _init(bundle: Dictionary) -> void: + for k in ["emit_cb", "counter_read_cb", "counter_add_cb", "fact_cb", "reveal_cb", "ac_cb"]: + assert(bundle.has(k), "[效果端口] 引用束缺键: %s" % k) + emit_cb = bundle.emit_cb + counter_read_cb = bundle.counter_read_cb + counter_add_cb = bundle.counter_add_cb + fact_cb = bundle.fact_cb + reveal_cb = bundle.reveal_cb + ac_cb = bundle.ac_cb + + +# 事件发射(收事件与载荷、交宿主路由效果引擎结算、取回命中批;未绑→空批) +func emit(event: String, payload: Dictionary) -> Array: + if not emit_cb.is_valid(): + return [] + return emit_cb.call(event, payload) + + +# 计数器层值读(value_counter 层值取数——宿主 counters_ctx 账面单点;未绑→0) +func counter_read(counter_id: String) -> int: + if not counter_read_cb.is_valid(): + return 0 + return int(counter_read_cb.call(counter_id)) + + +# 计数器施加(命中行 counter_add 用后清零/计账——未绑→no-op) +func counter_add(counter_id: String, value: int, set_form: bool) -> void: + if not counter_add_cb.is_valid(): + return + counter_add_cb.call(counter_id, value, set_form) + + +# 事实取值(闭面五键宿主侧单点——未绑→null) +func fact(name: String) -> Variant: + if not fact_cb.is_valid(): + return null + return fact_cb.call(name) + + +# 揭示效果入队(kill 发射点 reveal_tiles 订阅面——授予段末兑付;未绑→no-op) +func reveal_effect() -> void: + if not reveal_cb.is_valid(): + return + reveal_cb.call() + + +# AC 累积回调(damage_taken ac_bonus 石肤累积单点;未绑→no-op) +func apply_ac(h: Dictionary) -> void: + if not ac_cb.is_valid(): + return + ac_cb.call(h) diff --git a/game/core/effect_port.gd.uid b/game/core/effect_port.gd.uid new file mode 100644 index 00000000..2ff9776e --- /dev/null +++ b/game/core/effect_port.gd.uid @@ -0,0 +1 @@ +uid://b50wf7iako1uy diff --git a/game/core/reward_economy.gd b/game/core/reward_economy.gd index 907a7ec1..fd51b351 100644 --- a/game/core/reward_economy.gd +++ b/game/core/reward_economy.gd @@ -1,17 +1,26 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION -## 格面资源身份层:install 期一次性预分配每格资源身份,授予期零随机、Ready 后只读无重掷(垂直切片验证:格面资源同种子逐位回放)。 -## roll_board 每板恰一次(Ready 相位再调 = double-roll 拒绝;换图 = 合法新批);禁在本类引入授予期随机或重掷。 +## 格面资源身份层(受理账面):授予期零随机、Ready 后只读无重掷(垂直切片验证:格面资源同种子逐位回放)。 +## 摇号面已分家(architecture v1.29 轴戊):装配期格面资源摇号(池行装配/区间行 roll/权重合成) +## 归 RewardRoller(本件 _init 自建自持有,外部经 roller 组件出口直达);本类保留受理账面 +## (grant/身份查询/快照/retire/polarity_row)与 roll_board 装配序(守卫→复位→委托→收纳→ready)。 +## 衔接面=格→奖励身份映射批束(键集封闭 mapping/power_roll/clear_identity/clear_power——掷格件产出、 +## 本件接收收纳)。 +## roll_board 每板恰一次(Ready 相位再调 = double-roll 拒绝;换图 = 合法新批);禁引入授予期随机或重掷。 ## 极性路由归战斗结算授予段——本类只提供 polarity_row 的(类型, 极性)→ 字典行查询面。 class_name RewardEconomy extends RefCounted var db: ConfigDb -var rng: RngServiceCore # 违约中止通道——缺省空 Callable 退化 push_error+assert(现行为逐字保持);组装层注入 # orchestrator._fatal,使 release 剥 assert 后仍经 battle_result 封印真中止。 -var fatal_handler: Callable = Callable() +# 属性 setter 同拍转注 roller(构造序:economy._init 不触该成员、编排器注入晚于 _init—— +# roller 已在座时赋值即转注,掷格件违约封印与受理面同一通道) +var fatal_handler: Callable = Callable(): + set(v): + fatal_handler = v + if roller != null: + roller.fatal_cb = v var _phase := "unrolled" # unrolled → ready → retired -var _eligible: Array[Dictionary] = [] # [{cell}] 行主序规范序 var _mapping := {} # Vector2i → reward_id(正向行身份——格面资源类型由 cell_type 承载) var _power_roll := {} # Vector2i → 区间行 roll 值(身份批内追加取号) var _clear_power := -1 # 清图槽区间行 roll 值(-1 = 定值行) @@ -19,19 +28,19 @@ var _granted := {} # Vector2i → 快照(重复受理幂等返 var _clear_identity := "" var _clear_granted := false var _clear_snapshot := {} -# 偏运权重查询口(reward_roll 发射归 BattleFxHost 效果层——掷格域不持效果引擎/上下文; -# 参数=本次可掷格数、返回=偏运权重加算量、缺省空回调=恒 0=原行集零扰动) -var luck_bias_query: Callable = Callable() +var roller: RewardRoller # 掷格件(摇号面单点——自建自接线,BoardInstaller.generator 先例) func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: db = db_ref - rng = rng_ref + roller = RewardRoller.new({"db": db_ref, "rng": rng_ref, "fatal_cb": fatal_handler}) # ---- install 期 roll(恰一次;Ready 相位再调拒绝)---- -# eligible = 全部非封锁格(安全格 + 危险格,战斗结算 install 编排供给;封锁格不入清单) +# eligible = 全部非封锁格(安全格 + 危险格,战斗结算 install 编排供给;封锁格不入清单)。 +# 装配序:相位守卫(E5,先行——违约路径零取号)→ 空清单 fatal(E1)→ 受理面复位五拍 → +# 委托掷格件摇号 → 收纳批束 → ready func roll_board(eligible: Array) -> bool: # 相位守卫:Ready 再调 = double-roll 拒绝(E5);Retired(换图后)= 合法新批重置(E6 roll 半) if _phase == "ready": @@ -40,56 +49,20 @@ func roll_board(eligible: Array) -> bool: if eligible.is_empty(): _fatal("roll_board 空资格清单(E1 兜底 fail-fast——排雷玩法 不变式④前置)") return false - _eligible = _normalize_eligible(eligible) - var list: Array[Dictionary] = _eligible _granted.clear() # 新批重置映射受理面(旧图快照消亡) _power_roll.clear() _clear_power = -1 _clear_granted = false _clear_snapshot = {} - var stream: RandomNumberGenerator = rng.open_stream("reward_roll") # 恰一 scope 批(格批 + 清图批尾 + 区间行追加取号) - # 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 clear_rows: Array[Variant] = _pool_rows("clear_default") - # ① 身份批:每非封锁格恰一类型身份(雷安同池——同一行集同一权重分布) - var draft := {} - for e in list: - draft[e.cell] = weighted_pick(cell_rows, stream.randi_range(0, _pool_weight(cell_rows) - 1)) - # ② 清图槽批尾:同流同批判尾摇定清图奖励对象身份 - var clear_rid := weighted_pick(clear_rows, stream.randi_range(0, _pool_weight(clear_rows) - 1)) - # ③ 区间行 power roll(身份定稿后逐格恰一追加取号,定值行零取号) - for cell in draft.keys(): - var row: Dictionary[Variant, Variant] = _row_by_id(draft[cell]) - if row.has("power_min"): - _power_roll[cell] = stream.randi_range(int(row.power_min), int(row.power_max)) - var clear_row: Dictionary[Variant, Variant] = _row_by_id(clear_rid) - if clear_row.has("power_min"): - _clear_power = stream.randi_range(int(clear_row.power_min), int(clear_row.power_max)) - _mapping = draft - _clear_identity = clear_rid + var batch := roller.roll(eligible) + _mapping = batch.mapping + _power_roll = batch.power_roll + _clear_identity = batch.clear_identity + _clear_power = batch.clear_power _phase = "ready" return true -## 资格清单规范化域(开流前——零 RNG):行主序 + 去重(排序责任不外推给 战斗结算;排序键单点 BoardData.row_major_key) -func _normalize_eligible(eligible: Array) -> Array[Dictionary]: - var seen := {} - var list: Array[Dictionary] = [] - for e in eligible: - var c: Vector2i = e.cell - if seen.has(c): - push_warning("[地图战斗资源] 资格清单重复格已去重: %s" % str(c)) - continue - seen[c] = true - list.append({"cell": c}) - list.sort_custom(func(a, b): return BoardData.row_major_key(a.cell) < BoardData.row_major_key(b.cell)) - return list - - # ---- 查询面(Ready 只读;未开格恒可查——秘宝 桥接结构前提)---- # 相位只读出口(诊断观测面——unrolled/ready/retired;消费方禁直读 _phase 下划线字段) @@ -181,175 +154,6 @@ func polarity_row(cell_type: String, polarity: String) -> Dictionary: return {} -# ---- F1 选行算子纯函数 ---- - -# 偏运硬币 N(reward_roll bias_rewards 订阅命中合计经 _biased_cell_rows 入参;0 = 未装配零扰动) -# 稀有正向资源 kinds(底稿 §3——权重投影面,池结构零触碰) -const LUCK_BIAS_KINDS := ["heal", "gp", "pierce", "magic"] - -# 图级偏斜旋钮(battle_orchestrator 装板决议注入,全局型行 -# effect_value 语义=生存族权重下调百分比;0 = 中性 off 态。每板决议时显式重置/写入) -var board_skew: int = 0 - -# 偏斜族(需求卡片 §二:进攻族等比上浮/生存族按 v% 下调/白名单豁免恒不动——判定键=cell_type; -# 其余 cell_type(weaken/gp/action 白名单与未来扩型)= 豁免;负向行不入摇号已在 _rollable_rows 滤除) -const SKEW_DOWN_KINDS := ["heal", "defense"] -const SKEW_UP_KINDS := ["attack", "blast", "pierce", "magic"] - - -# 偏运硬币权重投影(底稿 §3:只动池内权重、雷安仍同池同分布;luck_bias=0 逐位返回原行集——off 态零扰动) -# 合成序:先经图级偏斜投影再叠件级加法——合成序锚=图级先、件级后 -#(环境事实先定型、玩家构筑叠其上;两投影均为池内权重面,雷安同池与取号次数零变化) -func _biased_cell_rows(bias: int) -> Array: - var base: Array[Variant] = _skewed_cell_rows() - if bias <= 0: - return base - var out: Array[Variant] = [] - for row in base: - if LUCK_BIAS_KINDS.has(String(row.get("cell_type", ""))): - var biased: Dictionary[Variant, Variant] = row.duplicate() - biased["weight"] = int(row.weight) + bias - out.append(biased) - else: - out.append(row) - return out - - -# 图级资源面偏斜投影(整数最大余数法:生存族按 board_skew% 下调,下调量按进攻族 -# 原权重比例整数重分配,合计恒等于原池合计 77;负向行与白名单三行逐位不动)。 -# board_skew<=0 逐位返回原行集 = off 态零扰动第一断言:不进投影域、行集/行序/取号域与基线逐位同。 -func _skewed_cell_rows() -> Array: - if board_skew <= 0: - return _rollable_rows() - var base: Array[Variant] = _rollable_rows() - var families := _skew_families(base) - if families["down"].is_empty() or families["up"].is_empty(): - return base # 结构缺族 = 无偏斜作用面(保守直通——池结构变更须走表加载校验另批) - # 下调总量 = round_half_up(Σ生存×v/100);逐行精确份额 w×v/100 最大余数取整(行序平局) - var drops := _largest_remainder_split(families["down"], board_skew, 100, (2 * _pool_weight(families["down"]) * board_skew + 100) / 200) - # 上浮总量 = 下调总量(守恒),逐行精确份额 drop×w/Σ进攻 最大余数取整 - var gains := _largest_remainder_split(families["up"], drops_total(drops), _pool_weight(families["up"]), drops_total(drops)) - var out := _apply_skew(base, families["down"], families["up"], drops, gains) - assert(_pool_weight(out) == _pool_weight(base), "图级偏斜守恒破(偏斜后合计须等于原池)") - return out - - -## 分族域:生存下调族与进攻上浮族行集拆分(池结构不变式零触碰) -func _skew_families(base: Array) -> Dictionary: - var down: Array[Variant] = [] - var up: Array[Variant] = [] - for row in base: - var k := String(row.get("cell_type", "")) - if SKEW_DOWN_KINDS.has(k): - down.append(row) - elif SKEW_UP_KINDS.has(k): - up.append(row) - return {"down": down, "up": up} - - -## 权重装配域:逐行精确份额增减(下调族扣 drops、上浮族加 gains——守恒 assert 随段迁移) -func _apply_skew(base: Array, down: Array, up: Array, drops: Array, gains: Array) -> Array: - var di := 0 - var ui := 0 - var out: Array[Variant] = [] - for row in base: - var k := String(row.get("cell_type", "")) - var w := int(row.weight) - if SKEW_DOWN_KINDS.has(k): - w -= drops[di] - di += 1 - elif SKEW_UP_KINDS.has(k): - w += gains[ui] - ui += 1 - assert(w > 0, "图级偏斜投影产生非正权重(池结构变更须走另批重审)") - var skewed: Dictionary[Variant, Variant] = row.duplicate() - skewed["weight"] = w - out.append(skewed) - return out - - -func drops_total(amounts: Array[int]) -> int: - var t := 0 - for a in amounts: - t += a - return t - - -# 最大余数整数分摊(行序平局——确定性):把 total 单位按精确份额 w_i×numer/denom 分到 rows -# (floor 后余数单位依次给分数最大者,平局取行序在前)——Σ结果恒 = total -static func _largest_remainder_split(rows: Array, numer: int, denom: int, total: int) -> Array[int]: - var floors: Array[int] = [] - var fracs: Array[int] = [] - var used := 0 - for row in rows: - var exact: int = int(row.weight) * numer - floors.append(int(exact / denom)) - fracs.append(exact % denom) - used += int(exact / denom) - var order: Array[int] = [] - for i in rows.size(): - order.append(i) - order.sort_custom(func(a: int, b: int) -> bool: - if fracs[a] != fracs[b]: - return fracs[a] > fracs[b] - return a < b) - var out: Array[int] = floors - var rest: int = total - used - assert(rest >= 0 and rest <= order.size(), "最大余数分摊余数越界(不应可达)") - for j in rest: - out[order[j]] += 1 - return out - - -func weighted_pick(rows: Array, r: int) -> String: - var total := _pool_weight(rows) - assert(r >= 0 and r < total, "weighted_pick 取号越域(纯函数域守卫)") - var acc := 0 - for row in rows: - acc += int(row.weight) - if r < acc: - return String(row.reward_id) - _fatal("weighted_pick 未命中(不应可达)") - return "" - - -# aux 解锁门控集(「解锁→进投放池」:未解锁 aux 行不入摇号池。 -# 空集默认 = 全部 aux 不投放(白板/夹具态零投放——模拟读数零扰动;真实侧 main 组装工坊解锁集传入) -var _treasure_unlocked := {} - - -func set_treasure_unlocked(ids: Dictionary) -> void: - _treasure_unlocked = ids - - -func _rollable_rows() -> Array: - # 摇号池 = cell 池正向行 weight>0(负向行纯字典不消费权重;封锁格不在本函数职责域——清单由调用方裁剪) - var out: Array[Dictionary] = [] - for row in db.run.reward.rows: - if String(row.get("cell_type", "")) == RewardTable.TREASURE_ITEM_TYPE: - var iid := String(row.get("effect_id", "")) - if iid == "" or not _treasure_unlocked.has(iid): - continue - if String(row.get("pool_tag", "")) == "cell" and String(row.get("polarity", "")) == "positive" and int(row.weight) > 0: - out.append(row) - return out - - -func _pool_rows(pool_tag: String) -> Array: - var out: Array[Dictionary] = [] - for row in db.run.reward.rows: - if row.pool_tag == pool_tag and int(row.weight) > 0: - out.append(row) - return out - - -func _pool_weight(rows: Array) -> int: - var w := 0 - for row in rows: - w += int(row.weight) - return w - - func _row_by_id(rid: String) -> Dictionary: for row in db.run.reward.rows: if row.reward_id == rid: diff --git a/game/core/reward_roller.gd b/game/core/reward_roller.gd new file mode 100644 index 00000000..57010bac --- /dev/null +++ b/game/core/reward_roller.gd @@ -0,0 +1,245 @@ +## 掷格件:装配期格面资源摇号——池行装配、区间行 roll、权重合成(偏运硬币/board_skew 两路投影)。 +## 自 RewardEconomy 迁出掷格面(architecture v1.29 轴戊②);受理账面(grant/身份查询/快照/retire/ +## polarity_row)留守 RewardEconomy——衔接面=格→奖励身份映射批束(本件产出、受理件接收)。 +## 取号流声明:roll 每批恰开一 reward_roll 流(scope=每次 install 批不变),体序=normalize(行主序 +## +去重,零 RNG)→ 开流 → luck_bias 查询 → 池行装配 → 身份批逐格取号 → 清图批尾 → 区间行追加取号 +## ——取号序与取号次数与拆件前逐字同(同种子逐位复现红线)。 +## 零回指声明:本件零回指 RewardEconomy(fatal 经构造注入 fatal_cb 链转宿主封印通道——BoardInstaller +## 协作件先例);随机只经注入 RngServiceCore(持 economy 传入的同一实例——流身份不变)。 +class_name RewardRoller +extends RefCounted + +var db: ConfigDb +var rng: RngServiceCore +var fatal_cb: Callable = Callable() # 协作件违约→宿主 fatal 封印(BoardInstaller 先例——economy fatal_handler 属性 setter 同拍转注) +var luck_bias_query: Callable = Callable() # 偏运合计查询口(reward_roll 发射归 BattleFxHost 效果层——缝拆形态随件迁移;缺省空回调=恒 0=原行集零扰动) +var board_skew: int = 0 # 图级偏斜旋钮(写点=BoardInstaller 装板决议,全局型行 effect_value 语义=生存族权重下调百分比;0 = 中性 off 态。每板决议时显式重置/写入) +var _treasure_unlocked := {} + +# 稀有正向资源 kinds(底稿 §3——权重投影面,池结构零触碰) +const LUCK_BIAS_KINDS := ["heal", "gp", "pierce", "magic"] +# 偏斜族(需求卡片 §二:进攻族等比上浮/生存族按 v% 下调/白名单豁免恒不动——判定键=cell_type; +# 其余 cell_type(weaken/gp/action 白名单与未来扩型)= 豁免;负向行不入摇号已在 _rollable_rows 滤除) +const SKEW_DOWN_KINDS := ["heal", "defense"] +const SKEW_UP_KINDS := ["attack", "blast", "pierce", "magic"] + + +func _init(bundle: Dictionary) -> void: + for k in ["db", "rng", "fatal_cb"]: + assert(bundle.has(k), "[掷格件] 引用束缺键: %s" % k) + db = bundle.db + rng = bundle.rng + fatal_cb = bundle.fatal_cb + + +# 资格清单摇号(economy.roll_board 装配序委托——守卫与受理复位留守 economy;本函数只承载摇号本体)。 +# 批束键集封闭:{"mapping": Dictionary(cell→reward_id 行主序插入序), "power_roll": Dictionary(cell→int), +# "clear_identity": String, "clear_power": int} +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 订阅命中合计经注入查询口取回); + # 零命中/未接线=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 clear_rows: Array[Variant] = _pool_rows("clear_default") + # ① 身份批:每非封锁格恰一类型身份(雷安同池——同一行集同一权重分布) + var draft := {} + for e in list: + draft[e.cell] = _weighted_pick_row(cell_rows, stream.randi_range(0, _pool_weight(cell_rows) - 1)).reward_id + # ② 清图槽批尾:同流同批判尾摇定清图奖励对象身份(选行算子返表行——就地取 reward_id, + # randi_range 求值位置与拆件前逐位同) + var clear_rid := String(_weighted_pick_row(clear_rows, stream.randi_range(0, _pool_weight(clear_rows) - 1)).reward_id) + # ③ 区间行 power roll(身份定稿后逐格恰一追加取号,定值行零取号;行查就地内联—— + # 局部变量预算内,取号序与取号次数零变化) + var power_roll := {} + for cell in draft.keys(): + if _row_by_id(draft[cell]).has("power_min"): + power_roll[cell] = stream.randi_range(int(_row_by_id(draft[cell]).power_min), int(_row_by_id(draft[cell]).power_max)) + var clear_power := -1 + if _row_by_id(clear_rid).has("power_min"): + clear_power = stream.randi_range(int(_row_by_id(clear_rid).power_min), int(_row_by_id(clear_rid).power_max)) + return {"mapping": draft, "power_roll": power_roll, "clear_identity": clear_rid, "clear_power": clear_power} + + +func set_treasure_unlocked(ids: Dictionary) -> void: + _treasure_unlocked = ids + + +## 资格清单规范化域(开流前——零 RNG):行主序 + 去重(排序责任不外推给 战斗结算;排序键单点 BoardData.row_major_key) +func _normalize_eligible(eligible: Array) -> Array[Dictionary]: + var seen := {} + var list: Array[Dictionary] = [] + for e in eligible: + var c: Vector2i = e.cell + if seen.has(c): + push_warning("[掷格件] 资格清单重复格已去重: %s" % str(c)) + continue + seen[c] = true + list.append({"cell": c}) + list.sort_custom(func(a, b): return BoardData.row_major_key(a.cell) < BoardData.row_major_key(b.cell)) + return list + + +# 偏运硬币权重投影(底稿 §3:只动池内权重、雷安仍同池同分布;luck_bias=0 逐位返回原行集——off 态零扰动) +# 合成序:先经图级偏斜投影再叠件级加法——合成序锚=图级先、件级后 +#(环境事实先定型、玩家构筑叠其上;两投影均为池内权重面,雷安同池与取号次数零变化) +func _biased_cell_rows(bias: int) -> Array: + var base: Array[Variant] = _skewed_cell_rows() + if bias <= 0: + return base + var out: Array[Variant] = [] + for row in base: + if LUCK_BIAS_KINDS.has(String(row.get("cell_type", ""))): + var biased: Dictionary[Variant, Variant] = row.duplicate() + biased["weight"] = int(row.weight) + bias + out.append(biased) + else: + out.append(row) + return out + + +# 图级资源面偏斜投影(整数最大余数法:生存族按 board_skew% 下调,下调量按进攻族 +# 原权重比例整数重分配,合计恒等于原池合计 77;负向行与白名单三行逐位不动)。 +# board_skew<=0 逐位返回原行集 = off 态零扰动第一断言:不进投影域、行集/行序/取号域与基线逐位同。 +func _skewed_cell_rows() -> Array: + if board_skew <= 0: + return _rollable_rows() + var base: Array[Variant] = _rollable_rows() + var families := _skew_families(base) + if families["down"].is_empty() or families["up"].is_empty(): + return base # 结构缺族 = 无偏斜作用面(保守直通——池结构变更须走表加载校验另批) + # 下调总量 = round_half_up(Σ生存×v/100);逐行精确份额 w×v/100 最大余数取整(行序平局) + var drops := _largest_remainder_split(families["down"], board_skew, 100, (2 * _pool_weight(families["down"]) * board_skew + 100) / 200) + # 上浮总量 = 下调总量(守恒),逐行精确份额 drop×w/Σ进攻 最大余数取整 + var gains := _largest_remainder_split(families["up"], drops_total(drops), _pool_weight(families["up"]), drops_total(drops)) + var out := _apply_skew(base, families["down"], families["up"], drops, gains) + assert(_pool_weight(out) == _pool_weight(base), "图级偏斜守恒破(偏斜后合计须等于原池)") + return out + + +## 分族域:生存下调族与进攻上浮族行集拆分(池结构不变式零触碰) +func _skew_families(base: Array) -> Dictionary: + var down: Array[Variant] = [] + var up: Array[Variant] = [] + for row in base: + var k := String(row.get("cell_type", "")) + if SKEW_DOWN_KINDS.has(k): + down.append(row) + elif SKEW_UP_KINDS.has(k): + up.append(row) + return {"down": down, "up": up} + + +## 权重装配域:逐行精确份额增减(下调族扣 drops、上浮族加 gains——守恒 assert 随段迁移) +func _apply_skew(base: Array, down: Array, up: Array, drops: Array, gains: Array) -> Array: + var di := 0 + var ui := 0 + var out: Array[Variant] = [] + for row in base: + var k := String(row.get("cell_type", "")) + var w := int(row.weight) + if SKEW_DOWN_KINDS.has(k): + w -= drops[di] + di += 1 + elif SKEW_UP_KINDS.has(k): + w += gains[ui] + ui += 1 + assert(w > 0, "图级偏斜投影产生非正权重(池结构变更须走另批重审)") + var skewed: Dictionary[Variant, Variant] = row.duplicate() + skewed["weight"] = w + out.append(skewed) + return out + + +func drops_total(amounts: Array[int]) -> int: + var t := 0 + for a in amounts: + t += a + return t + + +# 最大余数整数分摊(行序平局——确定性):把 total 单位按精确份额 w_i×numer/denom 分到 rows +# (floor 后余数单位依次给分数最大者,平局取行序在前)——Σ结果恒 = total +static func _largest_remainder_split(rows: Array, numer: int, denom: int, total: int) -> Array[int]: + var floors: Array[int] = [] + var fracs: Array[int] = [] + var used := 0 + for row in rows: + var exact: int = int(row.weight) * numer + floors.append(int(exact / denom)) + fracs.append(exact % denom) + used += int(exact / denom) + var order: Array[int] = [] + for i in rows.size(): + order.append(i) + order.sort_custom(func(a: int, b: int) -> bool: + if fracs[a] != fracs[b]: + return fracs[a] > fracs[b] + return a < b) + var out: Array[int] = floors + var rest: int = total - used + assert(rest >= 0 and rest <= order.size(), "最大余数分摊余数越界(不应可达)") + for j in rest: + out[order[j]] += 1 + return out + + +# 选行算子(weighted_pick 改造形态——算术逐字:acc 累加/断言/fatal 通道;返回表行,调用侧取 +# row.reward_id;取号算术与次数零变化。单点=本件 _weighted_pick_row,economy 受理面行查留守 _row_by_id) +func _weighted_pick_row(rows: Array, r: int) -> Dictionary: + var total := _pool_weight(rows) + assert(r >= 0 and r < total, "选行算子取号越域(纯函数域守卫)") + var acc := 0 + for row in rows: + acc += int(row.weight) + if r < acc: + return row + _fatal("选行算子未命中(不应可达)") + return {} + + +# aux 解锁门控集(「解锁→进投放池」:未解锁 aux 行不入摇号池。 +# 空集默认 = 全部 aux 不投放(白板/夹具态零投放——模拟读数零扰动;真实侧 main 组装工坊解锁集传入) +func _rollable_rows() -> Array: + # 摇号池 = cell 池正向行 weight>0(负向行纯字典不消费权重;封锁格不在本函数职责域——清单由调用方裁剪) + var out: Array[Dictionary] = [] + for row in db.run.reward.rows: + if String(row.get("cell_type", "")) == RewardTable.TREASURE_ITEM_TYPE: + var iid := String(row.get("effect_id", "")) + if iid == "" or not _treasure_unlocked.has(iid): + continue + if String(row.get("pool_tag", "")) == "cell" and String(row.get("polarity", "")) == "positive" and int(row.weight) > 0: + out.append(row) + return out + + +func _pool_rows(pool_tag: String) -> Array: + var out: Array[Dictionary] = [] + for row in db.run.reward.rows: + if row.pool_tag == pool_tag and int(row.weight) > 0: + out.append(row) + return out + + +func _pool_weight(rows: Array) -> int: + var w := 0 + for row in rows: + w += int(row.weight) + return w + + +func _row_by_id(rid: String) -> Dictionary: + for row in db.run.reward.rows: + if row.reward_id == rid: + return row + return {} + + +func _fatal(msg: String) -> void: + push_error("[掷格件/fatal] " + msg) + if fatal_cb.is_valid(): + fatal_cb.call(msg) # economy fatal_handler 同拍转注(封印传导,不绕第二中止通道) + assert(false, msg) diff --git a/game/core/reward_roller.gd.uid b/game/core/reward_roller.gd.uid new file mode 100644 index 00000000..1d71a0ce --- /dev/null +++ b/game/core/reward_roller.gd.uid @@ -0,0 +1 @@ +uid://ns1oyct41fhk diff --git a/game/core/settlement/combat_segment.gd b/game/core/settlement/combat_segment.gd index 1e9a6ac9..cf1c7b29 100644 --- a/game/core/settlement/combat_segment.gd +++ b/game/core/settlement/combat_segment.gd @@ -31,10 +31,9 @@ func run(ri: int) -> void: round_safe += 1 elif evt.kind == "mine": round_mine = true - oc._damage.inject["dig_streak"] = 0 if round_mine else round_safe - # 蓄力层注入(内容数据驱动扩展批 T3——整备军备「每层下轮首段 +1」/厚积薄发满层 spike/铁壁整备 - # 减伤——三件 fact charge_layers 同读 action_economy;battle 域每轮刷新) - oc._damage.inject["charge_layers"] = oc.action_economy.charge_layers + oc.fx_host().facts_inject["dig_streak"] = 0 if round_mine else round_safe + # 蓄力层不再逐轮注入(效果端口件闭面——fact charge_layers 由宿主 biz_fact 活读 action_economy + # 现值:唯一写点=finish_round_stop 于上轮授予段尾,本轮授予段发射前无第二写点,活读等价) _hero_strike(ri) _enemy_actions(ri) _board_deferred_settle(ri) @@ -61,7 +60,7 @@ func _hero_strike(ri: int) -> void: # ~3 安全格 +1 段穿刺附加(上限 cap;段值 provisional 固定值不乘 atk——台账披露)。施加位 # 紧随主角攻击单开立=迁移前开单位(先于敌方行动单入队——flush 兑现序逐位一致) for h in oc._hits_of(ht.context.get("fx_attack_hits", []) as Array, "attack_segments"): - var segments := mini(int(h.get("cap", 2)), int(oc._damage.inject["dig_streak"]) / maxi(1, int(h.get("divisor", 3)))) + var segments := mini(int(h.get("cap", 2)), int(oc.fx_host().facts_inject["dig_streak"]) / maxi(1, int(h.get("divisor", 3)))) for s in segments: # source_class 用中性名;attack_segments 兑付通道保留泛用面 var ct := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "fx_streak_segments", "连击分段", diff --git a/game/presentation/main.gd b/game/presentation/main.gd index ec7c655a..09caad6f 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -14,11 +14,21 @@ const AUTOLOAD_PATH_RNG := "/root/RngService" const AUTOLOAD_PATH_SAVE := "/root/SaveManager" const AUTOLOAD_PATH_META := "/root/MetaProgression" +const UiAssembler := preload("res://presentation/ui_assembler.gd") const Narrator := preload("res://presentation/battle_narrator.gd") +const TuneSimController := preload("res://presentation/tune_sim_controller.gd") +const DebugSessionScript := preload("res://presentation/debug_session.gd") +const TreasureInteractController := preload("res://presentation/treasure_interact_controller.gd") # 秘宝两步交互 +const CurioInteractController := preload("res://presentation/curio_interact_controller.gd") # 三选一与替换子交互 +const CampShopPresenter := preload("res://presentation/camp_shop_presenter.gd") # 营地/商店投影 +const BoardResolversScript := preload("res://presentation/board_resolvers.gd") # 棋盘解析回调族 const MainUiBuild := preload("res://presentation/main_ui_build.gd") # 装配分步承接件 const MainAdventureFlow := preload("res://presentation/main_adventure_flow.gd") # 冒险流程承接件 const MainOverlayFlow := preload("res://presentation/main_overlay_flow.gd") # 覆盖层流程承接件 const MainDebugFlow := preload("res://presentation/main_debug_flow.gd") # 调试流程承接件 +const MenuScreenScene := preload("res://presentation/scenes/menu_screen.tscn") # 三屏壳场景 +const CampScreenScene := preload("res://presentation/scenes/camp_screen.tscn") +const AdventureScreenScene := preload("res://presentation/scenes/adventure_screen.tscn") var db var run_scope = null # 会话状态承载(run_scope.gd;_ready 即建闲时态,永非 null) diff --git a/game/presentation/views/ui_kit.gd b/game/presentation/views/ui_kit.gd index 3c8d24c6..7ce53dd1 100644 --- a/game/presentation/views/ui_kit.gd +++ b/game/presentation/views/ui_kit.gd @@ -230,7 +230,7 @@ static func motion(dur: float) -> float: # 的字形/无演出回退路径;禁 preload(素材缺位树上直接断言炸脚本,冒烟安全红线) const TEX_HERO_IDLE := "res://assets/art/character/character_hero_idle.png" const TEX_HERO_WALK := "res://assets/art/character/character_hero_walk.png" -const TEX_HERO_PORTRAIT := "res://assets/art/character/character_hero_portrait.png" # B 区站桩立绘·normal 态(素材=原稿分辨率,显示档缩放读 standee_motion PORTRAIT_DISPLAY_H) +const TEX_HERO_PORTRAIT := "res://assets/art/character/character_hero_portrait.png" # B 区站桩立绘·normal 态(48px 档) const TEX_HERO_ATTACK := "res://assets/art/character/character_hero_attack.png" # B 区站桩立绘·attack 态(三态切换) const TEX_HERO_HIT := "res://assets/art/character/character_hero_hit.png" # B 区站桩立绘·hit 态(三态切换) # 敌种立绘(B 区站桩)——文件键 = 数据表主键 enemy_id(圣经 §8.3 命名规范), diff --git a/game/tests/golden/palette_baseline/art_snapshot.png b/game/tests/golden/palette_baseline/art_snapshot.png deleted file mode 100644 index 8e5fb8be7ed3faa186908f5f960b4f832e4bd863..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2572 zcmV+n3iI`eP)&(JVdZS2uNari2~&%k_XA{teqR_kIvk=bI#ste|xQOe{1b^ z2!K%X$6rLxV7U%$Un^lj=xrm?? zRG6o_;X7iZ3{d}+7>__v8Bhj~cfhrwES@_H--il={jd^HLp;$XV2&w(mM#2PahN7Q zYZp&!SD3uGqjuX)!pQSzJHo#BU|6!3QgGt<`Yl}bB%%p82_%Zx_MWT@rJybd)yjkA z3Z=bm@LU*as4rkggfSRU=V1gq`7vz$YNy2k=6UuLc`z**)JF?i?CpV}kV@za(Pu(f<<1k`z9i#Tv%fcetT9BLo`=pH72^di^yKw=R z|3GleGWch~6nLy4C)0zK!OYo+WV!SKxQ^&^1k)=O_F!Q4U3~r_8F%gfnX*KLM?VmU zqbbb^fF!(_Y-Hzga}%Gcm)S((qI8{7cD@Hw>qj`TbvlfwJqbU;uI;iU90Q7k0Rtj( zr<|W`mGOcl;oF+15d2EOl36t*P=*QHLr@7C>}VC^z=IPRb&v4NMI_pJTz?z?v<%)p z4LgeL*>I|09Mi8w^6$kZ(jzTmqekis5E&0s+XOW{vQe}|BB||-G~5V3-ik4qf^krz zABNyq+`mDNUSJ~#)v}`*$r8y3YYf~B+uzWqYzUfRg6H9cWm7*Q2%!lgW1i;0-NH=B zm+X_I?ibzkbi%Ua$P$wzNqJiG8br8;?Q69ICJaOBgPVpL)chZTH3uBz?lp+|@Lj2O zw4nw@Wp3?hhW`6Xdq5cjMM)R%&3>D+|k;SHjYa6+G89e9cd-qqrCfbrmN zz=lDoIl_sSZg@P~R!b~FLiP;;P2eH;bH%Iojv#gz5f5$+jL$|--{L_FkwXO<-2@C> zks`H#&7 zk6_oq>5(bF6jre|9^jixF{vEkdTl?FbTJ-l7fjK!foL5&))sal5SzwVpT?|-u)|0+ z@ZYaVX|zZY=_#&%?`gwxH{8!@>8B^Ba~ogHyT zmhW2y)FE?Hmh6opyc3!0VE04y=vr|(SNstTWhl-S<d8yH_9)E07k;0TLQNSdLH} zDV=d<9N`Iq&$B$SUx(?lV;dNtyh^`%vZQkp$}X2@U5pSf(Fs1l=bu1b7P-liZuAjv z5aXuD#GffSBqIo=0Z(Q@q^5EAa%5`@?inF?N!VpjgWUNSl-&sS%2Z;UoKE5bx30vP z5^>@10g+QUf=EW!t|u@&oDhFmzr-No<;I%t&>4+TaK!6@|>Q@XBJ}KBQV>f;c?4& zd<&A-i1S;Ukd7izDzfy#JU)cP-(fA}hf7dii=YU$M&KZ~EQZsENCj%H0pe_3ilmR~ zyVbs>AHP|;k3$@>sXOKp$WND1Av7)2($C@O9h z_g=u zw*R#d{#Q+)F%y-geBoE9>_y3}i$smUx?yA+x#&eX=(rkwV;K>tYF~{7QILY)iGn zqWYfFgfKsPM}Vl9&h4+@8;^7MQdoMBMD-3X=)#Mu#m~*(!i-XB28`!qZs-AhIW~bO z5Q=mA-%!0t%v#PMeVt}I$}dmjYwyF-ns)}czVSb|W#Lr(Q(;(;RmB*iX@T3Wkeuzr@%nTz|FxwjxRvpmGkk%q8lf_K1x0`U`FH z3N>_&UAy4hDu0%dI>S?{{{;Lx`NC>{SnVm{%1vDM6yIITo96PJ*DUGN+Ac%6~o2VVf{cXZ$O|2BAEw7+vYav~x12(Tt4m!5T?S<{7)&xD z8HCATQBsu1pOQ(}xuUp1Sq`5M0ch##*Of=G7-6s=Q?}>JtORTgevrT5_@yc@l4vH# zUkwsN$mjPvc$S^dVs+zFztz#z3Sub3Yy*~|J*m#XoL_!!&VQ|HWb5kSSR8+i jB#Km@BQ*np*PQ+bS}{J=%uygs00000NkvXXu0mjfN~e>V diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index e7ce2470..bd198255 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -196,6 +196,7 @@ func _suites() -> Array: {"path": "res://tests/unit/presentation/stage_background_test.gd", "expect": 0}, # B 区舞台背景三关变体与连续地面层(V1–V5/V8——ground_h 表单源/board_type 选图/缺图回退/地面几何/光柱退役) {"path": "res://tests/unit/battle_settlement/damage_math_test.gd", "expect": 0}, {"path": "res://tests/unit/battle_settlement/damage_controller_test.gd", "expect": 0}, + {"path": "res://tests/unit/battle_settlement/effect_port_test.gd", "expect": 0}, # 效果端口件(效果端口与掷格分件批 V1–V3——端口单实例/业务类零引擎直连源码锚/事实面闭集两对照) {"path": "res://tests/unit/battle_settlement/roll_multiplier_test.gd", "expect": 0}, # 掷点与乘区宿主(敌攻击类型轴/护甲掷点行为面/乘区接线/伤害类型克制与标错/fresh 池攻击链——批 B 自 smoke_all 迁入) {"path": "res://tests/unit/battle_settlement/seal_polarity_test.gd", "expect": 0}, # 封印与极性宿主(末点 AP 封印/封印不可逆四情况/清图奖励双份与弱形式/刷新 provenance 守卫/极性路由与封印溢价/负向字典/格面资源资格——批 B 自 smoke_all 迁入) {"path": "res://tests/unit/battle_settlement/round_flow_skill_pool_test.gd", "expect": 0}, # 轮内流与技能池宿主(全战斗回放/轮内刷新与 V5 快照单源/遭遇区间骰/结算五段全序/技能池数据面·回放独立·EV 锚·死亡打断·校验拒绝/妨害附加/四桶计数——批 B 自 smoke_all 迁入) diff --git a/game/tests/static_scan.sh b/game/tests/static_scan.sh index 8bf27ac4..a070c044 100644 --- a/game/tests/static_scan.sh +++ b/game/tests/static_scan.sh @@ -314,7 +314,7 @@ fi # get_tree());排除注释行(行首 #/##)与字符串字面量(双引号包裹段内的 await 非代码), # 口径仿 S1/S2 的 grep 排除手法。 # 自检:清单内文件缺位(路径写错/被改名)即判红——防扫面为空静默绿。 -S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd game/core/settlement/battle_refresh_flow.gd" +S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd game/core/settlement/battle_refresh_flow.gd game/core/effect_port.gd game/core/reward_roller.gd" S8_MISS=0 for f in $S8_FILES; do if [ ! -f "$f" ]; then @@ -358,7 +358,7 @@ fi if require_scan_face "S7" "$DIR_CORE"; then # B1 域文件集(迭代 46 卡A 增补 battle_refresh_flow——换图刷新流程件=C13 拆分边界的 B1 域拆件, # 承载旧资源退役/装板编排调用;architecture.md「模块归属」BattleRefreshFlow 行随反哺点落文) -S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd game/core/settlement/battle_refresh_flow.gd" +S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd game/core/settlement/battle_refresh_flow.gd game/core/effect_port.gd game/core/reward_roller.gd" S7_MISS=0 for f in $S7_B1_SET game/core/unit_container.gd game/core/status_container.gd game/core/reward_economy.gd; do if [ ! -f "$f" ]; then @@ -390,7 +390,7 @@ else s7_scan B3 game/core/status_container.gd \ 'notify_battle_start|apply_effect|query_active|decay_round|notify_battle_end' s7_scan B4 game/core/reward_economy.gd \ - 'roll_board|query_identity|query_full_snapshot|grant|grant_clear|retire_mapping|polarity_row|phase|weighted_pick' + 'roll_board|query_identity|query_full_snapshot|grant|grant_clear|retire_mapping|polarity_row|phase' if [ -n "$S7_HITS" ]; then echo "[static_scan] S7 FAIL: B2/B3/B4 API 符号越出 B1 文件集命中 $(printf '%s' "$S7_HITS" | grep -c .) 处(编排唯一性,B1 AC-43)" printf '%s' "$S7_HITS" diff --git a/game/tests/unit/battle_settlement/effect_port_test.gd b/game/tests/unit/battle_settlement/effect_port_test.gd new file mode 100644 index 00000000..85a0afbb --- /dev/null +++ b/game/tests/unit/battle_settlement/effect_port_test.gd @@ -0,0 +1,129 @@ +# tests/unit/battle_settlement/effect_port_test.gd — 效果端口件验收宿主(效果端口与掷格分件批) +# 断言面 = 三套(需求卡片 §9 对照表行 1/2/3 载体): +# V1 端口单实例与真实发射路由(装配链同一实例+attack_commit 经真实战斗流命中可观测) +# V2 业务类零引擎直连(源码契约锚——两业务文件全文零 EffectEngine/EffectContext token) +# V3 伤害域事实面闭集(合成行阳性/阴性对照——载荷携带事实可达、宿主链事实不可达) +# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload, +# ConfigDB 显式实例化构造注入(各套自建 RngService——源套掷点隔离形态)。 +# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/effect_port_test.gd +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const ConfigDBScript := preload("res://core/config_db.gd") +const EffectEngineScript := preload("res://core/effect_engine.gd") + +const TEST_SEED := 42 # 夹具种子(基建值非玩法数值——同种子两跑对比的隔离前提) +const LAYER_COUNTER := "swap_charge_layers" # 整备军备蓄力层计数器(feats.tres charge_rotation 行) + +var _db + + +func _init() -> void: + _db = ConfigDBScript.new() + var load_ok: bool = _db.load_tables("res://data/tables", true) + Kit.check("前置:表加载零 error", load_ok and _db.diagnostics_count("error") == 0) + if load_ok: + _smoke_effect_port_wiring() + _test_business_classes_engine_free() + _test_biz_fact_face_closed() + quit(Kit.finish("effect_port_test")) + + +# ---- 夹具 ---- + +func _enc(feats: Array) -> Dictionary: + return {"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), + "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "feats": feats, "tree_rules": [], + "battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "boss", "adv_bucket": {}} + + +func _orch(seed_v: int): + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(seed_v) + return load("res://core/battle_orchestrator.gd").new(_db, rng) + + +func _hero_attack_value(rows: Array) -> int: + for r in rows: + if String(r.get("type", "")) == "damage" and String(r.get("source_class", "")) == "hero_attack": + return int(r.get("value", 0)) + return -1 + + +# ---- V1 端口单实例与真实发射路由 ---- + +func _smoke_effect_port_wiring() -> void: + print("--- 效果端口:装配链单实例+真实发射路由 ---") + # 充能跑(整备军备在册——attack_commit 订阅行 attack_bonus per 蓄力层计数器) + var orch_c = _orch(TEST_SEED) + var rows_c: Array = [] + orch_c.step_row.connect(func(row): rows_c.append(row)) + Kit.check("V1 begin_battle 受理", orch_c.begin_battle(_enc(["charge_rotation"]))) + # 端口同一性(内部字段直查=受控例外:端口同一性无公开查询面,对照表行 1 注明理由) + var port_d = orch_c.fx_host().damage._port + var port_a = orch_c.action_economy._port + Kit.check("V1 端口在座(damage/action_economy 双绑)", port_d != null and port_a != null) + Kit.check("V1 端口单实例(两业务类持同一 EffectPort)", port_d == port_a) + # 蓄力层预置 3(调试注入两层·数据面粒度——计数器账面直写,与 refresh_feats_test V8 同法) + EffectEngineScript.counter_add(orch_c.fx_host().counters_ctx, LAYER_COUNTER, 3) + # 最小回合:停手即轮末结算(主角先手攻击在战斗段同步兑付——零 await 同步链) + orch_c.minesweeper.settle_batch([{"action": "stop"}]) + var v_c := _hero_attack_value(rows_c) + Kit.check("V1 attack_commit 命中批可观测:主角攻击行在流", v_c >= 0) + Kit.check("V1 命中批经端口路由全处理(attack_bonus 消费行+counter_add set-0 行——层账 3→0)", + EffectEngineScript.counter(orch_c.fx_host().counters_ctx, LAYER_COUNTER) == 0) + # 对照跑(同种子同 enc 零预置——attack_commit 订阅行 when 不通过=零命中零扰动) + var orch_0 = _orch(TEST_SEED) + var rows_0: Array = [] + orch_0.step_row.connect(func(row): rows_0.append(row)) + orch_0.begin_battle(_enc(["charge_rotation"])) + orch_0.minesweeper.settle_batch([{"action": "stop"}]) + var v_0 := _hero_attack_value(rows_0) + Kit.check("V1 对照跑攻击行在流", v_0 >= 0) + Kit.check("V1 delta 读数非零(蓄力层加算进入主角攻击基值)", v_c - v_0 >= 1) + + +# ---- V2 业务类零引擎直连(源码契约锚——theme_projection_test 源码锚惯例)---- + +func _test_business_classes_engine_free() -> void: + print("--- 效果端口:业务类零引擎直连源码锚 ---") + for path in ["res://core/damage_controller.gd", "res://core/action_economy.gd"]: + var src := FileAccess.get_file_as_string(path) + Kit.check("V2 扫描面非空前置:%s" % path, src.length() > 0) + Kit.check("V2 全文零引擎直连 token(EffectEngine/EffectContext):%s" % path, + not src.contains("EffectEngine") and not src.contains("EffectContext")) + + +# ---- V3 伤害域事实面闭集(合成行两对照——damage_taken 真实单据流)---- + +func _test_biz_fact_face_closed() -> void: + print("--- 效果端口:业务发射事实面闭集 ---") + var orch = _orch(TEST_SEED) + Kit.check("V3 begin_battle 受理", orch.begin_battle(_enc([]))) + # 合成行直改数据面(调试注入两层·数据面粒度):effect 段逐字抄现役铁壁 shield 表行(kind/value 零推表); + # rebuild 走持有 id 过滤真实表行——合成行经直改表行进入装载域(订阅面换代为合成行独占,确定性) + var pos_id := "probe_shield_pos" + var neg_id := "probe_shield_neg" + _db.crossrun.feats.rows.append({"feat_id": pos_id, "display_name": "探针盾甲甲", "rarity": "common", + "desc": "V3 阳性对照(hp_ratio 载荷携带)", "triggers": [{"on": "damage_taken", + "when": [{"fact": "hp_ratio", "op": "lte", "value": 100}], "do": {"kind": "shield", "value": 1}}]}) + _db.crossrun.feats.rows.append({"feat_id": neg_id, "display_name": "探针盾甲乙", "rarity": "common", + "desc": "V3 阴性对照(battle_index 闭面外)", "triggers": [{"on": "damage_taken", + "when": [{"fact": "battle_index", "op": "eq", "value": 1}], "do": {"kind": "shield", "value": 1}}]}) + var ctrl = orch.fx_host().damage + # 阳性对照(载荷携带事实——harness 能见命中):合成行独占订阅面(rebuild 持有 id 过滤—— + # 只挂阳性行),board_flat_hero 真实单据流,base_value 足量避开伤害下限保底 + orch.fx_host().rebuild_subscriptions([pos_id], [], []) + var hp0 := int(ctrl.units.hero_unit().get("hp", 0)) + var t_a: Dictionary = ctrl.open_ticket(ctrl.KIND_BOARD_FLAT_HERO, "probe_port", "闭面探测甲", + {"channel": "board", "base_value": 10, "target_rule": "hero", "ri": 1, "origin_cell": orch.minesweeper.hero_pos()}) + ctrl.flush_submit(t_a) + var hp1 := int(ctrl.units.hero_unit().get("hp", 0)) + Kit.check("V3 阳性对照:hp_ratio 载荷 when 通过——承伤减量恰 1(10→9)", hp0 - hp1 == 9) + # 阴性对照(闭面外事实——宿主链答而业务面不答):订阅面换代为阴性合成行独占,同路径零减量=F10 现状形态守卫 + orch.fx_host().rebuild_subscriptions([neg_id], [], []) + var t_b: Dictionary = ctrl.open_ticket(ctrl.KIND_BOARD_FLAT_HERO, "probe_port", "闭面探测乙", + {"channel": "board", "base_value": 10, "target_rule": "hero", "ri": 1, "origin_cell": orch.minesweeper.hero_pos()}) + ctrl.flush_submit(t_b) + var hp2 := int(ctrl.units.hero_unit().get("hp", 0)) + Kit.check("V3 阴性对照:battle_index 不答——零减量(10→10,事实面未扩)", hp1 - hp2 == 10) diff --git a/game/tests/unit/battle_settlement/effect_port_test.gd.uid b/game/tests/unit/battle_settlement/effect_port_test.gd.uid new file mode 100644 index 00000000..d4783f85 --- /dev/null +++ b/game/tests/unit/battle_settlement/effect_port_test.gd.uid @@ -0,0 +1 @@ +uid://b11nytp21w58b diff --git a/game/tests/unit/battle_settlement/variant_placement_test.gd b/game/tests/unit/battle_settlement/variant_placement_test.gd index 0456555c..7ac34fce 100644 --- a/game/tests/unit/battle_settlement/variant_placement_test.gd +++ b/game/tests/unit/battle_settlement/variant_placement_test.gd @@ -221,27 +221,27 @@ func _smoke_global_variant() -> void: Kit.check("确定性:同种子 Boss 图两遍 board_variants/变异格/取号逐位相同(region 布置路径)", _arr_str_eq(bb2.board_variant_ids(), boss1_ids) and bb2.variants == boss1_variants and crng2.variant_calls == boss1_calls) # ④ 资源面偏斜投影(RewardEconomy 纯函数面——守恒/白名单/构造例/合成序) var eco = load("res://core/reward_economy.gd").new(_db, load("res://core/rng_service.gd").new()) - var base_rows: Array = eco._rollable_rows() - var base_sum: int = eco._pool_weight(base_rows) + var base_rows: Array = eco.roller._rollable_rows() + var base_sum: int = eco.roller._pool_weight(base_rows) Kit.check("偏斜基线:正向池现行合计 77(挖掘风险批口径)", base_sum == 77) - eco.board_skew = 0 - var off_rows: Array = eco._biased_cell_rows(0) # 内容数据驱动扩展批 T4:偏运权重改 reward_roll 订阅命中入参(luck_bias 注入位退役) + eco.roller.board_skew = 0 + var off_rows: Array = eco.roller._biased_cell_rows(0) # 内容数据驱动扩展批 T4:偏运权重改 reward_roll 订阅命中入参(luck_bias 注入位退役) Kit.check("off 态零扰动:board_skew=0 行集与基线逐位相同(不进投影域)", off_rows.size() == base_rows.size() and _rows_weights_equal(off_rows, base_rows)) - eco.board_skew = 30 - var on_rows: Array = eco._biased_cell_rows(0) - Kit.check("合计守恒:偏斜后正向池合计 = 77(整数最大余数重归一)", eco._pool_weight(on_rows) == 77) + eco.roller.board_skew = 30 + var on_rows: Array = eco.roller._biased_cell_rows(0) + Kit.check("合计守恒:偏斜后正向池合计 = 77(整数最大余数重归一)", eco.roller._pool_weight(on_rows) == 77) var wmap := _rows_weight_map(on_rows) Kit.check("白名单豁免:weaken/gp/action 三行偏斜后逐位不动", wmap.get("weaken", -1) == 8 and wmap.get("ap_up", -1) == 6 and wmap.get("gp_coin", -1) == 2) 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.board_skew = 30 - var combo_rows: Array = eco._biased_cell_rows(2) # 件级加算 2=偏运硬币行 value(reward_roll bias_rewards 命中面同值) + eco.roller.board_skew = 30 + 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._pool_weight(combo_rows) == 77 + 4 * 2) + 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) - eco.board_skew = 50 - var v50: Dictionary = _rows_weight_map(eco._biased_cell_rows(0)) - Kit.check("域上界 v=50:守恒成立且逐行权重恒正(域 [0,50] 安全)", eco._pool_weight(eco._biased_cell_rows(0)) == 77 and int(v50.get("heal_minor", -1)) > 0) + eco.roller.board_skew = 50 + var v50: Dictionary = _rows_weight_map(eco.roller._biased_cell_rows(0)) + Kit.check("域上界 v=50:守恒成立且逐行权重恒正(域 [0,50] 安全)", eco.roller._pool_weight(eco.roller._biased_cell_rows(0)) == 77 and int(v50.get("heal_minor", -1)) > 0) # wr10 偏运判定收口+注入接线(p5 缝拆)——判定面夹具白盒装载(现网 game/data/tables/ 无产出 # bias_rewards 的数据行,先例=tree_rules_v11_test 读 _triggers);命中数前置写在消息里防空扫描面假绿。 var wrng = load("res://core/rng_service.gd").new() @@ -256,13 +256,13 @@ func _smoke_global_variant() -> void: Kit.check("wr10①判定面:空订阅=%d / 装载后命中 %d 行 → luck_bias_for(4)=%d(期望 0/1/2;命中 0 即判据空转)" % [empty_bias, hit_rows, loaded_bias], empty_bias == 0 and hit_rows == 1 and loaded_bias == 2) Kit.check("wr10②接线面:组装点注入的查询口有效且与宿主查询口同源", - woc._reward_economy.luck_bias_query.is_valid() and woc._reward_economy.luck_bias_query == wfx.luck_bias_for) - woc._reward_economy.board_skew = 0 + woc._reward_economy.roller.luck_bias_query.is_valid() and woc._reward_economy.roller.luck_bias_query == wfx.luck_bias_for) + woc._reward_economy.roller.board_skew = 0 # 取值面防御:未接线时按「取不到偏运」计红而非崩在空回调上(撤销证明⑤态下本套件其余断言仍须跑完) - var qb: Callable = woc._reward_economy.luck_bias_query + var qb: Callable = woc._reward_economy.roller.luck_bias_query var wbias: int = int(qb.call(4)) if qb.is_valid() else -1 - Kit.check("wr10③权重生效面:注入回调取回 %d → 偏运池合计 77+4*2(实测 %d)" % [wbias, woc._reward_economy._pool_weight(woc._reward_economy._biased_cell_rows(wbias))], - wbias == 2 and woc._reward_economy._pool_weight(woc._reward_economy._biased_cell_rows(wbias)) == 77 + 4 * 2) + Kit.check("wr10③权重生效面:注入回调取回 %d → 偏运池合计 77+4*2(实测 %d)" % [wbias, woc._reward_economy.roller._pool_weight(woc._reward_economy.roller._biased_cell_rows(wbias))], + wbias == 2 and woc._reward_economy.roller._pool_weight(woc._reward_economy.roller._biased_cell_rows(wbias)) == 77 + 4 * 2) # 普通图摇号全链路确定性(off 路径)——同种子普通图资源映射逐位相同(golden 对拍另有批前快照) var nrng = load("res://core/rng_service.gd").new() nrng.set_master_seed(42) diff --git a/reports/工程改动报告-工程清理批-2026-11-02.md b/reports/工程改动报告-工程清理批-2026-11-02.md deleted file mode 100644 index 91905693..00000000 --- a/reports/工程改动报告-工程清理批-2026-11-02.md +++ /dev/null @@ -1,75 +0,0 @@ -# 工程改动报告——工程清理批(2026-11-02) - -接收方:待办清单 7 条目(本批完成整条删除——palette_check 基线建档/CAMP_HEADER_BAND 评估/ui_kit 立绘注释/迭代 43 四色墨断言/断言提取器 check_quiet/run_all_gates 02 传参〔已完成漏删〕/main.gd 死 preload)。 - -## 一、批定位 - -工程改动批(用户发令直改,不写卡)。worktree `eng-cleanup`,基点 7e9e57fd,基点即 master 尖端。七件:六件本批落地+一件核查确认已完成(run_all_gates 02 传参)。 - -## 二、逐件落地 - -### 件 1 palette_check 基线建档+双门机检化 - -- 基线目录 `game/tests/golden/palette_baseline/`(图像白名单在册目录)两张条带 PNG:`ui_tokens.png` 92 色(ui_kit.gd 全量 Color8 常量唯一值快照)+ `art_snapshot.png` 1274 色(现役素材 128 张 PNG 唯一色并集快照);并集 1346 色=双门②「新增色 vs 既有色板」的既有色板机检承载物。 -- 新件 `tools/gen_palette_baseline.py`:基线建档与刷新器(token 提取剔除注释行;素材目录零 PNG 拒绝生成空快照)。 -- `tools/palette_check.py` 零代码改动——头注补基线构成与维护纪律、退出码描述校正(单张超 32 色是 WARN 非 FAIL)。 -- 闸机接线:`tools/run_all_gates.sh` 新增 **05b 色板闸**(code 域;--quick 与影响面选闸 SKIP;素材齐盘预检之后段执行)——扫 `game/assets/art` 全目录对基线判新增色近色红。 -- 口径边界(呈报):门①语义检查属设计面人工核对(§4.2 色相语义判域),不入本器执法面;48 token 计数上限不接闸——ui_kit 现役唯一色值 92 与「48 上限」的成员名单口径差归 art-bible §4.1 owner 裁定,本批不自裁。 -- 维护纪律:素材批收尾跑 `python tools/gen_palette_baseline.py` 重生成全套并随批提交——新色过门后须入基线,否则下批扫描把已过门的新色再报新增;两张 PNG 缺一即漏判。 - -### 件 2 extract_checks.py 补 check_quiet 静默通道 - -- `CALL_RE` → `check(?:_quiet)?\(`、`DEF_RE` 同扩(func check / func check_quiet 定义行双拦)、`collect_call` 按实际匹配长度偏移;check_quiet 通道契约 cond 在前,条目 quiet 列记 `channel:check_quiet`。单趟正则无重叠匹配,结构上不双计。 -- 自证三重(当轮实跑):①双计等式——旧两正则匹配数之和 2155 == 新 CALL_RE 2155(全 tests 面);②新旧版条目对拍 1719 → 2133,差 414 条 check_quiet 入账(grep 调用行实数 416,差 2=定义行与注释行不入账,方向自洽);③`--extract` 生成临时清单后 `--diff` 往返对账 PASS 零差异(标签/cond/计数三对齐)。 -- 验证集清单已随验证体系整改阶段 2 退役,`--diff` 无存档参照——对账自证以临时清单往返承载,语义等同。 -- 随批换代 `design/engineering/test-conventions.md` §5.2 工具面局限备案句(2026-10-27 立的「断言消息不进扫描面——验收尾缀 grep 对该通道失效」已消除,留存会漂移);文档设计面仅此一句现状描述更新,申报。 - -### 件 3 意图徽标四色墨机检断言 - -- `tools/render_sample_sheet_v3.py` `run_screen_checks` 增 **S1b**:意图徽标四色墨(攻击/增益/减益/蓄力)对 PANEL_DATA 垫牌(#E9D9B8,与绘制矩形 fill 同源)≥4.5:1 色值对表断言,四墨值即脚本意图色表四值,零新增色。 -- 当轮真实读数:攻击 5.62/增益 5.10/减益 4.97/蓄力 5.21——与独立审计复算区间 4.97–5.62 逐位一致。 -- 样张机检 31/31 全绿。V10 改前基线对拍图属 artwork(不入 git)环境件,worktree 用 `--save-baseline` 重建(本批零素材零绘制变更,基线等值)。 - -### 件 4 main.gd 拆件遗留死 preload 清理 - -- 逐条 grep 复核(登记纪律):死码 **10 条**——登记 8 条,复核多出 2 条(三场景 preload 中 AdventureScreenScene 登记时另有消费面、UiAssembler 登记时在役;现全零)。UiAssembler 唯一非定义出现=组合根接线段注释提及类名(非常量使用)。 -- 7 具名(TuneSimController/DebugSessionScript/TreasureInteractController/CurioInteractController/CampShopPresenter/BoardResolversScript 等)消费面已迁 `main_ui_build.gd:9-13`(拆件自持 preload)——main.gd 旧常量=拆件残留重复声明;三场景消费面=system_test.gd `load()` 与 main.tscn,删常量不触。 -- 删除 10 条 const preload,保留 Narrator/MainUiBuild/MainAdventureFlow/MainOverlayFlow/MainDebugFlow 五条在役。 - -### 件 5 过时注释换代 - -- `game/presentation/views/ui_kit.gd` TEX_HERO_PORTRAIT 行注释「48px 档」→「素材=原稿分辨率,显示档缩放读 standee_motion PORTRAIT_DISPLAY_H」(迭代 48 显示档批现行口径)。 -- `tools/path_check.py` 头注删「· 立绘定档批 2026-10-07」日期批注记(变更叙事黑名单类);47 行键集指名核实现行仍在役(standee_asset_check 现行 import STANDEE_DISPLAY_H),不改。 - -### 件 6 ui_kit CAMP_HEADER_BAND 令牌退役评估 - -现役消费面 4 处逐一核过:`level_map_screen.gd:107/144`(关卡屏头带与地图条)、`shop_overlay_view.gd:49`(商店头带)、`ui_kit.gd` KeystoneBadge 工厂(钥石徽描边——消费方=营地覆盖层+状态面板钥石余额行)。 - -**结论**:「退役删除」不成立——4 处在用且全是活面板绘制面;营地域柜台头带语义岗已随迭代 47 四柜台覆盖层退役,令牌现役值 #6E4526 只剩材质棕底色语义(木色头带/徽章描边)。**处置=改名中性化**(脱 CAMP_ 前缀,名随现役语义,如 WOOD_BAND 一族)归 ui_kit 令牌批顺带;改名零值变化零渲染变化,届时逐点核后动。维持原登记口径「退役归 ui_kit 令牌批顺带处置、非独立强删」。 - -### 件 7 run_all_gates 02 关联档传参(核查确认已完成) - -`d32ff342`(工程小件批·件 1)已落两案中的「解析器补逗号切分」案:`run_all.gd` 选跑解析补 `split(",")`,闸脚本逗号串传参与之配套,孤立验证四套件全绿。本批零改动,条目属完成漏删。 - -## 三、待办清单 - -删 7 条(整条删除,状态机出口):palette_check 基线建档/CAMP_HEADER_BAND 评估/ui_kit 立绘注释/迭代 43 移交小件(①能力清单补行已随 2026-10-31 写卡批落位、②四色墨断言本批落)/断言提取器改版/run_all_gates 02 传参(d32ff342 已完成)/main.gd 死 preload。流水线闸复验 PASS(33 条目/49 总表行)。 - -## 四、窄验读数(worktree 内当轮实跑) - -| 验证 | 读数 | -|---|---| -| palette_check 带基线扫 art 全目录 | 128 张 0 红 0 新增色 | -| presentation 单元套件(main.gd/ui_kit.gd 编译面) | 17 suites 全绿 | -| 冒烟 run_smoke | 15 项 0 失败,三闸全过 | -| 样张 render_sample_sheet_v3 | 31/31 全绿(含新 S1b) | -| path_check | 0 处问题 | -| extract_checks | 双计等式过/1719→2133/往返对账 PASS | -| pipeline_gate | PASS(33 条目/49 总表行/交叉核 3 链) | - -全量回归归合回后本地 CI(post-merge 钩子自动跑,含新 05b 色板闸)。 - -## 五、边界与移交 - -- 48 token 计数上限的机检承载与门①语义检查不属本批(口径归 art-bible owner),呈报待裁。 -- 05b 色板闸的基线维护义务(素材批收尾刷新)已写进 palette_check.py 头注与 gen_palette_baseline.py 头注,闸机注释同步点名。 diff --git a/tools/capability_audit.py b/tools/capability_audit.py index 6a29592a..9130eeae 100644 --- a/tools/capability_audit.py +++ b/tools/capability_audit.py @@ -45,6 +45,7 @@ STATUS_VALUES = ("已覆盖", "部分覆盖", "未覆盖") # 验收标准节豁免:无可写验收面的在册件(逐件附理由;新豁免须经用户裁) ACCEPT_EXEMPT = { + "锻造工坊.md": "工程化系统,验收面由天赋树与元进度两篇承接,本批迁移实现事实后复核", "生涯.md": "P6 后置未实装,无现成可观测面;实装批补验收标准节", } diff --git a/tools/extract_checks.py b/tools/extract_checks.py index c1344161..5183eeb6 100644 --- a/tools/extract_checks.py +++ b/tools/extract_checks.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- r"""extract_checks.py — 断言静态提取器(验证集清单生成与出口对账) -机器提取宿主脚本的全部静态断言调用点(每个调用点记录:所在文件、所在函数、 +机器提取宿主脚本的全部静态 check( 调用点(每个调用点记录:所在文件、所在函数、 行号、标签字符串字面量原文、cond 表达式原文),提供两种模式: 用法: @@ -19,11 +19,10 @@ r"""extract_checks.py — 断言静态提取器(验证集清单生成与出口 断言,对账时以「新增」出现(非迁移面),出口判定以显式模式为准。 提取口径: - - 静态断言调用点,三通道都认:smoke/系统宿主的 check(cond, label)(cond 在前)、 - 单元/集成宿主的 Kit.check("label", cond)(点前缀=标签在前)、静默收集通道 - Kit.check_quiet(cond, label)(cond 在前,条目 quiet 列记 channel:check_quiet); - `_check(` 等同名前缀调用非标准通道,不取。 - - 调用点可跨行:从调用名起括号配平归集完整实参文本(() [] {} 三种括号都计深度); + - 静态 check( 调用点,两种通道都认:smoke/系统宿主的 check(cond, label)(cond 在前) + 与单元/集成宿主的 Kit.check("label", cond)(点前缀=标签在前);`_check(` 等同名 + 前缀调用非标准通道,不取。 + - 调用点可跨行:从 `check(` 起括号配平归集完整实参文本(() [] {} 三种括号都计深度); 字符串字面量内的括号/逗号/井号、行尾注释、转义序列均不参与结构判定(码位掩码单趟预扫)。 - cond 摘要=cond 原文跨行折叠为单行(连续空白并一空格);标签原文=标签实参里的字符串 字面量逐字保真(含引号与转义序列原样,禁改写);标签实参若带格式化/拼接后缀 @@ -65,8 +64,8 @@ def default_sources(): files.extend(sorted(p.rglob("*.gd"))) return sorted(files) -CALL_RE = re.compile(r"check(?:_quiet)?\(") -DEF_RE = re.compile(r"^\s*(static\s+)?func\s+check(?:_quiet)?\s*\(") +CALL_RE = re.compile(r"check\(") +DEF_RE = re.compile(r"^\s*(static\s+)?func\s+check\s*\(") FUNC_RE = re.compile(r"^(static\s+)?func\s+([A-Za-z_][A-Za-z0-9_]*)") LITERAL_RE = re.compile(r'"((?:[^"\\]|\\.)*)"') OPENERS = "([{" @@ -189,9 +188,9 @@ def split_args(body): return args -def collect_call(joined, start, call_len): - """从调用名起点起括号配平归集调用。返回 (body, end_pos);不配平返回 None。""" - i = start + call_len +def collect_call(joined, start): + """从 check( 的 start 起括号配平归集调用。返回 (body, end_pos);不配平返回 None。""" + i = start + len("check(") depth = 1 in_str = False parts = [] @@ -229,7 +228,7 @@ def collect_call(joined, start, call_len): def extract(src_path: Path): - """提取单文件全部静态断言调用点(check/check_quiet)。返回(条目列表, 函数表),条目保持源码顺序。""" + """提取单文件全部静态 check( 调用点。返回(条目列表, 函数表),条目保持源码顺序。""" src = src_path.read_text(encoding="utf-8") lines = src.split("\n") funcs = func_table(lines) @@ -248,32 +247,28 @@ def extract(src_path: Path): entries = [] for m in CALL_RE.finditer(joined): if not mask[m.start()]: - continue # 字符串字面量内 / 注释里的调用名字样 + continue # 字符串字面量内 / 注释里的 check( 字样 ln = line_of(m.start()) if DEF_RE.match(lines[ln - 1]): - continue # func check / func check_quiet 定义行 + continue # func check 定义行 prev = joined[m.start() - 1] if m.start() > 0 else "" if prev and (prev.isalnum() or prev == "_"): continue # `_check(` 等同名前缀——非标准通道 - is_quiet = m.group(0).startswith("check_quiet") - if is_quiet: - label_first = False # Kit.check_quiet(cond, label)——cond 在前 - else: - label_first = prev == "." # Kit.check("label", cond)——点前缀=标签在前 - body, end_pos = collect_call(joined, m.start(), len(m.group(0))) + label_first = prev == "." # Kit.check("label", cond)——点前缀=标签在前 + body, end_pos = collect_call(joined, m.start()) if body is None: - raise SystemExit("[extract_checks] FAIL:%s 行 %d 起的断言调用括号不配平到文件尾——先查解析缺口" + raise SystemExit("[extract_checks] FAIL:%s 行 %d 起的 check( 括号不配平到文件尾——先查解析缺口" % (src_path.name, ln)) args = split_args(body) if len(args) not in (2, 3): raise SystemExit( - "[extract_checks] FAIL:%s 行 %d 的断言调用拆出 %d 个实参(预期 2/3)——解析缺口,先查再跑" + "[extract_checks] FAIL:%s 行 %d 的 check( 拆出 %d 个实参(预期 2/3)——解析缺口,先查再跑" % (src_path.name, ln, len(args))) if label_first: label_expr, cond = args[0], args[1] else: cond, label_expr = args[0], args[1] - quiet = "channel:check_quiet" if is_quiet else (args[2] if len(args) == 3 else "") + quiet = args[2] if len(args) == 3 else "" lm = LITERAL_RE.search(label_expr) if lm is None: continue # 标签实参无字面量(wrapper 直传变量——既有套件先例 reward_overlay_test _req) @@ -341,7 +336,7 @@ def render_list(entries, funcs_by_src, src_names, carry=None) -> str: out.append("# 验证集清单(断言快照·冻结)") out.append("") out.append("> 生成日期:%s · 基点 commit:`%s` · 源:%s" % (date.today().isoformat(), base_commit(), "、".join(src_names))) - out.append("> 提取口径:静态 `check(`/`check_quiet(` 调用点,括号配平归集(可跨行);缺省源=三宿主遍历") + out.append("> 提取口径:静态 `check(` 调用点,括号配平归集(可跨行);缺省源=三宿主遍历") out.append("> (unit/**+integration/**+system_test.gd)。本基点实测 %d 个静态调用点;行级" % len(entries)) out.append("> `grep -c \"check(\"` 是另一口径(含 `func check(` 定义行与注释行),两口径各自浮动,") out.append("> 禁互相换算(tests/README「断言计数双口径」)。运行时计数大于静态计数=循环展开条目。") diff --git a/tools/gen_palette_baseline.py b/tools/gen_palette_baseline.py deleted file mode 100644 index 78d58086..00000000 --- a/tools/gen_palette_baseline.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -"""色板基线生成器——palette_check 双门②基线的建档与刷新件。 - -产出两张 1×N 条带 PNG 落 game/tests/golden/palette_baseline/(图像白名单在册目录): - ui_tokens.png = ui_kit.gd 全量 Color8 常量唯一色值(token 板现役值快照) - art_snapshot.png = 现役素材(game/assets/art 递归)全量唯一色快照(像素板+各屏环境子板) - -palette_check --baseline 取目录下全部 PNG 的唯一色并集为「既有色板」;基线缺一张 -即漏判——基线刷新必须重生成全套。维护纪律:素材批收尾跑一次本器刷新并随批提交 -(新色过门②后须入基线,否则下批扫描把已过门的新色再报新增)。 - -用法: - python tools/gen_palette_baseline.py [--repo <仓库根>] -退出码: 0 成功 / 2 前置缺位(ui_kit.gd 或素材目录缺 / 素材零 PNG) -""" -import argparse -import os -import re -import sys - -from PIL import Image - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from palette_check import palette_of # 排除全透明的唯一色提取——与检查器同口径 - - -def strip_comments(text): - """剔 GDScript 行注释与字符串字面量,防注释里举的色值被误收进 token 快照。""" - out = [] - for line in text.split("\n"): - i = line.find("#") - if i != -1: - line = line[:i] - out.append(line) - return "\n".join(out) - - -def token_cols(ui_kit_path): - """ui_kit.gd 全量 Color8 唯一色值集合。""" - text = strip_comments(open(ui_kit_path, encoding="utf-8").read()) - vals = re.findall(r"Color8\((\d+),\s*(\d+),\s*(\d+)\)", text) - return sorted(set((int(r), int(g), int(b)) for r, g, b in vals)) - - -def scan_dirs(roots): - """递归扫 PNG,唯一色并集(含像素位置首见序不保——并集无序)。""" - cols = set() - count = 0 - for root in roots: - for dirpath, _, names in os.walk(root): - for fn in sorted(names): - if fn.lower().endswith(".png"): - c, _n = palette_of(os.path.join(dirpath, fn)) - cols |= c - count += 1 - return cols, count - - -def write_strip(cols, out_path): - """1×N 条带 PNG(一色一像素列)。""" - cols = sorted(cols) - im = Image.new("RGB", (len(cols), 1)) - for i, c in enumerate(cols): - im.putpixel((i, 0), c) - im.save(out_path) - return len(cols) - - -def main(): - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8", errors="replace") - ap = argparse.ArgumentParser(description="色板基线生成器(token∪素材快照)") - ap.add_argument("--repo", default=".", help="仓库根(含 game/ 与 tools/)") - args = ap.parse_args() - - ui_kit = os.path.join(args.repo, "game", "presentation", "views", "ui_kit.gd") - artdir = os.path.join(args.repo, "game", "assets", "art") - outdir = os.path.join(args.repo, "game", "tests", "golden", "palette_baseline") - if not os.path.isfile(ui_kit): - print("[gen_palette_baseline] FAIL:ui_kit.gd 缺:%s" % ui_kit) - return 2 - if not os.path.isdir(artdir): - print("[gen_palette_baseline] FAIL:素材目录缺:%s" % artdir) - return 2 - os.makedirs(outdir, exist_ok=True) - - tcols = token_cols(ui_kit) - n_t = write_strip(tcols, os.path.join(outdir, "ui_tokens.png")) - - acols, npng = scan_dirs([artdir]) - if npng == 0: - print("[gen_palette_baseline] FAIL:素材目录零 PNG(素材未同步)——拒绝生成空快照") - return 2 - n_a = write_strip(acols, os.path.join(outdir, "art_snapshot.png")) - - union = set(tcols) | set(acols) - print("[gen_palette_baseline] 已生成:ui_tokens.png %d 色 / art_snapshot.png %d 色" - "(素材 %d 张 PNG);基线并集 %d 色 → %s" - % (n_t, n_a, npng, len(union), outdir)) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tools/palette_check.py b/tools/palette_check.py index 9a0575ee..b1cc3d87 100644 --- a/tools/palette_check.py +++ b/tools/palette_check.py @@ -17,21 +17,12 @@ 另:必须排除全透明像素 (0,0,0,0)(同standee_asset_check 的坑)。 -基线(双门②「新增色 vs 既有色板」的既有色板机检承载物): - 目录 = game/tests/golden/palette_baseline/,两张条带 PNG(1×N,一色一像素列): - ui_tokens.png = ui_kit.gd 全量 Color8 token 快照(token 板现役值) - art_snapshot.png = 现役素材全量唯一色快照(像素板+各屏环境子板) - 两张并集=「既有色板」。缺一张即漏判——基线刷新必须重生成全套。 - 门①语义检查属设计面人工核对(§4.2 色相语义判域),不在本器执法面。 - 用法: - # 主用法(闸机 05b 色板闸承载):扫目录,新增色相对 --baseline 判定 + # 主用法:扫目录,新增色相对 --baseline 判定 python tools/palette_check.py <目录> --baseline <基线目录> # 无基线:只出读数(单张色数/ 近色对计数),不判红 python tools/palette_check.py <目录> - # 基线建档与刷新:重扫现役素材∪token 重生成基线条带(素材批收尾动作,随批提交) - python tools/gen_palette_baseline.py -退出码: 0 通过 / 1 有红灯(仅新增色近色不过门②时) +退出码: 0 通过 / 1 有红灯(仅新增色违规或超单张上限时) """ import os import sys diff --git a/tools/path_check.py b/tools/path_check.py index eba52dbf..60f979b5 100644 --- a/tools/path_check.py +++ b/tools/path_check.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""B3 路径核对器(圣经 §8.3② 挂点实现)。 +"""B3 路径核对器(圣经 §8.3② 挂点实现 · 立绘定档批 2026-10-07)。 职责:扫描 asset-path-map.md 声明的全部路径 + 代码侧素材常量, 报「悬空(声明存在但盘上无)/ 退役未清(仍引用已退役命名)」。 diff --git a/tools/render_sample_sheet_v3.py b/tools/render_sample_sheet_v3.py index 744f007e..f540deb8 100644 --- a/tools/render_sample_sheet_v3.py +++ b/tools/render_sample_sheet_v3.py @@ -1209,18 +1209,6 @@ def run_screen_checks(scr, zones, board_origin, frames_strip, frames_meta, s6_im weakest = min(ratios, key=lambda r: r[1]) check("S1", "整屏①全部文字对直接背景 ≥4.5:1(WCAG AA)", all(r[1] >= 4.5 for r in ratios), "最弱 %s=%.2f;全部 %s" % (weakest[0], weakest[1], ratios)) - # S1b 意图徽标四色墨对 PANEL_DATA 垫牌 ≥4.5:1(色值对表——镜像实装同值;垫牌=ui_kit PANEL_DATA #E9D9B8, - # 与 draw_standee 徽标垫矩形 fill 同源;四墨=意图色表 damage/buff/debuff/charge 四值) - panel_data = (233, 217, 184) - intent_inks = [("攻击墨", (150, 44, 36)), ("增益墨", (30, 90, 150)), - ("减益墨", (122, 66, 148)), ("蓄力墨", (116, 80, 11))] - iratios = [(n, round((max(wcag_lum(a), wcag_lum(panel_data)) + 0.05) - / (min(wcag_lum(a), wcag_lum(panel_data)) + 0.05), 2)) - for n, a in intent_inks] - iweak = min(iratios, key=lambda r: r[1]) - check("S1b", "意图徽标四色墨对 PANEL_DATA 垫牌 ≥4.5:1(WCAG AA·色值对表)", - all(r[1] >= 4.5 for r in iratios), - "最弱 %s=%.2f;全部 %s" % (iweak[0], iweak[1], iratios)) # S2 整屏②明度三档带 + 世界底层两子带(§2;舞台=实测区域读数,格底=实测板面;WCAG 0–1 域) stage_l = _region_wcag(scr, (300, 190, 600, 220)) bx, by = board_origin diff --git a/tools/run_all_gates.sh b/tools/run_all_gates.sh index 341b4efe..4c38787b 100644 --- a/tools/run_all_gates.sh +++ b/tools/run_all_gates.sh @@ -413,14 +413,6 @@ if [ $QUICK -eq 1 ] || [ $NONBEHAVIOR -eq 1 ]; then else gate "05 渲染闸" "render.log" bash "$REPO/tools/render_gate.sh" "$GAME" fi -# ── 5b. 色板闸(§4.1 新色双门②:素材新增色 vs 基线〔token∪素材快照〕近色判红; -# 门①语义检查属设计面人工核对不入机检;基线刷新=tools/gen_palette_baseline.py,素材批收尾动作)── -if [ $QUICK -eq 1 ] || [ $NONBEHAVIOR -eq 1 ]; then - NAMES+=("05b 色板闸"); EXITS+=("SKIP"); LOGS+=("") - echo ""; echo "[gates] 05b 色板闸 → SKIP(--quick 或 影响面选闸:素材色板面不随测量面改动开闸)" -else - gate "05b 色板闸" "palette.log" "$PY" "$REPO/tools/palette_check.py" "$GAME/assets/art" --baseline "$REPO/game/tests/golden/palette_baseline" -fi fi # ══ 文档域(6–8e)══ if in_domain docs; then diff --git a/待办清单.md b/待办清单.md index 268cd45b..a7ac2e85 100644 --- a/待办清单.md +++ b/待办清单.md @@ -2,6 +2,22 @@ 本清单是流水线的唯一登记处:以后才做的事在此登记一条,随流水线段推进改状态,做完整条删除(条目记进度,不是各段的开工前置);各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据含可选优先级,正文=实况/缺口/改动面/状态四属性)与状态机以 [doc-conventions §12.1](design/engineering/doc-conventions.md) 为权威。 +#### palette_check 基线建档与双门机检化(2026-11-02 登记,来源=批次移交) + +**实况**:证据=迭代 47 实现报告 §7.2(已按吸收即死退役,git 提交 3443890d 可考)——卡④ V10 素材级读数:15 张素材 palette_check 只读模式 0 红灯 0 新增色;但 48 令牌 baseline 目录在库缺位(机检无对照基线),§4.1 两道门(语义检查+RGB 三通道差)的双门机检化缺承载。归工程改动批:建 baseline 目录+闸件双门判据落地。 + +**改动面**:工具代码 + +**状态**:待执行 + +#### ui_kit CAMP_HEADER_BAND 令牌退役评估(2026-11-02 登记,来源=批次移交) + +**实况**:证据=迭代 47 审计记档 2 订正在案(git 提交 3443890d 可考)——营地域柜台头带消费面已随迭代 47 四柜台覆盖层退役注销(ui_contrast 令牌表四行随形态注销),现役消费面仅剩 keystone_badge 工厂与关卡屏/商店两处非营地域用途。处置口径(实现批备案):退役归 ui_kit 令牌批顺带处置、非独立强删;届时逐点核消费面后删令牌或改名。 + +**改动面**:游戏代码 + +**状态**:待执行 + #### hobgoblin 内容三件套补齐(2026-11-01 登记,来源=审计发现) **实况**:敌人表 hobgoblin 行(寻址键 (1, battle, 3))数值完整在役、战斗正常运行,但内容三件套未交付——立绘、敌种语义模板、敌人系统.md 敌种表行均缺,呈现面因语义模板未成文无定义可依。登记凭据=[敌人系统](design/gdd/systems/战斗循环/敌人系统.md) v1.1「hobgoblin 缺口登记」节(2026-10-13 迭代 24 反哺④)。 @@ -22,6 +38,22 @@ **状态**:待文档设计 +#### ui_kit.gd 立绘注释陈旧顺带换代(2026-11-02 登记,来源=批次移交) + +**实况**:证据=迭代 48 实现报告§六与验收审计观察项(已退役,git 提交 4c94eee3 可考)——`ui_kit.gd` TEX_HERO_PORTRAIT 行注释仍写「48px 档」(现行=原稿分辨率+显示档缩放),`path_check.py` 一处注释表述也已改指外;零行为,随下次触碰该两件的批次顺带换代(顺带件,不独立出批)。 + +**改动面**:游戏代码 + +**状态**:待执行 + +#### 迭代 43 批后移交小件——能力清单补行+四色墨断言(2026-10-30 登记,来源=批次移交) + +**实况**:证据=迭代 43 反哺点清单 9/13(已按吸收即死退役,git 历史可考)。逐条:①**能力清单补行**——game/tests/README.md 能力清单无战斗一屏样张批 V5–V9 代表用例行(卡片成文早于清单 2026-10-29 改版定稿),归写卡段随下一写卡会话按 test-conventions §8.3 补行(能力=站桩呈现定值与素材绑定;代表用例=`game/tests/unit/presentation/standee_present_values_test.gd:_init`,真实入口=`UiKit.load_tex()`/`StandeeBody.name_line_probe()`),并同步覆盖域表「战斗表现.md」行。②**意图徽标四色墨补机检断言**——样张脚本 S1 文字对比为绘图用色值对表,意图徽标四色墨(对 PANEL_DATA 垫牌)未入 CHECKS;独立审计复算 4.97–5.62 全 ≥4.5 事实成立仅缺机检断言,`tools/render_sample_sheet_v3.py` CHECKS 补一条对表断言(values 全在册,零新增色)。**2026-10-31 写卡会话落位**:①能力清单补行已随「站桩名字行提档与地台色合并批」卡片成文同批入表(能力行+覆盖域表战斗表现.md 行);②四色墨断言仍待执行。 + +**改动面**:游戏代码+工具代码 + +**状态**:待执行 + #### 架构审查优化讨论——全库架构审计读数的逐条裁决与整改分批(2026-10-29 登记,来源=审计发现) **实况**:2026-10-29 全库架构静态审计完成,读数见 reports/审计-全库架构审查-2026-10-29.md(七节:总评/健康面/P0/P1/P2/待裁点/取证口径;零测试执行零引擎运行,全部结论带 文件:行号)。健康面已逐条核干净:Core 零 UI 八项判据全零命中、core 内 RNG 单点 100% 成立、字符串式信号连接全库 0 处、core 层零超长函数、19 表 ↔ 19 schema 一一对应、无循环 preload。问题集中在三条「写了没闸守」的纪律被实际破坏:①数值单一事实源被 18 处硬编码击穿,最危险两处是 `presentation/main.gd:787` 与 `:790` 把 `rap_refresh_cost=8` 与 `refresh_cap_per_round=2` 写死进提示文案(紧邻 783 行是正确读表的),表改文案不改、玩家直接受错误数字;另有 4 组代码自标「机制结构常量/产出约束」的字面量(`node_map_generator.gd:12` `MAX_WIDTH`、`action_economy.gd:14` `CHARGE_LAYER_CAP`、`board_data.gd:9` `SPAWN_AREA_CELLS`、`node_map_generator.gd:83-94` 域字面量)是否入表待裁。②`game/sim/` 与 `game/core/` 四组平行实现——扫雷推理引擎双份(`board_generator.gd` `measure_inferability` 族 vs `auto_player.gd` `_deduce` 族,输入集口径不同:前者含 safe_set oracle、后者 no-peek)、伤害掷点期望重算(`damage_probe.gd:196-201`)、档位排序与驱动循环双份(`level_autoplay.gd` vs `treasure_item_table.gd:44` 与 `battle_driver.gd:17`)、FakeDB 伪造表喂生成器——直接威胁「模拟读数 = 游戏真实数值」这一数值验证前提。③RNG 单点纪律在 core 外失守(sim 4 处/probe 1 处/tests 3 处自建 `RandomNumberGenerator`,均显式定种、确定性尚存),而 `static_scan.sh` 的 S1/S2 扫描面只含 `DIR_CORE` + `DIR_PRES`。守卫面另有三缺口:S8 是 9 文件显式清单(新增结算文件忘登记=静默不扫,缺位自检只防删改不防增)、S9 类型闸与 S13 质量闸扫描面止步于 core+data+presentation(sim/probe/tests 三层除 S11/S12 外零闸)、S13 三处豁免常态化到最大户文件(`battle_orchestrator`/`main.gd`/`tune_sim_controller`);另有四个 core 文件零测试点名(`attune_loadout`/`battle_fx_host`/`meta_state_store`/`slot_store`)。P2 级:`core/settlement/` 五段类直读写编排器私有成员 20+ 处(`oc._damage.*`/`oc._enemy_system.*`,GDScript 无编译期检查、改名静默断裂);架构文档三处漂移(`architecture.md:110` 登记的 `core/hero_ledger.gd` 盘上不存在且 `hero_ledger_test.gd:171` 显式断言它必须不存在、`:135` 行数过期、EC8 热重载门后 `ConfigDb.reload()` 零调用方且走 `load()` 拿不到磁盘新值);class_name 级双向依赖(`core/config_db` → `data/schemas/board_gen_table` → `core/board_data.SPAWN_AREA_CELLS`)与 `NODE_TYPES` 词表双份;`MetaProgression` 是唯一用 `get_node("/root/...")` 取依赖、唯一跨 Autoload 直调的单例。 @@ -55,18 +87,29 @@ **状态**:文档设计已闭合 +#### 断言提取器改版——覆盖 Kit.check_quiet 静默通道(2026-10-29 登记,来源=会话提及) + +**实况**:`tools/extract_checks.py:67` 的 `CALL_RE = check\(` 只认即时通道,静默收集通道 `Kit.check_quiet(cond, desc)` 静态站点实测 407 个在提取口径外(证据=`grep -rn "check_quiet(" game/tests --include="*.gd"` 实数;test-conventions §5.2 已备案该工具局限)。能力清单以 `文件:函数` 为锚,不受影响;但要把断言面全账纳入机器对账(重建账本或全量断言面审计),须先改版提取器:识别 `check_quiet(` 并按反参序(cond 在前)提取标签与 cond。改版后须以全量套件跑一遍 `--diff` 自证提取面覆盖、且 `Kit.check(` 与 `check_quiet(` 不双计。 + +**改动面**:工具代码 + +**状态**:待执行 + #### 能力清单覆盖缺口补齐——六类无断言面或设计未定项(2026-10-29 登记,来源=会话提及) -**实况**:实际情况=2026-11-02 设计讨论闭合——④除名(用户裁:死亡灰化/碎裂属纯表现锦上添花,探针+人工目检已是合规终态,呈现与 battle.md 承诺行零触碰);范围收窄:④除名后余五类,其中②多语言半边(P1 功能缺口,归 owner 文档待设计问题清单)、③音频(P1 后置)、⑤数值落带半边(归 M4 数值批)三块延期不在本条,可写卡面=①增益状态结算链、②减动效开关、⑤机制门控、⑥锻造工坊结构四处。关联文档: -- `game/tests/README.md` 覆盖域表 25 篇对照行(2026-10-29 立,缺口证据) -- `design/gdd/systems/跨局循环/锻造工坊.md` v0.11(2026-11-02,验收标准节九条判据落文——⑥写卡输入合同) -- `design/gdd/systems/战斗循环/增益状态.md` v1.1(2026-10-26,①结算链规则定稿) -- `design/gdd/systems/冒险循环/难度曲线与Boss.md` v1.2(⑤机制门控规则定稿;数值带 provisional 归 M4) -- `design/gdd/systems/表现与支撑/设置与本地化.md` v1.1(②减动效开关与呈现承诺现行口径) +**实况**:证据=`game/tests/README.md` 覆盖域表 25 篇对照行(2026-10-29 立)。以下部分覆盖/未覆盖项作为缺口指针,逐项:①增益状态——只验契约词表与布尔受理,叠层/衰减/清除结算链无断言;②设置与本地化——减动效开关本体仅系统测试与冒烟覆盖、无单元/集成断言,多语言切换面未建;③音频——MVP 无音频,P1 后置无断言面;④战斗表现——死亡灰化与减动效演出档只有探针与人工目检;⑤难度曲线与Boss——难度曲线整链与 Boss 出场门控无专项断言;⑥锻造工坊——价格梯仅长剑路线逐级验,GDD 无验收标准节(`capability_audit` 验收标准豁免在册)。教学引导与生涯两篇的缺口归各自在册条目(「教学形态未定」「「生涯」界面——冒险中数据回看的唯一承接处」),不在本条。补齐时同批刷新覆盖域表与能力行,新增断言按 test-conventions §8.4 绑真实入口。 -**改动面**:游戏代码+工具代码 +**改动面**:游戏代码 -**状态**:文档设计已闭合+待写卡 +**状态**:待执行 + +#### run_all_gates 02 关联档多套件词传参修复(2026-10-31 登记,来源=批次移交) + +**实况**:证据=iter45 实现报告 §5 遗留观察(已按吸收即死退役,git 历史可考)——`tools/run_all_gates.sh` 02 关联档以逗号连接多套件词传单个 `--only=a,b` 参数,而 `game/tests/run_all.gd` 选跑解析器不切逗号(多词整串做子串匹配→零命中 quit(2));iter45 施工时以多 `--only=` 旗标形态绕过。合回后 CI 跑全量档不经此路径不受影响,只挡开发期关联档窄跑。修法两案待裁:闸脚本改传多旗标,或解析器补逗号切分。 + +**改动面**:工具代码 + +**状态**:待执行 #### 战斗域结构深拆批——效果发射端口+掷格/受理分件(2026-10-31 重立,来源=批次移交) @@ -83,6 +126,14 @@ **状态**:文档设计已闭合+已写卡+已编迭代 49 +#### main.gd 拆件遗留死 preload 清理(2026-10-31 登记,来源=批次移交) + +**实况**:证据=iter44 反哺点 11(已按吸收即死退役,git 历史可考;实现报告实现决定记录同载)——迭代 44 卡① 组合根注册面批拆件后,main.gd 遗留零使用 preload 常量 8 条(UiAssembler/TuneSimController/DebugSessionScript/TreasureInteractController/CurioInteractController/CampShopPresenter/BoardResolversScript/三场景 preload 中 main 零使用者三件);卡① 施工时仅清理与迁移直接相关的 ModalStack/RowPacer 两条,其余属存量债不扩范围,归工程改动直改批顺路清理。清理前须逐条 grep 复核零使用(布局批与 iter45 合回后消费面或有变化)。 + +**改动面**:游戏代码 + +**状态**:待执行 + #### 数值标定窗口总挂账——原九条数值类条目合并(2026-10-28 登记,来源=会话提及) **实况**:2026-10-28 用户令把九条数值类挂账合并为一条:削弱档位的最终数值/迭代 10 奖励重构联动欠账/调参批数值点评登记/三方向各自的解锁清单与定价/GP 经济换算失衡/天赋树实装批登记欠账余两件/商店独立刷新机制两参数/基线通关率越带加难杠杆扫描/成长系数节奏——原九条同步删除,内容全量迁入汇总文档,git 历史可考(本提交)。共同背景:M4 数值批卡已随 2026-10-16 删卡令退役;2026-10-18 用户裁=各条留待办清单、未设计未启动、不另行归属,未来数值标定窗口重开时重走设计段再写卡——本条沿用该裁定,重启条件=用户发令开窗口。合并动机=九条共享同一重启条件与同一执行形态(窗口内一次 MC 批量统一覆盖各条对比需求:全冒险期望+奖励期望),分散登记徒增维护面。逐条挂账明细、裁定史与相关文档见汇总文档。 From 240f1a1769c6c67208cf2ee3c8dff69ecb99a53c Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 2 Nov 2026 07:30:33 +0800 Subject: [PATCH 2/4] =?UTF-8?q?iter49-=E5=8D=A12=EF=BC=9A=E6=A8=A1?= =?UTF-8?q?=E6=80=81=E5=8F=A5=E6=9F=84=E5=88=86=E5=9F=9F=E6=89=B9(?= =?UTF-8?q?=E5=8F=A5=E6=9F=84=E5=86=8C=E4=B8=89=E5=9F=9F=E5=8C=96):UiModal?= =?UTF-8?q?s=2014=E2=86=923=E7=BA=AF=E5=AD=97=E6=AE=B5=E5=AE=B9=E5=99=A8+r?= =?UTF-8?q?un/menu=E4=B8=A4=E6=96=B0=E5=AD=90=E5=AE=B9=E5=99=A8+ModalFlow?= =?UTF-8?q?=E5=8D=87=E6=A0=BC=E6=B3=A8=E5=86=8C=E9=9D=A2=E7=9B=B4=E9=A9=BB?= =?UTF-8?q?+=E5=85=A8=E4=BB=93=E5=AF=BB=E5=9D=80=E6=8D=A2=E4=BB=A3+V1?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/presentation/main.gd | 42 ++-- game/presentation/main_adventure_flow.gd | 58 ++--- game/presentation/main_debug_flow.gd | 20 +- game/presentation/main_esc_flow.gd | 14 +- game/presentation/main_overlay_flow.gd | 60 ++--- game/presentation/main_ui_build.gd | 124 ++++----- game/presentation/ui_camp_modals.gd | 4 +- game/presentation/ui_menu_modals.gd | 11 + game/presentation/ui_menu_modals.gd.uid | 1 + game/presentation/ui_modals.gd | 34 +-- game/presentation/ui_registry.gd | 46 ++-- game/presentation/ui_run_modals.gd | 11 + game/presentation/ui_run_modals.gd.uid | 1 + game/probe/fx_shot_probe.gd | 6 +- game/probe/layout_probe.gd | 4 +- game/probe/probe_ui_util.gd | 2 +- game/probe/screen_flow_probe.gd | 38 +-- game/probe/shot_probe.gd | 40 +-- game/probe/t17_defer_probe.gd | 14 +- game/probe/ui_click_probe.gd | 236 +++++++++--------- game/probe/ui_input_probe.gd | 26 +- game/probe/ui_probe.gd | 24 +- game/probe/ui_struct_probe.gd | 129 ++++++---- game/probe/ui_tune_probe.gd | 70 +++--- game/tests/smoke_boot.gd | 4 +- game/tests/system_test.gd | 42 ++-- .../unit/presentation/pacing_queue_test.gd | 2 +- .../presentation/theme_projection_test.gd | 2 +- 28 files changed, 552 insertions(+), 513 deletions(-) create mode 100644 game/presentation/ui_menu_modals.gd create mode 100644 game/presentation/ui_menu_modals.gd.uid create mode 100644 game/presentation/ui_run_modals.gd create mode 100644 game/presentation/ui_run_modals.gd.uid diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 09caad6f..fa3d5bd2 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -1,7 +1,7 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION ## 组合根:唯一的模式流转与 core↔view 接线处(垂直切片验证:线性第一关承载元进度循环)。 ## db.hero 运行期只读,加成经 runner.set_hero_override / debug_session 生效值入战斗。 -## 禁链式穿透 settlement.*(相位读取只走 presentation_snapshot);模态开合只走 ui.modals.flow(注册面编排口)。 +## 禁链式穿透 settlement.*(相位读取只走 presentation_snapshot);模态开合只走 ui.flow(注册面编排口)。 ## 界面句柄与层清单在 ui 注册面(ui_registry.gd)——屏壳/战区/模态三域句柄与 LAYER_CONTRACT 单点宿主。 ## 局态全在 run_scope.gd 节点,禁在屏/视图侧持有局态引用;autoload 节点路径常量单点在 AUTOLOAD_PATH_*。 extends Control @@ -104,7 +104,7 @@ func _ready() -> void: func _build_ui() -> void: MainUiBuild.build(self) - ui.modals.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed) # 诊断转储入口 + ui.modals.menu.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed) # 诊断转储入口 func _fatal_screen(msg: String) -> void: @@ -126,11 +126,11 @@ func _start_adventure_from_menu() -> void: #(确认才删、取消只关窗——裁决 3)。"gate"/"gate_new"=卡④ 大门两分支消费方(有/无断点), # "campfire_leave"=篝火二次确认(卡④ T4 接入——分派机制扩展第三/第四消费方)。 func _on_confirm_accepted() -> void: - ui.modals.flow.close(ui.modals.confirm_dialog) # 模态栈单口 + ui.flow.close(ui.modals.menu.confirm_dialog) # 模态栈单口 match _confirm_consumer: "delete_slot": save_manager.slots.delete_slot(_confirm_consumer_slot) - ui.modals.save_select_overlay.open_save_select(HudPresenter.save_select_data(save_manager)) # 就地刷新(该槽回空态) + ui.modals.menu.save_select_overlay.open_save_select(HudPresenter.save_select_data(save_manager)) # 就地刷新(该槽回空态) "gate": _camp_pres.on_camp_closed() # 冒险发起后营地场景屏隐藏(选档层同收——卡④ T4 关层收闭) _resume_from_menu() # 继续上一段冒险(现役续玩编排同源——卡④ 契约 4) @@ -144,7 +144,7 @@ func _on_confirm_accepted() -> void: # 次确认分派(三钮形态中钮——大门「开始新冒险」出口;卡④ T4) func _on_confirm_secondary() -> void: - ui.modals.flow.close(ui.modals.confirm_dialog) # 模态栈单口 + ui.flow.close(ui.modals.menu.confirm_dialog) # 模态栈单口 match _confirm_consumer: "gate": _camp_pres.on_camp_closed() @@ -153,7 +153,7 @@ func _on_confirm_secondary() -> void: func _on_confirm_declined() -> void: - ui.modals.flow.close(ui.modals.confirm_dialog) # 只关窗(取消无状态变更——裁决 3:取消不删/卡④:退回营地);关侧恢复归 ModalFlow + ui.flow.close(ui.modals.menu.confirm_dialog) # 只关窗(取消无状态变更——裁决 3:取消不删/卡④:退回营地);关侧恢复归 ModalFlow _confirm_consumer = "" @@ -169,16 +169,16 @@ func _resume_from_menu() -> void: if not save_manager.has_breakpoint(): return var bp: Dictionary[Variant, Variant] = save_manager.breakpoint_snapshot() - ui.modals.flow.close(ui.modals.debug_overlay) # 开冒险路径显式关浮窗(续玩同口径) + ui.flow.close(ui.modals.menu.debug_overlay) # 开冒险路径显式关浮窗(续玩同口径) ui.screens.start_overlay.visible = false _begin_session(true) _wire_adventure() run_scope.level_active = true _sync_status_entry() # 续玩恢复点亮状态面板入口角标 - ui.modals.level_screen.set_abandon_available(true) + ui.modals.run.level_screen.set_abandon_available(true) ui.battle.log_view.clear() logger.start_session(int(bp.get("master_seed", 0))) - run_scope.adventure.set_hero_override(ui.modals.debug_overlay.atk_pct(), ui.modals.debug_overlay.hp_pct()) + run_scope.adventure.set_hero_override(ui.modals.menu.debug_overlay.atk_pct(), ui.modals.menu.debug_overlay.hp_pct()) # 同调三键随续玩下传(build_adventure_params 统一构造,与开新局同源同口径) if not run_scope.adventure.resume_adventure(bp, build_adventure_params(save_manager, meta, {"treasure_unlocked": _treasure_ui.unlocked_set()})): @@ -214,16 +214,16 @@ static func build_adventure_params(save_manager_ref: SaveManagerCore, meta_ref: # 冒险模式(三关连探——AdventureRunner 编排;转发信号签名与 runner 一致,接线照原样对接; # 信号连接只在组合根) func _start_adventure() -> void: - ui.modals.flow.close(ui.modals.debug_overlay) # 开冒险路径显式关浮窗(调试浮窗默认开态下开局不带窗;对局中经角标再开) + ui.flow.close(ui.modals.menu.debug_overlay) # 开冒险路径显式关浮窗(调试浮窗默认开态下开局不带窗;对局中经角标再开) _begin_session(true) _wire_adventure() run_scope.level_active = true _sync_status_entry() # 进对局点亮状态面板入口角标 - ui.modals.level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期) + ui.modals.run.level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期) ui.battle.log_view.clear() var seed_val := _seed_value() logger.start_session(seed_val) - run_scope.adventure.set_hero_override(ui.modals.debug_overlay.atk_pct(), ui.modals.debug_overlay.hp_pct()) + run_scope.adventure.set_hero_override(ui.modals.menu.debug_overlay.atk_pct(), ui.modals.menu.debug_overlay.hp_pct()) # 永久强化注入:冒险开始参数面——meta 现算 override 显式传参(全零 = 白板) # 开局参数面(同调三键同源构造——确定性承载=固定种子+参数档,首局教学图由种子域三分判定给 42) if not run_scope.adventure.start_adventure(seed_val, build_adventure_params(save_manager, meta, @@ -287,16 +287,16 @@ func _on_battle_finished(payload: Dictionary) -> void: _refresh_presentations() return "level_end": - ui.modals.end_overlay.open_victory(Narrator.battle_end_text("victory", true), + ui.modals.run.end_overlay.open_victory(Narrator.battle_end_text("victory", true), "返回", ui.battle.board_zone.board_view.get_global_rect(), _end_victory_opts(payload)) "standalone_end": if result == "victory": - ui.modals.end_overlay.open_victory(Narrator.battle_end_text("victory", false), + ui.modals.run.end_overlay.open_victory(Narrator.battle_end_text("victory", false), "回到遭遇选择", ui.battle.board_zone.board_view.get_global_rect(), _end_victory_opts(payload)) else: - ui.modals.end_overlay.open_defeat(Narrator.battle_end_text("defeat", false), + ui.modals.run.end_overlay.open_defeat(Narrator.battle_end_text("defeat", false), "返回", ui.battle.board_zone.board_view.get_global_rect()) - ui.modals.flow.open_after_playback(ui.modals.end_overlay) + ui.flow.open_after_playback(ui.modals.run.end_overlay) _refresh_presentations() @@ -562,7 +562,7 @@ func _on_wallet_changed(event: String, gp: int, extra: Dictionary)-> void: func _on_board_action(cell: Vector2i, kind: String) -> void: # 玩家动作 → 核心写调用的唯一通道(模态/无战斗拒绝在此收口) - if ui.modals.flow.locks_input(): + if ui.flow.locks_input(): return # 结算演播期棋盘冻结(v3 口径——不可点/不可移/不可挖): # 锁面=排播队列活跃(纯队列判据;排播泵尾迹窗口不可作演播中标志——见 row_pacer._process 注)。 @@ -605,7 +605,7 @@ func _on_board_action(cell: Vector2i, kind: String) -> void: func _on_stop_pressed() -> void: - if ui.modals.flow.locks_input(): + if ui.flow.locks_input(): return # stop 经批次提交(排雷玩法 D1×6①——不直调 行动经济) if run_scope.settlement == null: @@ -622,7 +622,7 @@ func _on_stop_pressed() -> void: func _on_refresh_confirmed() -> void: # 弃图刷新(挖掘期 Active 当场换图继续挖——D4;RAP 按门槛扣费 · 旧板封印作废,每轮上限读表) - if ui.modals.flow.locks_input(): + if ui.flow.locks_input(): return var ok: bool = false if run_scope.level_active and run_scope.runner != null: @@ -764,8 +764,8 @@ func _seed_value() -> int: # 重放模板 0 即在此种子下标定〕;置位 true=教学局已收束的非首局走 ③ 随机); # ③非首局空/非法框=引擎全局熵 randi()(Godot 4 启动自播种子,headless 同;不走 rng_service # 封闭流、禁 randomize()/自建 RNG——组合根建种子属引擎熵豁免面,不进玩法 RNG 消费) - if ui.modals.debug_overlay.seed_edit.text.is_valid_int(): - return int(ui.modals.debug_overlay.seed_edit.text) + if ui.modals.menu.debug_overlay.seed_edit.text.is_valid_int(): + return int(ui.modals.menu.debug_overlay.seed_edit.text) if save_manager.meta_state.career_state().first_adventure_done: return randi() # rng-seed-exempt(非首局空框引擎熵——组合根建种子不进玩法 RNG 消费,static_scan S1/S2 标记豁免) return 42 diff --git a/game/presentation/main_adventure_flow.gd b/game/presentation/main_adventure_flow.gd index f52a4557..8eedfa9f 100644 --- a/game/presentation/main_adventure_flow.gd +++ b/game/presentation/main_adventure_flow.gd @@ -30,7 +30,7 @@ static func _on_adventure_level_started(root: Variant, level_index: int, display # 开场行只显示 atk/max_hp 两值,注记指向不存在的显示轴反成失真),白板局不带 var ov: Dictionary[Variant, Variant] = root.meta.hero_override() var boosted: bool = int(ov.get("atk", 0)) > 0 or int(ov.get("max_hp", 0)) > 0 \ - or root.ui.modals.debug_overlay.atk_pct() > 0 or root.ui.modals.debug_overlay.hp_pct() > 0 \ + or root.ui.modals.menu.debug_overlay.atk_pct() > 0 or root.ui.modals.menu.debug_overlay.hp_pct() > 0 \ or root.run_scope.runner.journal().bonus_total("atk") > 0 \ or root.run_scope.runner.journal().bonus_total("max_hp") > 0 root.ui.battle.log_view.append_bb(root.Narrator.level_open_line(display_name, int(root.run_scope.adventure.master_seed), @@ -70,21 +70,21 @@ static func _on_runner_node(root: Variant, node: Dictionary, info: Dictionary) - root.ui.battle.log_view.append_bb(root.Narrator.reward_line()) elif t == "rest": var healed: int = int(info.get("healed", 0)) - root.ui.modals.flow.open(root.ui.modals.node_overlay) - root.ui.modals.node_overlay.show_rest(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0))) + root.ui.flow.open(root.ui.modals.run.node_overlay) + root.ui.modals.run.node_overlay.show_rest(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0))) root.ui.battle.log_view.append_bb(root.Narrator.rest_line(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0)))) elif t == "shop": # 商店房兑付:到达即开店——商品/货架快照与余额经 runner 只读投影喂入; # 购买/离开归玩家意图(buy_requested/leave_pressed 接线),零自动推进。 # pool 面 = 共享池货架三选一同构卡;legacy 面 = 固定三件(off 对照承载) - root.ui.modals.flow.open(root.ui.modals.shop_overlay) + root.ui.flow.open(root.ui.modals.menu.shop_overlay) if root.run_scope.runner.shop.is_shop_pool(): - root.ui.modals.shop_overlay.open_shelf(root.run_scope.runner.shop.shop_shelf(), int(root.run_scope.runner.gp)) - root.ui.modals.shop_overlay.set_visit_info(int(root.run_scope.runner.shop.shop_bp_visit_buys()), float(root.db.run.shop.price_a)) # 爬价规则行 - root.ui.modals.shop_overlay.set_refresh_display(root.run_scope.runner.shop.shop_refresh_free_left(), root.run_scope.runner.shop.shop_refresh_cost(), int(root.run_scope.runner.gp) >= root.run_scope.runner.shop.shop_refresh_cost()) # MC-1 + root.ui.modals.menu.shop_overlay.open_shelf(root.run_scope.runner.shop.shop_shelf(), int(root.run_scope.runner.gp)) + root.ui.modals.menu.shop_overlay.set_visit_info(int(root.run_scope.runner.shop.shop_bp_visit_buys()), float(root.db.run.shop.price_a)) # 爬价规则行 + root.ui.modals.menu.shop_overlay.set_refresh_display(root.run_scope.runner.shop.shop_refresh_free_left(), root.run_scope.runner.shop.shop_refresh_cost(), int(root.run_scope.runner.gp) >= root.run_scope.runner.shop.shop_refresh_cost()) # MC-1 else: - root.ui.modals.shop_overlay.open_shop(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp)) - root.ui.modals.shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载) + root.ui.modals.menu.shop_overlay.open_shop(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp)) + root.ui.modals.menu.shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载) root.ui.battle.log_view.append_bb(root.Narrator.shop_line(int(root.run_scope.runner.gp))) root.logger.log_node(node, info, root.run_scope.runner.node_index) root._refresh_presentations() @@ -110,20 +110,20 @@ static func _on_adventure_finished(root: Variant, summary: Dictionary) -> void: if not detail.is_empty(): text += "\n" + root.Narrator.keystone_settle_text(detail, root._bonus.item_name_lookup()) if result == "completed": - root.ui.modals.end_overlay.open_victory(text, "返回冒险", root.ui.battle.board_zone.board_view.get_global_rect(), + root.ui.modals.run.end_overlay.open_victory(text, "返回冒险", root.ui.battle.board_zone.board_view.get_global_rect(), {"gp_summary": "本局 GP 入账 · 合计 %d" % int(summary.get("gp_final", 0))}) # GP 行(取数=summary gp_final 清账前快照);钥石明细已在 text(keystone_settle_text)——三行账 else: - root.ui.modals.end_overlay.open_defeat(text, "返回", root.ui.battle.board_zone.board_view.get_global_rect()) + root.ui.modals.run.end_overlay.open_defeat(text, "返回", root.ui.battle.board_zone.board_view.get_global_rect()) root.logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷) if root.run_scope.runner != null: root.run_scope.runner.restore_hero_base() - root.ui.modals.flow.open_after_playback(root.ui.modals.end_overlay) + root.ui.flow.open_after_playback(root.ui.modals.run.end_overlay) root._refresh_presentations() static func _on_battle_end_continue(root: Variant) -> void: # 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石在收束时入存档) - root.ui.modals.flow.close(root.ui.modals.end_overlay) - root.ui.modals.end_overlay.close() + root.ui.flow.close(root.ui.modals.run.end_overlay) + root.ui.modals.run.end_overlay.close() # 三选一停点挂起分支:战斗总结结算(Boss 胜利钥石增量行)确认后进三选一面 if root._bonus.resume_if_pending(): # 三选一停点在途恢复入口(curio_interact_controller) return @@ -131,9 +131,9 @@ static func _on_battle_end_continue(root: Variant) -> void: if root.run_scope.settlement_stop_timer.is_armed(): root.run_scope.settlement_stop_timer.disarm() _enter_level_screen(root) - root.ui.modals.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held()))) - root.ui.modals.level_screen.open_choices(root.run_scope.pending_candidates, root.run_scope.runner) - root.ui.modals.level_screen.apply_status(level_map_status_payload(root.run_scope.runner)) + root.ui.modals.run.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held()))) + root.ui.modals.run.level_screen.open_choices(root.run_scope.pending_candidates, root.run_scope.runner) + root.ui.modals.run.level_screen.apply_status(level_map_status_payload(root.run_scope.runner)) root.run_scope.pending_candidates = [] root._refresh_presentations() return @@ -141,7 +141,7 @@ static func _on_battle_end_continue(root: Variant) -> void: # 局态封印/账本/模态挂起全复位(局态字段清单见 run_scope.gd) root._debug.reset() root._begin_session(false) - root.ui.modals.debug_overlay.reset_sliders() + root.ui.modals.menu.debug_overlay.reset_sliders() root.ui.screens.start_overlay.visible = true root._refresh_presentations() @@ -158,7 +158,7 @@ static func _on_inter_next_pressed(root: Variant) -> void: static func _on_node_continue(root: Variant) -> void: - root.ui.modals.flow.close(root.ui.modals.node_overlay) + root.ui.flow.close(root.ui.modals.run.node_overlay) if root.run_scope.runner != null: # 确认 ≠ 推进——兑付确认后经 choices_presented 弹出选路口 root.run_scope.runner.continue_after_node() @@ -176,24 +176,24 @@ static func _on_choices_presented(root: Variant, candidates: Array) -> void: and String(root.run_scope.runner.current_node().get("type", "")) == "battle" and not candidates.is_empty(): root.run_scope.settlement_stop_timer.arm() root.run_scope.pending_candidates = candidates.duplicate() - root.ui.modals.end_overlay.open_victory(root.Narrator.battle_end_text("victory", false), "下一节点", + root.ui.modals.run.end_overlay.open_victory(root.Narrator.battle_end_text("victory", false), "下一节点", root.ui.battle.board_zone.board_view.get_global_rect(), {"gp_summary": "战利品 GP +%d · 合计 %d" % [root.run_scope.last_gain_amount, int(root.run_scope.runner.gp)]}) - root.ui.modals.flow.open_after_playback(root.ui.modals.end_overlay) + root.ui.flow.open_after_playback(root.ui.modals.run.end_overlay) root._refresh_presentations() return _enter_level_screen(root) - root.ui.modals.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held()))) - root.ui.modals.level_screen.open_choices(candidates, root.run_scope.runner) - root.ui.modals.level_screen.apply_status(level_map_status_payload(root.run_scope.runner)) + root.ui.modals.run.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held()))) + root.ui.modals.run.level_screen.open_choices(candidates, root.run_scope.runner) + root.ui.modals.run.level_screen.apply_status(level_map_status_payload(root.run_scope.runner)) root._refresh_presentations() static func _on_route_chosen(root: Variant, node_id: int) -> void: # 视图已按节点类型定去留——战斗/Boss 出屏(visible=false,此处收栈 # 记录一致性+回战斗屏);奖励/恢复/商店本屏保持激活(兑付覆盖层由节点事件域内弹出,模态栈 # 打开低层时自动隐藏本屏、关闭自动恢复) - if not root.ui.modals.level_screen.visible: - root.ui.modals.flow.close(root.ui.modals.level_screen) + if not root.ui.modals.run.level_screen.visible: + root.ui.flow.close(root.ui.modals.run.level_screen) _exit_level_screen(root) if root.run_scope.runner != null and not root.run_scope.runner.pick_next_node(node_id): push_error("[组合根] 选路被拒(编号 %d 不在候选)" % node_id) @@ -203,14 +203,14 @@ static func _on_route_chosen(root: Variant, node_id: int) -> void: static func _enter_level_screen(root: Variant) -> void: root.run_scope.level_screen_active = true root.ui.screens.battle_root.visible = false - root.ui.modals.flow.open(root.ui.modals.level_screen) + root.ui.flow.open(root.ui.modals.run.level_screen) root._sync_status_entry() # 屏级互斥切换同步(_open_modal 单口不识屏级状态) static func _exit_level_screen(root: Variant) -> void: root.run_scope.level_screen_active = false root.ui.screens.battle_root.visible = true - if root.ui.modals.level_screen.visible: - root.ui.modals.flow.close(root.ui.modals.level_screen) + if root.ui.modals.run.level_screen.visible: + root.ui.flow.close(root.ui.modals.run.level_screen) root._sync_status_entry() # 同上 diff --git a/game/presentation/main_debug_flow.gd b/game/presentation/main_debug_flow.gd index 949b96ed..4dce4189 100644 --- a/game/presentation/main_debug_flow.gd +++ b/game/presentation/main_debug_flow.gd @@ -21,17 +21,17 @@ static func _show_debug_overlay(root: Variant) -> void: # 门禁:对局中可开;本函数仍是唯一开窗入口,门禁逻辑收函数内: # 棋盘输入锁/状态角标判据排除浮窗, # 遮挡语义=ModalStack 登记制(他模态开时暂隐、关后自动恢复),此处零场景限制 - if root.ui.modals.debug_overlay == null: + if root.ui.modals.menu.debug_overlay == null: return - root.ui.modals.flow.open(root.ui.modals.debug_overlay) + root.ui.flow.open(root.ui.modals.menu.debug_overlay) _sync_debug_overlay(root) # 调试浮窗角标点击(两屏同款处理器——与状态角标同形制守卫;仍收口 _show_debug_overlay 单口) static func _on_debug_badge_pressed(root: Variant) -> void: - var map_on: bool = root.ui.modals.level_screen != null and root.ui.modals.level_screen.visible - if (map_on and root.ui.modals.flow.badge_greyed_map()) or (not map_on and root.ui.modals.flow.badge_greyed_battle()): + var map_on: bool = root.ui.modals.run.level_screen != null and root.ui.modals.run.level_screen.visible + if (map_on and root.ui.flow.badge_greyed_map()) or (not map_on and root.ui.flow.badge_greyed_battle()): return _show_debug_overlay(root) @@ -40,10 +40,10 @@ static func _on_debug_badge_pressed(root: Variant) -> void: # 浮窗上下文同步(当局数据刷新+对局中按钮禁用单点):调用面=开窗口与模态开合 # 单口(凡影响开态/对局态的路径必经其一——与 _sync_status_entry 同款纪律) static func _sync_debug_overlay(root: Variant) -> void: - if root.ui.modals.debug_overlay == null: + if root.ui.modals.menu.debug_overlay == null: return - root.ui.modals.debug_overlay.set_context_locked(root.run_scope.settlement != null or root.run_scope.is_adventure) # 对局中批量模拟/两层调试入口禁用(长程异步模拟与活跃结算并存态不开放) - root.ui.modals.debug_overlay.update_run_data(_debug_run_data(root)) # 当局数据只读区块喂入(开窗+模态开合信号驱动) + root.ui.modals.menu.debug_overlay.set_context_locked(root.run_scope.settlement != null or root.run_scope.is_adventure) # 对局中批量模拟/两层调试入口禁用(长程异步模拟与活跃结算并存态不开放) + root.ui.modals.menu.debug_overlay.update_run_data(_debug_run_data(root)) # 当局数据只读区块喂入(开窗+模态开合信号驱动) @@ -63,7 +63,7 @@ static func _debug_run_data(root: Variant) -> Dictionary: static func _close_debug_overlay(root: Variant) -> void: - root.ui.modals.flow.close(root.ui.modals.debug_overlay) # 模态栈单口——开始层全程未隐藏,无需恢复 + root.ui.flow.close(root.ui.modals.menu.debug_overlay) # 模态栈单口——开始层全程未隐藏,无需恢复 static func _on_tune_apply(root: Variant, atk_pct: int, hp_pct: int) -> void: # 加成无持久态可"落"——各战斗开局经 board_resolvers.debug_hero_override/runner 现算生效(应用=关闭浮窗) @@ -83,8 +83,8 @@ static func _start_debug_levels(root: Variant) -> void: # settlement/编排态=状态机破坏;模拟在跑时 reset_sliders 会作废测量读数(d5 置灰之外的信号绕过闸) if root.run_scope.is_adventure or root.run_scope.settlement != null or root._tune_sim.is_running(): return - root.ui.modals.debug_overlay.reset_sliders() - root.ui.modals.flow.close(root.ui.modals.debug_overlay) # 浮窗内入口发起——随开随收(对局期不可再开:门禁在 _show_debug_overlay) + root.ui.modals.menu.debug_overlay.reset_sliders() + root.ui.flow.close(root.ui.modals.menu.debug_overlay) # 浮窗内入口发起——随开随收(对局期不可再开:门禁在 _show_debug_overlay) root.ui.screens.start_overlay.visible = false root.ui.battle.log_view.clear() root.ui.battle.log_view.append_bb(root.Narrator.debug_open_line(root._seed_value(), 1, true)) diff --git a/game/presentation/main_esc_flow.gd b/game/presentation/main_esc_flow.gd index 9ec94068..40cfc2a1 100644 --- a/game/presentation/main_esc_flow.gd +++ b/game/presentation/main_esc_flow.gd @@ -18,22 +18,22 @@ static func on_esc_input(root: Variant) -> void: var esc: Control = root.ui.modals.camp.esc_overlay var window := _topmost_camp_window(root) if window != null: - root.ui.modals.flow.close(window) # ① 先关最上层功能窗(v2.3 定稿) + root.ui.flow.close(window) # ① 先关最上层功能窗(v2.3 定稿) return - if root.ui.modals.settings_overlay.visible: + if root.ui.modals.menu.settings_overlay.visible: return # ①' 设置开在浮窗之上 → 无反馈(浮窗不隔层关闭——契约 3) if esc.visible: - root.ui.modals.flow.close(esc) # ② 再按 Esc 关窗 + root.ui.flow.close(esc) # ② 再按 Esc 关窗 return if is_camp_context(root): - root.ui.modals.flow.open(esc) # ③ 营地语境唤起 + root.ui.flow.open(esc) # ③ 营地语境唤起 # ④ 非营地语境 → 无反馈 # 五功能窗现开查询(camp.md v2.3 五窗互斥单开——模态栈深恒 1,至多一窗在开;生涯页入窗集) static func _topmost_camp_window(root: Variant) -> Control: for w: Control in [root.ui.modals.camp.forge, root.ui.modals.camp.treasure, root.ui.modals.camp.tree, - root.ui.modals.camp.codex, root.ui.modals.career_overlay]: + root.ui.modals.camp.codex, root.ui.modals.menu.career_overlay]: if w != null and w.visible: return w return null @@ -42,13 +42,13 @@ static func _topmost_camp_window(root: Variant) -> Control: ## 返回主菜单动线(契约 4):首步 close 浮窗(缺此步浮窗残留主菜单上方)→营地与选档层同收+开始层恢复 static func _on_return_to_menu(root: Variant) -> void: - root.ui.modals.flow.close(root.ui.modals.camp.esc_overlay) + root.ui.flow.close(root.ui.modals.camp.esc_overlay) root._camp_pres.on_camp_closed() # 关层收闭单口(卡④ T3——营地层与选档层同收+开始层恢复) ## 设置出口(浮窗登记在设置层之下——open 只隐上方层,浮窗由树序遮挡非属性隐藏;关后重现=栈语义) static func _on_settings(root: Variant) -> void: - root.ui.modals.flow.open(root.ui.modals.settings_overlay) + root.ui.flow.open(root.ui.modals.menu.settings_overlay) ## 退出出口(_quit_game 原样复用——settings_store.save_settings+quit,零语义副作用) diff --git a/game/presentation/main_overlay_flow.gd b/game/presentation/main_overlay_flow.gd index 7df4e973..4a9e4e37 100644 --- a/game/presentation/main_overlay_flow.gd +++ b/game/presentation/main_overlay_flow.gd @@ -1,15 +1,15 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION ## 组合根覆盖层流程承接件:设置/状态面板/生涯页/选档界面/图例/钱包事件的开合与数据投喂。 ## 本件是 main.gd 同名函数的逐字搬运(成员访问改 root 前缀)——main 侧留同名薄委托,探针 main.X 路径零改动。 -## 全部开合走组合根模态编排口(root.ui.modals.flow.open/close);本件零成员态。 +## 全部开合走组合根模态编排口(root.ui.flow.open/close);本件零成员态。 extends RefCounted static func _open_settings(root: Variant) -> void: root.ui.screens.start_overlay.visible = false - root.ui.modals.flow.open(root.ui.modals.settings_overlay) # 模态栈单口 + root.ui.flow.open(root.ui.modals.menu.settings_overlay) # 模态栈单口 static func _on_settings_closed(root: Variant) -> void: - root.ui.modals.flow.close(root.ui.modals.settings_overlay) # 模态栈单口 + root.ui.flow.close(root.ui.modals.menu.settings_overlay) # 模态栈单口 # 语境分支(卡⑤ 契约 3——谓词=camp_esc_overlay.visible,谁开的设置谁收): # 营地浮窗路径关设置回浮窗不回主菜单(栈语义:浮窗登记在下、由树序遮挡非属性隐藏); # 菜单路径保留开始层恢复硬副作用。 @@ -29,21 +29,21 @@ static func _status_data(root: Variant) -> Dictionary: return HudPresenter.status_data(root.db, root.meta, root.run_scope.runner, root._treasure_ui) static func _open_status_panel(root: Variant) -> void: - root.ui.modals.flow.open(root.ui.modals.status_panel) # 模态栈单口(开层前排空排播队列——既有层同款行为,目检确认项) - root.ui.modals.status_panel.open_status(_status_data(root)) + root.ui.flow.open(root.ui.modals.run.status_panel) # 模态栈单口(开层前排空排播队列——既有层同款行为,目检确认项) + root.ui.modals.run.status_panel.open_status(_status_data(root)) static func _on_status_closed(root: Variant) -> void: - root.ui.modals.flow.close(root.ui.modals.status_panel) # 模态栈单口 + root.ui.flow.close(root.ui.modals.run.status_panel) # 模态栈单口 static func _open_career(root: Variant) -> void: root.ui.screens.start_overlay.visible = false - root.ui.modals.flow.open(root.ui.modals.career_overlay) # 模态栈单口 - root.ui.modals.career_overlay.open_career(HudPresenter.career_data(root.db, root.save_manager, root.meta)) + root.ui.flow.open(root.ui.modals.menu.career_overlay) # 模态栈单口 + root.ui.modals.menu.career_overlay.open_career(HudPresenter.career_data(root.db, root.save_manager, root.meta)) static func _on_career_closed(root: Variant) -> void: - root.ui.modals.flow.close(root.ui.modals.career_overlay) # 模态栈单口 + root.ui.flow.close(root.ui.modals.menu.career_overlay) # 模态栈单口 if not root.ui.modals.camp.scene.visible: # 营地语境关生涯不回开始层(笔记本入口——卡④ T3;栈已复现场景根) root.ui.screens.start_overlay.visible = true @@ -70,17 +70,17 @@ static func _on_camp_object_pressed(root: Variant, key: StringName) -> void: _open_gate_confirm(root) &"campfire": root._confirm_consumer = "campfire_leave" - root.ui.modals.confirm_dialog.open("确定离开营地回到主菜单?", "离开", "留下") + root.ui.modals.menu.confirm_dialog.open("确定离开营地回到主菜单?", "离开", "留下") static func _open_gate_confirm(root: Variant) -> void: if root.save_manager.has_breakpoint(): root._confirm_consumer = "gate" - root.ui.modals.confirm_dialog.open_three("有未完成的冒险——继续上一段冒险,还是开始新冒险?", + root.ui.modals.menu.confirm_dialog.open_three("有未完成的冒险——继续上一段冒险,还是开始新冒险?", "继续上一段冒险", "开始新冒险", "取消") return root._confirm_consumer = "gate_new" - root.ui.modals.confirm_dialog.open("是否开始新冒险?", "是", "否") + root.ui.modals.menu.confirm_dialog.open("是否开始新冒险?", "是", "否") # 生涯页快照投影 —— 承接体见 HudPresenter.career_data(开层调用点直接改引) @@ -93,11 +93,11 @@ static func _open_gate_confirm(root: Variant) -> void: # 该钮的唯一落点(卡③ 批内探针函数直调=中间态,实现报告「已知中间态」登记)。 static func _open_save_select(root: Variant) -> void: root.ui.screens.start_overlay.visible = false - root.ui.modals.flow.open(root.ui.modals.save_select_overlay) # 模态栈单口 - root.ui.modals.save_select_overlay.open_save_select(HudPresenter.save_select_data(root.save_manager)) + root.ui.flow.open(root.ui.modals.menu.save_select_overlay) # 模态栈单口 + root.ui.modals.menu.save_select_overlay.open_save_select(HudPresenter.save_select_data(root.save_manager)) static func _on_save_select_closed(root: Variant) -> void: - root.ui.modals.flow.close(root.ui.modals.save_select_overlay) # 模态栈单口 + root.ui.flow.close(root.ui.modals.menu.save_select_overlay) # 模态栈单口 root.ui.screens.start_overlay.visible = true @@ -125,18 +125,18 @@ static func _on_save_select_load(root: Variant, slot: int) -> void: static func _on_save_select_delete(root: Variant, slot: int) -> void: root._confirm_consumer = "delete_slot" root._confirm_consumer_slot = slot - root.ui.modals.flow.open(root.ui.modals.confirm_dialog) # 模态栈单口(显示编排/挂起兜底/遮挡记账——与其余覆盖层同口) - root.ui.modals.confirm_dialog.open("删除存档位 %d?删除后该档不可恢复。" % slot, "删除", "取消") + root.ui.flow.open(root.ui.modals.menu.confirm_dialog) # 模态栈单口(显示编排/挂起兜底/遮挡记账——与其余覆盖层同口) + root.ui.modals.menu.confirm_dialog.open("删除存档位 %d?删除后该档不可恢复。" % slot, "删除", "取消") # 入口角标点击(两屏同款处理器):P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰, # 探针 pressed.emit 绕过 disabled 时由此兜住;地图屏自身的登记开态不计——has_open_excluding) static func _on_status_badge_pressed(root: Variant) -> void: - var map_on: bool = root.ui.modals.level_screen != null and root.ui.modals.level_screen.visible + var map_on: bool = root.ui.modals.run.level_screen != null and root.ui.modals.run.level_screen.visible # P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰,探针 pressed.emit 绕过 disabled 时由此兜住; # 地图屏自身的登记开态不计;判据排除调试浮窗:对局中浮窗开着时状态角标仍可点) - if (map_on and root.ui.modals.flow.badge_greyed_map()) or (not map_on and root.ui.modals.flow.badge_greyed_battle()): + if (map_on and root.ui.flow.badge_greyed_map()) or (not map_on and root.ui.flow.badge_greyed_battle()): return _open_status_panel(root) @@ -146,14 +146,14 @@ static func _on_status_badge_pressed(root: Variant) -> void: # 地图屏角标=随屏;置灰=登记册上除地图屏自身外任一层开着(防「看得见点不着」态)。 # 调用面=模态开合单口与屏级互斥口(凡影响开态/屏态的路径必经其一)。 static func _sync_status_entry(root: Variant) -> void: - if root.ui.modals.status_panel == null or root.ui.modals.flow == null or root.ui.battle.board_zone == null or root.ui.modals.level_screen == null: + if root.ui.modals.run.status_panel == null or root.ui.flow == null or root.ui.battle.board_zone == null or root.ui.modals.run.level_screen == null: return - var map_on: bool = root.ui.modals.level_screen.visible + var map_on: bool = root.ui.modals.run.level_screen.visible root.ui.battle.board_zone.status_badge.visible = root.run_scope.level_active and not map_on - root.ui.modals.level_screen.status_badge.visible = map_on + root.ui.modals.run.level_screen.status_badge.visible = map_on # 置灰判据为排除式(调试浮窗开态不计入:对局中浮窗开着时角标仍可点) - root.ui.battle.board_zone.status_badge.disabled = root.ui.modals.flow.badge_greyed_battle() # 互斥域谓词 - root.ui.modals.level_screen.status_badge.disabled = root.ui.modals.flow.badge_greyed_map() + root.ui.battle.board_zone.status_badge.disabled = root.ui.flow.badge_greyed_battle() # 互斥域谓词 + root.ui.modals.run.level_screen.status_badge.disabled = root.ui.flow.badge_greyed_map() # 调试浮层角标(全局浮层任何 UI 不遮挡——可见性不随场景变,只抬 offset # 避让:地图屏底栏 85px 常驻,角标抬至底栏上方 16px;其余场景右下角 16px 贴底) root.ui.screens.debug_badge.offset_bottom = -101.0 if map_on else -16.0 @@ -171,19 +171,19 @@ static func _on_wallet_changed(root: Variant, event: String, gp: int, extra: Dic root.logger.log_wallet(event, gp, extra) if String(event) == "gain": root.run_scope.last_gain_amount = int(extra.get("amount", 0)) # GP 增量取数(每战恰一笔 gain) - if root.ui.modals.shop_overlay.visible and root.run_scope.runner != null: + if root.ui.modals.menu.shop_overlay.visible and root.run_scope.runner != null: if root.run_scope.runner.shop.is_shop_pool(): - root.ui.modals.shop_overlay.refresh_shelf_state(int(root.run_scope.runner.gp)) # pool 面余额联动(货架内容仅买后重抽变) + root.ui.modals.menu.shop_overlay.refresh_shelf_state(int(root.run_scope.runner.gp)) # pool 面余额联动(货架内容仅买后重抽变) else: - root.ui.modals.shop_overlay.refresh_state(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp)) + root.ui.modals.menu.shop_overlay.refresh_state(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp)) root._refresh_presentations() static func _on_legend_toggled(root: Variant) -> void: - if root.ui.modals.legend_overlay.is_open(): - root.ui.modals.flow.close(root.ui.modals.legend_overlay) + if root.ui.modals.run.legend_overlay.is_open(): + root.ui.flow.close(root.ui.modals.run.legend_overlay) else: - root.ui.modals.flow.open(root.ui.modals.legend_overlay) + root.ui.flow.open(root.ui.modals.run.legend_overlay) diff --git a/game/presentation/main_ui_build.gd b/game/presentation/main_ui_build.gd index 09164e42..fb628fff 100644 --- a/game/presentation/main_ui_build.gd +++ b/game/presentation/main_ui_build.gd @@ -46,7 +46,7 @@ static func _build_screens(root: Variant) -> void: root.ui.battle.info_panel = root.ui.screens.adventure_screen.info_panel root.ui.battle.board_zone = root.ui.screens.adventure_screen.board_zone root.ui.battle.settlement_fx = root.ui.screens.adventure_screen.settlement_fx - root.ui.modals.level_screen = root.ui.screens.adventure_screen.level_screen + root.ui.modals.run.level_screen = root.ui.screens.adventure_screen.level_screen static func _build_battle_wiring(root: Variant) -> void: # 结算行排播泵(本体在 row_pacer.gd;本类注入视图句柄与呈现刷新回调,零信号连接) @@ -57,7 +57,7 @@ static func _build_battle_wiring(root: Variant) -> void: root.ui.screens.row_pacer.settlement_fx = root.ui.battle.settlement_fx root.ui.screens.row_pacer.board_view = root.ui.battle.board_zone.board_view root.ui.screens.row_pacer.refresh_request = root._refresh_presentations - root.ui.screens.row_pacer.pump_ended = root.ui.modals.flow.pump_ended # 演播完边沿统一派发(挂起 modal 优先,其次锁期点击重放) + root.ui.screens.row_pacer.pump_ended = root.ui.flow.pump_ended # 演播完边沿统一派发(挂起 modal 优先,其次锁期点击重放) root.add_child(root.ui.screens.row_pacer) # 秘宝两步交互(本体在 treasure_interact_controller.gd;settlement/runner/grant 随关卡/战斗更替, # 经现值回调注入——仿 identity_resolver 只读回调先例;grant 每战新建编排器,现值取件防悬挂) @@ -86,10 +86,10 @@ static func _build_overlays(root: Variant) -> void: # < 商店 < 营地场景根 < 四子界面 < 设置 < 状态面板(只读) < 生涯页(只读) < 确认弹窗 < 调试浮窗; # 关卡画面住冒险屏壳(树序在模态之下)——登记序仍夹在节点层与三选一层之间,遮挡语义零改动) var overlays: Dictionary[Variant, Variant] = UiAssembler.build_overlays(root, root.db) - root.ui.modals.legend_overlay = overlays.legend_overlay - root.ui.modals.node_overlay = overlays.node_overlay - root.ui.modals.end_overlay = overlays.end_overlay - root.ui.modals.shop_overlay = overlays.shop_overlay + root.ui.modals.run.legend_overlay = overlays.legend_overlay + root.ui.modals.run.node_overlay = overlays.node_overlay + root.ui.modals.run.end_overlay = overlays.end_overlay + root.ui.modals.menu.shop_overlay = overlays.shop_overlay root.ui.screens.camp_screen = CampScreenScene.instantiate() root.add_child(root.ui.screens.camp_screen) # 营地场景根装配(布局表经 ConfigDB 单点注入——视图禁自载表路径;CampOverlay 随四柜台拆解退役) @@ -120,58 +120,58 @@ static func _build_overlays(root: Variant) -> void: var hud_band := CampHudBand.new() root.ui.modals.camp.scene.set_hud_band(hud_band) # 屏壳/子界面插位由 LAYER_CONTRACT 槽统一落位——禁散落 move_child - root.ui.modals.legend_overlay.closed.connect(root.ui.modals.flow.close.bind(root.ui.modals.legend_overlay)) - root.ui.modals.node_overlay.continue_pressed.connect(root._on_node_continue) - root.ui.modals.level_screen.route_chosen.connect(root._on_route_chosen) - root.ui.modals.level_screen.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(地图屏顶栏角标) - root.ui.modals.level_screen.abandon_requested.connect(root._on_abandon_requested) # 放弃入口 - root.ui.modals.end_overlay.continue_pressed.connect(root._on_battle_end_continue) + root.ui.modals.run.legend_overlay.closed.connect(root.ui.flow.close.bind(root.ui.modals.run.legend_overlay)) + root.ui.modals.run.node_overlay.continue_pressed.connect(root._on_node_continue) + root.ui.modals.run.level_screen.route_chosen.connect(root._on_route_chosen) + root.ui.modals.run.level_screen.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(地图屏顶栏角标) + root.ui.modals.run.level_screen.abandon_requested.connect(root._on_abandon_requested) # 放弃入口 + root.ui.modals.run.end_overlay.continue_pressed.connect(root._on_battle_end_continue) static func _build_aux_panels(root: Variant) -> void: - root.ui.modals.settings_overlay = SettingsOverlay.new(root.settings_store) # 最小设置面(音量/全屏/减动效) - root.add_child(root.ui.modals.settings_overlay) - root.ui.modals.settings_overlay.closed.connect(root._on_settings_closed) + root.ui.modals.menu.settings_overlay = SettingsOverlay.new(root.settings_store) # 最小设置面(音量/全屏/减动效) + root.add_child(root.ui.modals.menu.settings_overlay) + root.ui.modals.menu.settings_overlay.closed.connect(root._on_settings_closed) # 局内状态面板·只读(登记设置层同档带 dim 遮罩,确认弹窗之前; # 数据=组合根开层实时读 MetaProgression 访问器喂入,本层零写通路) - root.ui.modals.status_panel = StatusPanel.new() - root.add_child(root.ui.modals.status_panel) - root.ui.modals.status_panel.closed.connect(root._on_status_closed) + root.ui.modals.run.status_panel = StatusPanel.new() + root.add_child(root.ui.modals.run.status_panel) + root.ui.modals.run.status_panel.closed.connect(root._on_status_closed) # 生涯页·只读全屏页(登记序=营地同档,设置层旁、确认弹窗之前; # 树序同步=addChild 同位(登记序=树序=z 序纪律);数据=组合根开层实时读 SaveManager 计数 # 访问器+MetaProgression 访问器喂入,本层零写通路零跳转;入口=开始层「营地与系统」组生涯钮) - root.ui.modals.career_overlay = CareerOverlay.new() - root.add_child(root.ui.modals.career_overlay) - root.ui.modals.career_overlay.closed.connect(root._on_career_closed) + root.ui.modals.menu.career_overlay = CareerOverlay.new() + root.add_child(root.ui.modals.menu.career_overlay) + root.ui.modals.menu.career_overlay.closed.connect(root._on_career_closed) # 选档界面(营地交互重做卡③):登记序=生涯同档(树序同步=addChild 同位);三动作只发意图信号 #(激活/创建/删除+确认分派归组合根——视图零数据层依赖);入口=「进入存档」钮(卡② mn3 接线)。 - root.ui.modals.save_select_overlay = SaveSelectOverlay.new() - root.add_child(root.ui.modals.save_select_overlay) - root.ui.modals.save_select_overlay.closed.connect(root._on_save_select_closed) - root.ui.modals.save_select_overlay.new_slot_requested.connect(root._on_save_select_new) - root.ui.modals.save_select_overlay.load_slot_requested.connect(root._on_save_select_load) - root.ui.modals.save_select_overlay.delete_slot_requested.connect(root._on_save_select_delete) + root.ui.modals.menu.save_select_overlay = SaveSelectOverlay.new() + root.add_child(root.ui.modals.menu.save_select_overlay) + root.ui.modals.menu.save_select_overlay.closed.connect(root._on_save_select_closed) + root.ui.modals.menu.save_select_overlay.new_slot_requested.connect(root._on_save_select_new) + root.ui.modals.menu.save_select_overlay.load_slot_requested.connect(root._on_save_select_load) + root.ui.modals.menu.save_select_overlay.delete_slot_requested.connect(root._on_save_select_delete) static func _build_curio(root: Variant) -> void: # 提升三选一覆盖层(z 序 = 节点层之上、终局层之下;确认后同拍推进; # 插位由 LAYER_CONTRACT curio 槽统一落位——禁散落 move_child) - root.ui.modals.curio_overlay = CurioPickOverlay.new() - root.add_child(root.ui.modals.curio_overlay) + root.ui.modals.run.curio_overlay = CurioPickOverlay.new() + root.add_child(root.ui.modals.run.curio_overlay) # 提升三选一与替换子交互(本体在 curio_interact_controller.gd;本类只接线与回调注入, # 模态栈/演播后排窗/呈现刷新/fatal 经回调注入,adventure/runner 现值回调) root._bonus = CurioInteractController.new(CurioFlowContext.new(root.db, - {"curio_overlay": root.ui.modals.curio_overlay, "end_overlay": root.ui.modals.end_overlay, + {"curio_overlay": root.ui.modals.run.curio_overlay, "end_overlay": root.ui.modals.run.end_overlay, "board_view": root.ui.battle.board_zone.board_view, "toast": root.ui.battle.board_zone.toast}, {"meta_get": func() -> Variant: return root.meta, "save_get": func() -> Variant: return root.save_manager, "adventure_get": func() -> Variant: return root.run_scope.adventure, "runner_get": func() -> Variant: return root.run_scope.runner, "adventure_mode_get": func() -> Variant: return root.run_scope.is_adventure}, - {"open_modal": root.ui.modals.flow.open, "open_modal_after_playback": root.ui.modals.flow.open_after_playback, - "close_modal": root.ui.modals.flow.close, "refresh_presentations": root._refresh_presentations, + {"open_modal": root.ui.flow.open, "open_modal_after_playback": root.ui.flow.open_after_playback, + "close_modal": root.ui.flow.close, "refresh_presentations": root._refresh_presentations, "fatal_screen": root._fatal_screen, "gp_summary": _gp_summary_cb(root)})) # 构造参数收拢 context(u20 三束) - root.ui.modals.curio_overlay.pick_confirmed.connect(root._bonus.on_pick_confirmed) - root.ui.modals.curio_overlay.continue_pressed.connect(root._bonus.on_continue) - root.ui.modals.curio_overlay.refresh_requested.connect(root._bonus.on_refresh) + root.ui.modals.run.curio_overlay.pick_confirmed.connect(root._bonus.on_pick_confirmed) + root.ui.modals.run.curio_overlay.continue_pressed.connect(root._bonus.on_continue) + root.ui.modals.run.curio_overlay.refresh_requested.connect(root._bonus.on_refresh) static func _build_menu_wiring(root: Variant) -> void: # 菜单屏接线(句柄经屏壳暴露;三入口化——进入存档/设置/退出游戏,卡② mn3: @@ -192,17 +192,17 @@ static func _build_menu_wiring(root: Variant) -> void: root._camp_pres = CampShopPresenter.new(CampFlowContext.new(root.db, {"camp_scene": root.ui.modals.camp.scene, "forge_view": root.ui.modals.camp.forge, "treasure_view": root.ui.modals.camp.treasure, "tree_view": root.ui.modals.camp.tree, - "codex_view": root.ui.modals.camp.codex, "shop_overlay": root.ui.modals.shop_overlay, + "codex_view": root.ui.modals.camp.codex, "shop_overlay": root.ui.modals.menu.shop_overlay, "toast": root.ui.battle.board_zone.toast, "start_overlay": root.ui.screens.start_overlay, - "save_select_overlay": root.ui.modals.save_select_overlay, "logger": root.logger, + "save_select_overlay": root.ui.modals.menu.save_select_overlay, "logger": root.logger, "refresh_bus": camp_bus}, {"meta_get": func() -> Variant: return root.meta, "save_get": func() -> Variant: return root.save_manager, "runner_get": func() -> Variant: return root.run_scope.runner}, - {"open_modal": root.ui.modals.flow.open, "close_modal": root.ui.modals.flow.close, + {"open_modal": root.ui.flow.open, "close_modal": root.ui.flow.close, "refresh_presentations": root._refresh_presentations})) # 构造参数收拢 context(u21 三束) - root.ui.modals.shop_overlay.buy_requested.connect(root._camp_pres.on_shop_buy) - root.ui.modals.shop_overlay.refresh_requested.connect(root._camp_pres.on_shop_refresh) # MC-1 - root.ui.modals.shop_overlay.leave_pressed.connect(root._camp_pres.on_shop_leave) + root.ui.modals.menu.shop_overlay.buy_requested.connect(root._camp_pres.on_shop_buy) + root.ui.modals.menu.shop_overlay.refresh_requested.connect(root._camp_pres.on_shop_refresh) # MC-1 + root.ui.modals.menu.shop_overlay.leave_pressed.connect(root._camp_pres.on_shop_leave) # 营地信号接线(CampOverlay 退役——四子界面意图信号+场景点选组合根路由+closed 经 bind 携视图) root.ui.modals.camp.forge.forge_buy_requested.connect(root._camp_pres.on_camp_forge_buy) root.ui.modals.camp.forge.forge_unlock_requested.connect(root._camp_pres.on_camp_forge_unlock) @@ -229,13 +229,13 @@ static func _build_debug_and_resolvers(root: Variant) -> void: #(纪律「登记序 = 树序 = z 序」——confirm_dialog.open 禁运行时 move_child 自提抬层; # 弹窗只在开始菜单域弹出,绘制序恒最上; # 诊断转储入口在调试浮窗) - root.ui.modals.confirm_dialog = ConfirmDialog.new() - root.add_child(root.ui.modals.confirm_dialog) + root.ui.modals.menu.confirm_dialog = ConfirmDialog.new() + root.add_child(root.ui.modals.menu.confirm_dialog) # 消费方编排(卡③ 契约 3/sl8——禁双常驻静态接线):open 前设 _confirm_consumer, # confirmed/cancelled 后按该状态量分派;分派面可容纳后续消费方(卡④ 到位直接接入不返工) - root.ui.modals.confirm_dialog.confirmed.connect(root._on_confirm_accepted) - root.ui.modals.confirm_dialog.secondary_confirmed.connect(root._on_confirm_secondary) # 三钮次确认(卡④ 大门「开始新冒险」出口) - root.ui.modals.confirm_dialog.cancelled.connect(root._on_confirm_declined) + root.ui.modals.menu.confirm_dialog.confirmed.connect(root._on_confirm_accepted) + root.ui.modals.menu.confirm_dialog.secondary_confirmed.connect(root._on_confirm_secondary) # 三钮次确认(卡④ 大门「开始新冒险」出口) + root.ui.modals.menu.confirm_dialog.cancelled.connect(root._on_confirm_declined) # Esc 浮窗三出口接线(卡⑤——意图信号经薄委托归 MainEscFlow 动线;输入入口=main._unhandled_input) root.ui.modals.camp.esc_overlay.return_requested.connect(root._on_esc_return) root.ui.modals.camp.esc_overlay.settings_requested.connect(root._on_esc_settings) @@ -246,15 +246,15 @@ static func _build_debug_and_resolvers(root: Variant) -> void: # 诊断转储入口在本浮窗,玩家画面零常驻调试件)。 # 树序在开始层与确认弹窗之后 = 浮于菜单之上的 Dialog(不隐藏开始层——与确认弹窗同款形态); # 开窗唯一入口 = _show_debug_overlay(C6 单口——角标/探针/启动路径全收口) - root.ui.modals.debug_overlay = DebugOverlay.new(root.db, int(root.db.combat.hero.atk), int(root.db.combat.hero.max_hp)) - root.add_child(root.ui.modals.debug_overlay) - root.ui.modals.debug_overlay.closed.connect(root.ui.modals.flow.close.bind(root.ui.modals.debug_overlay)) - root.ui.modals.debug_overlay.apply_requested.connect(root._on_tune_apply) - root.ui.modals.debug_overlay.simulate_requested.connect(root._on_tune_simulate) - root.ui.modals.debug_overlay.debug_levels_requested.connect(root._start_debug_levels) + root.ui.modals.menu.debug_overlay = DebugOverlay.new(root.db, int(root.db.combat.hero.atk), int(root.db.combat.hero.max_hp)) + root.add_child(root.ui.modals.menu.debug_overlay) + root.ui.modals.menu.debug_overlay.closed.connect(root.ui.flow.close.bind(root.ui.modals.menu.debug_overlay)) + root.ui.modals.menu.debug_overlay.apply_requested.connect(root._on_tune_apply) + root.ui.modals.menu.debug_overlay.simulate_requested.connect(root._on_tune_simulate) + root.ui.modals.menu.debug_overlay.debug_levels_requested.connect(root._start_debug_levels) # 棋盘解析回调族(有态绑定件——board_resolvers.gd 承接;组合根只接线;局部持有不占类成员位) - var resolvers = BoardResolversScript.new(root.db, root.ui.modals.debug_overlay, func() -> Variant: return root.run_scope.settlement) + var resolvers = BoardResolversScript.new(root.db, root.ui.modals.menu.debug_overlay, func() -> Variant: return root.run_scope.settlement) # 奖励徽章数据源:地图战斗资源 身份解析只读回调(已开格身份 icon——挖掘期 Active 相位内有效) root.ui.battle.board_zone.board_view.set_identity_resolver(resolvers.identity_lookup) # 数字遮蔽数据源:地形变异 hide_number 解析只读回调(信息型呈现——board_view 渲染面读取) @@ -271,21 +271,21 @@ static func _build_modal_flow(root: Variant) -> void: # _apply_layer_contract 统一落位(关卡画面宿住冒险屏壳、营地屏壳夹商店与设置层之间、 # 三选一对位终局层之下等特例均在合同数据内表达,禁散落 move_child) # 模态编排注入(ModalFlow 纯逻辑类:依赖仅 ModalStack+查询 Callable——实例随 ui.modals 容器构造建立) - root.ui.modals.flow.start_visible_get = func() -> bool: return root.ui.screens.start_overlay.visible - root.ui.modals.flow.inter_visible_get = func() -> bool: return root.ui.screens.inter_overlay.visible - root.ui.modals.flow.settlement_get = func() -> Variant: return root.run_scope.settlement - root.ui.modals.flow.rows_empty_get = func() -> bool: return root.ui.screens.row_pacer.pending_rows().is_empty() - root.ui.modals.flow.flush_rows = root.ui.screens.row_pacer.flush - root.ui.modals.flow.on_open_sync = func() -> void: + root.ui.flow.start_visible_get = func() -> bool: return root.ui.screens.start_overlay.visible + root.ui.flow.inter_visible_get = func() -> bool: return root.ui.screens.inter_overlay.visible + root.ui.flow.settlement_get = func() -> Variant: return root.run_scope.settlement + root.ui.flow.rows_empty_get = func() -> bool: return root.ui.screens.row_pacer.pending_rows().is_empty() + root.ui.flow.flush_rows = root.ui.screens.row_pacer.flush + root.ui.flow.on_open_sync = func() -> void: root._sync_status_entry() # 状态面板入口角标可见性/置灰随开态同步 root._sync_debug_overlay() # 浮窗当局数据/按钮态随开态同步 - root.ui.modals.flow.on_close_sync = func() -> void: + root.ui.flow.on_close_sync = func() -> void: root._sync_status_entry() # 关闭恢复角标可点态(无残留——同源口径) root._sync_debug_overlay() # 他层关闭复活浮窗时同步刷新(关后自动恢复语义配套) - root.ui.modals.flow.replay_click = root._replay_locked_click + root.ui.flow.replay_click = root._replay_locked_click # 视觉层合同装配(唯一事实源=LAYER_CONTRACT:树序+登记序同源派生——改序只改合同表) root.ui.apply_layer_contract(root) - root._tune_sim = TuneSimController.new(root.db, root.ui.modals.debug_overlay, int(root.db.combat.hero.max_hp)) + root._tune_sim = TuneSimController.new(root.db, root.ui.modals.menu.debug_overlay, int(root.db.combat.hero.max_hp)) static func fatal_screen(root: Variant, msg: String) -> void: var l := UiKit.label(msg,UiKit.FS_SUB, true, UiKit.RED_ALERT) diff --git a/game/presentation/ui_camp_modals.gd b/game/presentation/ui_camp_modals.gd index 69cbd2a3..22db9832 100644 --- a/game/presentation/ui_camp_modals.gd +++ b/game/presentation/ui_camp_modals.gd @@ -1,6 +1,6 @@ ## 营地交互域模态句柄子容器:持营地域全部模态句柄(终态六枚=营地场景根+锻造/秘宝/天赋树/图鉴四子界面+Esc 浮窗)。 -## RefCounted 不挂树;随 UiModals._init 构造(与 flow 同拍)。Esc 浮窗枚随卡⑤落——禁空占位凑结构。 -## 新营地句柄禁平铺回 ui_modals:营地域句柄寻址一律 ui.modals.camp.X 二层(模态句柄册分域纪律——全量分家另立批)。 +## RefCounted 不挂树;随 UiModals._init 构造(flow 同拍升格由 UiRegistry._init 直驻)。Esc 浮窗枚随卡⑤落——禁空占位凑结构。 +## 新营地句柄禁平铺回 ui_modals:营地域句柄寻址一律 ui.modals.camp.X 二层(模态句柄册分域纪律——册级全量分家已落地)。 class_name UiCampModals extends RefCounted diff --git a/game/presentation/ui_menu_modals.gd b/game/presentation/ui_menu_modals.gd new file mode 100644 index 00000000..5f78a77d --- /dev/null +++ b/game/presentation/ui_menu_modals.gd @@ -0,0 +1,11 @@ +## 菜单与跨语境域六句柄唯一容器——菜单层与多语境共用模态的句柄册(纯字段 RefCounted)。 +## 分域纪律册级化:任何模态句柄禁平铺回 ui_modals(营地先例条款推广)——寻址一律 ui.modals.menu.X 二段式。 +class_name UiMenuModals +extends RefCounted + +var shop_overlay: ShopOverlay # 商店(冒险商店节点+营地商店跨语境) +var settings_overlay: SettingsOverlay # 设置(开始层+营地 Esc 双路径跨语境) +var career_overlay: CareerOverlay # 生涯记录(开始层笔记本+营地语境跨语境) +var save_select_overlay: SaveSelectOverlay # 选档(开始层+营地大门流跨语境) +var confirm_dialog: ConfirmDialog # 确认框(全语境) +var debug_overlay: DebugOverlay # 调试浮窗(全语境,non_locking) diff --git a/game/presentation/ui_menu_modals.gd.uid b/game/presentation/ui_menu_modals.gd.uid new file mode 100644 index 00000000..01cba150 --- /dev/null +++ b/game/presentation/ui_menu_modals.gd.uid @@ -0,0 +1 @@ +uid://byakojaxcljbw diff --git a/game/presentation/ui_modals.gd b/game/presentation/ui_modals.gd index e068d317..5cd1a3b3 100644 --- a/game/presentation/ui_modals.gd +++ b/game/presentation/ui_modals.gd @@ -1,33 +1,17 @@ -## 模态层域句柄册:十二个平铺模态句柄+营地交互域子容器(camp)+模态栈编排(ModalFlow——登记/开合/输入锁谓词)的注册面域容器。 -## RefCounted 不挂树;模态登记唯一口=register_layer(anchors 撑满宿主+排入 ModalStack)。 -## flow 随容器构造即建(装配早期注入点经 root.ui.modals.flow.* 取 Callable——晚于 flow 建立时点)。 +## 模态层域句柄册:三子容器(run=局内冒险域六句柄/menu=菜单与跨语境域六句柄/camp=营地域六句柄) +## 的纯字段容器——与 UiScreens/UiBattle 同型。 +## 分域纪律全册化:任何模态句柄禁平铺回本册(营地先例条款推广)——子容器成员寻址一律 ui.modals.<域>.X 二段式。 +## RefCounted 不挂树;模态登记唯一口=register_layer 已迁驻注册面 ui_registry(登记职责随编排域—— +## ModalFlow 升格由 UiRegistry._init 直驻,本册无 flow 成员)。 class_name UiModals extends RefCounted -var flow: ModalFlow -var legend_overlay: LegendOverlay -var node_overlay: NodeOverlay -var level_screen: LevelMapScreen -var curio_overlay: CurioPickOverlay -var end_overlay: BattleEndOverlay -var shop_overlay: ShopOverlay +var run: UiRunModals +var menu: UiMenuModals var camp: UiCampModals -var settings_overlay: SettingsOverlay -var status_panel: StatusPanel -var career_overlay: CareerOverlay -var save_select_overlay: SaveSelectOverlay -var confirm_dialog: ConfirmDialog -var debug_overlay: DebugOverlay func _init() -> void: - flow = ModalFlow.new() # 查询/钩子 Callable 由 _build_modal_flow 注入(注入时点不变——注入值面向已在座实例) + run = UiRunModals.new() + menu = UiMenuModals.new() camp = UiCampModals.new() # 营地域句柄子容器(分域纪律——营地域句柄禁平铺回本册) - - -func register_layer(m: Control, tags: Array = []) -> void: - # 撑满宿主(Control 默认锚 = 0×0——全屏变暗层与居中布局整体失效;route 选路层预览截图实证后 - # 统一收口:六个模态同病,探针经信号直发绕过真实点击从未暴露。anchors_and_offsets 而非 anchors - # ——后者会把控件重置回最小内容尺寸,ui_assembler 头注同款纪律) - m.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) - flow.register(m, tags) # 排除组随层登记(non_locking=调试浮窗/screen_state=关卡地图屏) diff --git a/game/presentation/ui_registry.gd b/game/presentation/ui_registry.gd index ad4db46b..f8b9ad8c 100644 --- a/game/presentation/ui_registry.gd +++ b/game/presentation/ui_registry.gd @@ -1,6 +1,7 @@ ## 界面注册面:呈现层界面元素清单的唯一事实源——哪些层、什么序、地址在哪。 -## 持有三域容器(screens/battle/modals)、LAYER_CONTRACT 合同表、node(key) 合同寻址、 -## apply_layer_contract(root) 唯一落位口(全仓唯一 move_child 点)。 +## 持有三域容器(screens/battle/modals)+模态编排件 ModalFlow 直驻(自 UiModals 升格——构造时点 +## 与今等价:注册面构造点先于装配)、模态登记唯一口 register_layer(随登记职责迁驻)、 +## LAYER_CONTRACT 合同表、node(key) 合同寻址、apply_layer_contract(root) 唯一落位口(全仓唯一 move_child 点)。 ## RefCounted 不挂树:零信号连接、零会话期引用、零核心写调用——组合根单点装配不变。 class_name UiRegistry extends RefCounted @@ -42,12 +43,23 @@ const LAYER_CONTRACT: Array[Dictionary] = [ var screens: UiScreens var battle: UiBattle var modals: UiModals +var flow: ModalFlow # 模态编排件直驻注册面(升格自 UiModals——查询/钩子 Callable 由 _build_modal_flow 注入,注入时点不变) func _init() -> void: screens = UiScreens.new() battle = UiBattle.new() modals = UiModals.new() + flow = ModalFlow.new() + + +# 模态登记唯一口(自 ui_modals.gd 逐字迁驻——登记职责随编排域归注册面单点) +func register_layer(m: Control, tags: Array = []) -> void: + # 撑满宿主(Control 默认锚 = 0×0——全屏变暗层与居中布局整体失效;route 选路层预览截图实证后 + # 统一收口:六个模态同病,探针经信号直发绕过真实点击从未暴露。anchors_and_offsets 而非 anchors + # ——后者会把控件重置回最小内容尺寸,ui_assembler 头注同款纪律) + m.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + flow.register(m, tags) # 排除组随层登记(non_locking=调试浮窗/screen_state=关卡地图屏) # 合同层寻址(key→节点;纯查表不含层序语义——层序唯一事实源=LAYER_CONTRACT) @@ -56,12 +68,12 @@ func node(key: String) -> Node: "adventure_screen": return screens.adventure_screen "menu_screen": return screens.menu_screen "row_pacer": return screens.row_pacer - "legend_overlay": return modals.legend_overlay - "node_overlay": return modals.node_overlay - "level_screen": return modals.level_screen - "curio_overlay": return modals.curio_overlay - "end_overlay": return modals.end_overlay - "shop_overlay": return modals.shop_overlay + "legend_overlay": return modals.run.legend_overlay + "node_overlay": return modals.run.node_overlay + "level_screen": return modals.run.level_screen + "curio_overlay": return modals.run.curio_overlay + "end_overlay": return modals.run.end_overlay + "shop_overlay": return modals.menu.shop_overlay "camp_screen": return screens.camp_screen "camp_scene": return modals.camp.scene "forge_view": return modals.camp.forge @@ -69,13 +81,13 @@ func node(key: String) -> Node: "tree_view": return modals.camp.tree "codex_view": return modals.camp.codex "camp_esc_overlay": return modals.camp.esc_overlay - "settings_overlay": return modals.settings_overlay - "status_panel": return modals.status_panel - "career_overlay": return modals.career_overlay - "save_select_overlay": return modals.save_select_overlay + "settings_overlay": return modals.menu.settings_overlay + "status_panel": return modals.run.status_panel + "career_overlay": return modals.menu.career_overlay + "save_select_overlay": return modals.menu.save_select_overlay "inter_overlay": return screens.inter_overlay - "confirm_dialog": return modals.confirm_dialog - "debug_overlay": return modals.debug_overlay + "confirm_dialog": return modals.menu.confirm_dialog + "debug_overlay": return modals.menu.debug_overlay "debug_badge": return screens.debug_badge return null @@ -94,7 +106,7 @@ func apply_layer_contract(root: Control) -> void: root.move_child(shell, slot) slot += 1 if member != null: - modals.register_layer(member as Control, tags) + register_layer(member as Control, tags) else: push_error("[合同装配] 层缺失: %s" % String(entry["key"])) continue @@ -103,9 +115,9 @@ func apply_layer_contract(root: Control) -> void: push_error("[合同装配] 层缺失: %s" % String(entry["key"])) continue if entry.has("host"): - modals.register_layer(layer as Control, tags) + register_layer(layer as Control, tags) continue root.move_child(layer, slot) slot += 1 if String(entry["kind"]) == "modal": - modals.register_layer(layer as Control, tags) + register_layer(layer as Control, tags) diff --git a/game/presentation/ui_run_modals.gd b/game/presentation/ui_run_modals.gd new file mode 100644 index 00000000..23cd57a4 --- /dev/null +++ b/game/presentation/ui_run_modals.gd @@ -0,0 +1,11 @@ +## 局内冒险域六句柄唯一容器——冒险会话内开合的模态呈现面句柄册(纯字段 RefCounted)。 +## 分域纪律册级化:任何模态句柄禁平铺回 ui_modals(营地先例条款推广)——寻址一律 ui.modals.run.X 二段式。 +class_name UiRunModals +extends RefCounted + +var legend_overlay: LegendOverlay # 战斗中图例(棋盘角标触发) +var node_overlay: NodeOverlay # 节点结算窗 +var level_screen: LevelMapScreen # 关卡地图屏(screen_state 全屏态) +var curio_overlay: CurioPickOverlay # 奖励三选一 +var end_overlay: BattleEndOverlay # 终局面板 +var status_panel: StatusPanel # 状态面板(地图/棋盘两角标入口) diff --git a/game/presentation/ui_run_modals.gd.uid b/game/presentation/ui_run_modals.gd.uid new file mode 100644 index 00000000..5e0f9065 --- /dev/null +++ b/game/presentation/ui_run_modals.gd.uid @@ -0,0 +1 @@ +uid://ssh8gogewptf diff --git a/game/probe/fx_shot_probe.gd b/game/probe/fx_shot_probe.gd index eed8663e..14075ee3 100644 --- a/game/probe/fx_shot_probe.gd +++ b/game/probe/fx_shot_probe.gd @@ -37,14 +37,14 @@ func _ready() -> void: # 等关卡画面出图(choices_presented 异步到达——最多 3s 轮询) for i in 60: await get_tree().process_frame - if main.ui.modals.level_screen != null and main.ui.modals.level_screen.visible: + if main.ui.modals.run.level_screen != null and main.ui.modals.run.level_screen.visible: break - if main.ui.modals.level_screen == null or not main.ui.modals.level_screen.visible: + if main.ui.modals.run.level_screen == null or not main.ui.modals.run.level_screen.visible: _fail("关卡画面未出现(函数直调中间态路径)") return for i in 5: await get_tree().process_frame - main.ui.modals.level_screen.press_candidate(0) + main.ui.modals.run.level_screen.press_candidate(0) for i in 20: await get_tree().process_frame var st = main.run_scope.settlement diff --git a/game/probe/layout_probe.gd b/game/probe/layout_probe.gd index 0346ae24..cdeebcdc 100644 --- a/game/probe/layout_probe.gd +++ b/game/probe/layout_probe.gd @@ -24,12 +24,12 @@ func _ready() -> void: add_child(main) await get_tree().process_frame ProbeUiUtil.isolate_save(main, "smoke_layout_probe", true, false, true) - main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(确定性禁依赖默认随机分支) + main.ui.modals.menu.debug_overlay.seed_edit.text = "42" # 种子钉死(确定性禁依赖默认随机分支) main._start_adventure_from_menu() for i in range(8): await get_tree().process_frame # 进战斗屏——三区(B 战斗画面/C 角色信息/D 扫雷图)同屏常驻,菜单屏不构成断言面 - main.ui.modals.level_screen.press_candidate(0) + main.ui.modals.run.level_screen.press_candidate(0) for i in range(20): await get_tree().process_frame for vp in VIEWPORTS: diff --git a/game/probe/probe_ui_util.gd b/game/probe/probe_ui_util.gd index 27b614b9..f885dfb2 100644 --- a/game/probe/probe_ui_util.gd +++ b/game/probe/probe_ui_util.gd @@ -130,7 +130,7 @@ static func open_camp_via_savesel(main, tag: String) -> bool: if FileAccess.file_exists(sp): DirAccess.remove_absolute(sp) main._open_save_select() - var new_btn: Button = main.ui.modals.save_select_overlay._new_btns.get(1) + var new_btn: Button = main.ui.modals.menu.save_select_overlay._new_btns.get(1) if new_btn == null: return false new_btn.pressed.emit() # 真实按钮信号路径 → _on_save_select_new → create_slot+open_camp diff --git a/game/probe/screen_flow_probe.gd b/game/probe/screen_flow_probe.gd index b2232b59..ef87e290 100644 --- a/game/probe/screen_flow_probe.gd +++ b/game/probe/screen_flow_probe.gd @@ -54,7 +54,7 @@ func _screen_seals() -> Dictionary: "菜单": main.ui.screens.start_overlay != null and main.ui.screens.start_overlay.visible, "营地": main.ui.modals.camp.scene != null and main.ui.modals.camp.scene.visible, "冒险": main.run_scope != null and main.run_scope.is_adventure, - "终局": main.ui.modals.end_overlay != null and main.ui.modals.end_overlay.visible, + "终局": main.ui.modals.run.end_overlay != null and main.ui.modals.run.end_overlay.visible, } @@ -102,7 +102,7 @@ func _frames(n: int = 2) -> void: func _step_boot_menu() -> void: _expect_exclusive("①boot", "菜单") _expect_adventure_seal("①boot", false) - if main.ui.modals.debug_overlay != null and main.ui.modals.debug_overlay.visible: + if main.ui.modals.menu.debug_overlay != null and main.ui.modals.menu.debug_overlay.visible: _problems.append("①boot:调试浮窗 boot 自动开(菜单优先失守)") @@ -123,9 +123,9 @@ func _step_camp_back_to_menu() -> void: return main.ui.modals.camp.scene.object_pressed.emit(&"campfire") await _frames() - if not main.ui.modals.confirm_dialog.visible: + if not main.ui.modals.menu.confirm_dialog.visible: _problems.append("③篝火:二次确认弹窗未现(camp.md v2.2 必须先确认)〔营地图批 V7〕") - main.ui.modals.confirm_dialog.confirmed.emit() # 二次确认离场(confirmed 先于关层——卡④ T4) + main.ui.modals.menu.confirm_dialog.confirmed.emit() # 二次确认离场(confirmed 先于关层——卡④ T4) await _frames() _expect_exclusive("③营地→菜单(回退)", "菜单") @@ -143,15 +143,15 @@ func _step_save_select_open_back() -> void: esb.pressed.emit() # 真实按钮信号路径 → _open_save_select(卡② mn3 点击链——卡③ 直调中间态就此闭环) await get_tree().process_frame var seals := _screen_seals() - if not main.ui.modals.save_select_overlay.visible or not seals_check_single(seals): - _problems.append("[savesel] sl10 开层后屏级互斥失守(选档=%s)" % str(main.ui.modals.save_select_overlay.visible)) - var back_btn: Button = ProbeUiUtil.find_button(main.ui.modals.save_select_overlay, "返回", "prefix") + if not main.ui.modals.menu.save_select_overlay.visible or not seals_check_single(seals): + _problems.append("[savesel] sl10 开层后屏级互斥失守(选档=%s)" % str(main.ui.modals.menu.save_select_overlay.visible)) + var back_btn: Button = ProbeUiUtil.find_button(main.ui.modals.menu.save_select_overlay, "返回", "prefix") if back_btn == null: _problems.append("[savesel] sl10 返回钮缺失(页壳顶条)") else: back_btn.pressed.emit() await get_tree().process_frame - if main.ui.modals.save_select_overlay.visible or not main.ui.screens.start_overlay.visible: + if main.ui.modals.menu.save_select_overlay.visible or not main.ui.screens.start_overlay.visible: _problems.append("[savesel] sl10 返回后选档未隐藏或开始层未恢复(关闭链失守)") # ESC 语境:再开选档→合成 ESC 键→层不关、流程零扰动(ESC 不属选档语境——零实现断言) main._open_save_select() @@ -162,7 +162,7 @@ func _step_save_select_open_back() -> void: ev.pressed = true main.get_viewport().push_input(ev, true) await get_tree().process_frame - if not main.ui.modals.save_select_overlay.visible: + if not main.ui.modals.menu.save_select_overlay.visible: _problems.append("[savesel] sl10 ESC 关闭了选档层(选档语境 ESC 应不响应——卡⑤ 范围外)") main._on_save_select_closed() await get_tree().process_frame @@ -174,7 +174,7 @@ func seals_check_single(f: Dictionary) -> bool: for k in f: if bool(f[k]): on += 1 - if main.ui.modals.save_select_overlay.visible: + if main.ui.modals.menu.save_select_overlay.visible: on += 1 # 选档自身计入(调用点已开层) return on == 1 @@ -187,25 +187,25 @@ func _step_menu_to_adventure() -> void: _expect_adventure_seal("④菜单→冒险", true) if main.run_scope != null and main.run_scope.adventure == null: _problems.append("④菜单→冒险:冒险实例未建立(入口停点未达)") - if main.ui.modals.level_screen != null and not main.ui.modals.level_screen.visible: + if main.ui.modals.run.level_screen != null and not main.ui.modals.run.level_screen.visible: _problems.append("④菜单→冒险:入口停点未落在关卡地图屏(level_screen 不可见)") # ---- ⑤ 冒险内屏切换:关卡地图屏 ⇄ 战斗屏(屏内两态,屏级仍独占)---- func _step_level_to_battle_and_back() -> void: - if not main.ui.modals.level_screen.visible: + if not main.ui.modals.run.level_screen.visible: _problems.append("⑤冒险内切换:前置不在关卡地图屏") return main._exit_level_screen() await _frames() - if main.ui.modals.level_screen.visible: + if main.ui.modals.run.level_screen.visible: _problems.append("⑤冒险内切换:退出关卡屏后 level_screen 仍可见") if main.ui.screens.battle_root != null and not main.ui.screens.battle_root.visible: _problems.append("⑤冒险内切换:战斗屏未接手(battle_root 不可见)") _expect_exclusive("⑤冒险内切换(关卡→战斗)", "冒险") main._enter_level_screen() await _frames() - if not main.ui.modals.level_screen.visible: + if not main.ui.modals.run.level_screen.visible: _problems.append("⑤冒险内切换:回到关卡地图屏失败") _expect_exclusive("⑤冒险内切换(战斗→关卡)", "冒险") @@ -218,16 +218,16 @@ func _step_abandon_to_end() -> void: main._on_abandon_requested() await _frames(4) _expect_exclusive("⑥放弃→终局", "终局") - if main.ui.modals.end_overlay != null and not main.ui.modals.end_overlay.visible: + if main.ui.modals.run.end_overlay != null and not main.ui.modals.run.end_overlay.visible: _problems.append("⑥放弃→终局:终局覆盖层未开") # ---- ⑦ 终局 → 菜单(终局出口返回开始界面)---- func _step_end_back_to_menu() -> void: - if main.ui.modals.end_overlay == null or not main.ui.modals.end_overlay.visible: + if main.ui.modals.run.end_overlay == null or not main.ui.modals.run.end_overlay.visible: _problems.append("⑦终局→菜单:前置不在终局") return - main.ui.modals.end_overlay.continue_pressed.emit() + main.ui.modals.run.end_overlay.continue_pressed.emit() await _frames(3) _expect_exclusive("⑦终局→菜单", "菜单") if main.run_scope != null and main.run_scope.is_adventure: @@ -273,7 +273,7 @@ func _step_camp_esc_and_hud() -> void: await _frames() main._on_esc_settings() await _frames() - if not main.ui.modals.settings_overlay.visible: + if not main.ui.modals.menu.settings_overlay.visible: _problems.append("[esc] 浮窗设置出口断裂〔ESC批 V4〕") _push_esc() await _frames() @@ -286,7 +286,7 @@ func _step_camp_esc_and_hud() -> void: # 返回主菜单动线(契约 4——浮窗自收+营地隐+选档收+菜单显) main.ui.modals.camp.esc_overlay.return_requested.emit() await _frames() - if main.ui.modals.camp.esc_overlay.visible or main.ui.modals.camp.scene.visible or main.ui.modals.save_select_overlay.visible or not main.ui.screens.start_overlay.visible: + if main.ui.modals.camp.esc_overlay.visible or main.ui.modals.camp.scene.visible or main.ui.modals.menu.save_select_overlay.visible or not main.ui.screens.start_overlay.visible: _problems.append("[esc] 返回主菜单动线失守(浮窗自收/营地隐/选档收/菜单显)〔ESC批 V3〕〔ESC批 B3〕") # 非营地语境(boot 菜单)Esc→无响应(契约 9④ 回归面) _push_esc() diff --git a/game/probe/shot_probe.gd b/game/probe/shot_probe.gd index 90184d93..d7e92577 100644 --- a/game/probe/shot_probe.gd +++ b/game/probe/shot_probe.gd @@ -52,9 +52,9 @@ func _ready() -> void: var top_rx := -1 var top_ry := -1 for c: Node in [main.ui.screens.start_overlay, main.ui.screens.battle_root, main.ui.screens.inter_overlay, - main.ui.modals.legend_overlay, main.ui.modals.node_overlay, main.ui.modals.level_screen, main.ui.modals.curio_overlay, - main.ui.modals.end_overlay, main.ui.modals.shop_overlay, main.ui.modals.camp.scene, main.ui.modals.settings_overlay, - main.ui.modals.status_panel, main.ui.modals.career_overlay, main.ui.modals.confirm_dialog, main.ui.modals.debug_overlay]: + main.ui.modals.run.legend_overlay, main.ui.modals.run.node_overlay, main.ui.modals.run.level_screen, main.ui.modals.run.curio_overlay, + main.ui.modals.run.end_overlay, main.ui.modals.menu.shop_overlay, main.ui.modals.camp.scene, main.ui.modals.menu.settings_overlay, + main.ui.modals.run.status_panel, main.ui.modals.menu.career_overlay, main.ui.modals.menu.confirm_dialog, main.ui.modals.menu.debug_overlay]: if c == null or not (c is CanvasItem) or not (c as CanvasItem).visible: continue var rx: int = c.get_parent().get_index() @@ -89,12 +89,12 @@ func _ready() -> void: setbtn.pressed.emit() for i in 3: await get_tree().process_frame - if not main.ui.modals.settings_overlay.visible: + if not main.ui.modals.menu.settings_overlay.visible: _problems.append("设置面未打开(settingsbtn 路径)") else: - _meta["settings_title"] = ProbeUiUtil.label_rect(main.ui.modals.settings_overlay, "设置") + _meta["settings_title"] = ProbeUiUtil.label_rect(main.ui.modals.menu.settings_overlay, "设置") await _snap("settings") - var set_close: Button = ProbeUiUtil.find_button(main.ui.modals.settings_overlay, "返回", "prefix") + var set_close: Button = ProbeUiUtil.find_button(main.ui.modals.menu.settings_overlay, "返回", "prefix") if set_close == null: _problems.append("设置面返回钮缺失") else: @@ -105,19 +105,19 @@ func _ready() -> void: main._open_career() for i in 3: await get_tree().process_frame - if not main.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible: + if not main.ui.modals.menu.career_overlay.visible or main.ui.screens.start_overlay.visible: _problems.append("生涯页未打开或开始层未隐藏(函数直调中间态路径)") else: - _meta["career_title"] = ProbeUiUtil.label_rect(main.ui.modals.career_overlay, "生 涯") + _meta["career_title"] = ProbeUiUtil.label_rect(main.ui.modals.menu.career_overlay, "生 涯") await _snap("career") - var career_close: Button = ProbeUiUtil.find_button(main.ui.modals.career_overlay, "返回", "prefix") + var career_close: Button = ProbeUiUtil.find_button(main.ui.modals.menu.career_overlay, "返回", "prefix") if career_close == null: _problems.append("生涯页返回钮缺失") else: career_close.pressed.emit() for i in 3: await get_tree().process_frame - if main.ui.modals.career_overlay.visible or not main.ui.screens.start_overlay.visible: + if main.ui.modals.menu.career_overlay.visible or not main.ui.screens.start_overlay.visible: _problems.append("生涯页关闭后页未隐藏或开始层未恢复") # ② 营地场景图(进档开层新路径——卡② mn9;截图=可点击场景图形态——卡④ 契约 9 V1 验收载体) if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_shot_probe_camp"): @@ -127,7 +127,7 @@ func _ready() -> void: await _snap("camp") main.ui.modals.camp.scene.object_pressed.emit(&"campfire") # 收闭改接篝火动线(营地根态无关闭钮——卡④ T5) await get_tree().process_frame - main.ui.modals.confirm_dialog.confirmed.emit() # 二次确认离场(关层收闭——选档层随收) + main.ui.modals.menu.confirm_dialog.confirmed.emit() # 二次确认离场(关层收闭——选档层随收) await get_tree().process_frame # ③ 关卡画面(开始冒险→停点出图——入口停点语义;函数直调中间态 mn10) main._start_adventure_from_menu() @@ -135,16 +135,16 @@ func _ready() -> void: await get_tree().process_frame await _snap("level") # GP 常驻徽章断言(读数=runner.gp 现值,刷新=写路径回执后重查〔open_choices〕) - var gp_label: Label = ProbeUiUtil.find_label(main.ui.modals.level_screen, "◈ ") + var gp_label: Label = ProbeUiUtil.find_label(main.ui.modals.run.level_screen, "◈ ") if gp_label == null or not gp_label.is_visible_in_tree(): _problems.append("GP 徽章不可见(关卡画面顶栏)") - elif main.ui.modals.level_screen.get("_runner") != null: - var expect := "◈ %d" % int(main.ui.modals.level_screen.get("_runner").gp) + elif main.ui.modals.run.level_screen.get("_runner") != null: + var expect := "◈ %d" % int(main.ui.modals.run.level_screen.get("_runner").gp) if gp_label.text != expect: _problems.append("GP 徽章读数不符: %s != %s" % [gp_label.text, expect]) # ④ 战斗(节点一——真实选路提交路径 press_candidate,ui_click_probe :199 同款) - if main.ui.modals.level_screen != null and main.ui.modals.level_screen.visible: - main.ui.modals.level_screen.press_candidate(0) + if main.ui.modals.run.level_screen != null and main.ui.modals.run.level_screen.visible: + main.ui.modals.run.level_screen.press_candidate(0) for i in 10: await get_tree().process_frame await _snap("battle") @@ -167,18 +167,18 @@ func _ready() -> void: pb.pressed.emit() for i in 3: await get_tree().process_frame - if not main.ui.modals.status_panel.visible: + if not main.ui.modals.run.status_panel.visible: _problems.append("状态面板未打开(角标路径)") else: - _meta["status_title"] = ProbeUiUtil.label_rect(main.ui.modals.status_panel, "状态") + _meta["status_title"] = ProbeUiUtil.label_rect(main.ui.modals.run.status_panel, "状态") await _snap("status") - var status_close: Button = ProbeUiUtil.find_button(main.ui.modals.status_panel, "继续", "prefix") + var status_close: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix") if status_close == null: _problems.append("状态面板继续钮缺失") else: status_close.pressed.emit() await get_tree().process_frame - if main.ui.modals.status_panel.visible: + if main.ui.modals.run.status_panel.visible: _problems.append("状态面板关闭后未隐藏(继续钮路径)") else: _problems.append("关卡画面未出现(函数直调中间态路径)") diff --git a/game/probe/t17_defer_probe.gd b/game/probe/t17_defer_probe.gd index 53c1b4b7..086adf3c 100644 --- a/game/probe/t17_defer_probe.gd +++ b/game/probe/t17_defer_probe.gd @@ -29,14 +29,14 @@ func _ready() -> void: main._start_adventure_from_menu() # 函数直调中间态(卡② mn10——开始冒险钮已撤) for i in 60: await get_tree().process_frame - if main.ui.modals.level_screen != null and main.ui.modals.level_screen.visible: + if main.ui.modals.run.level_screen != null and main.ui.modals.run.level_screen.visible: break - if main.ui.modals.level_screen == null or not main.ui.modals.level_screen.visible: + if main.ui.modals.run.level_screen == null or not main.ui.modals.run.level_screen.visible: _fail("关卡画面未出现") return for i in 5: await get_tree().process_frame - main.ui.modals.level_screen.press_candidate(0) + main.ui.modals.run.level_screen.press_candidate(0) for i in 20: await get_tree().process_frame if main.run_scope.settlement == null: @@ -69,16 +69,16 @@ func _ready() -> void: var t_settle := Time.get_ticks_msec() var _rows_at_victory: int = main.ui.screens.row_pacer.pending_rows().size() # 时间戳①:结算入队完成帧(AP 耗尽/停手→五行段同步跑完→行全在队) # 断言①胜利同帧:终局面未开 + 挂起在案 + 队列非空(判别性——旧行为同帧开窗即红) - if main.ui.modals.end_overlay.is_open(): + if main.ui.modals.run.end_overlay.is_open(): _problems.append("胜利同帧终局面已开(演播后排窗失效——同帧 flush 旧路径回潮)") - if main.ui.modals.flow._deferred == null: + if main.ui.flow._deferred == null: _problems.append("胜利同帧挂起态缺失(_deferred_modal == null)") if main.ui.screens.row_pacer.pending_rows().is_empty(): _problems.append("胜利同帧排播队列已空(结算行未走排播链)") var _played_rows: int = _rows_at_victory # 断言②排播放完:终局面开启(20s 守卫——0.22s×行数 ×10 余量) var waited := 0.0 - while not main.ui.modals.end_overlay.is_open() and waited < 20.0: + while not main.ui.modals.run.end_overlay.is_open() and waited < 20.0: await get_tree().process_frame waited += get_process_delta_time() var t_popup := Time.get_ticks_msec() @@ -86,7 +86,7 @@ func _ready() -> void: print("[t17_defer] AP点用完: %s" % ap_clock) print("[t17_defer] 触发结算: %s(与 AP 用完同帧——核心同步,相差 0ms)" % settle_clock) print("[t17_defer] 弹窗出现: %s(距触发结算 %d ms = 逐行演播时长,行数 %d)" % [popup_clock, t_popup - t_ap_out, _played_rows]) - if not main.ui.modals.end_overlay.is_open(): + if not main.ui.modals.run.end_overlay.is_open(): _problems.append("排播放完后终局面未开(%.1fs——演播完边沿派发失守)" % waited) if _problems.is_empty(): print("[t17_defer] PASS — 胜利同帧挂起+排播完开窗 两断言全过(waited=%.1fs)" % waited) diff --git a/game/probe/ui_click_probe.gd b/game/probe/ui_click_probe.gd index 9871f175..637f418a 100644 --- a/game/probe/ui_click_probe.gd +++ b/game/probe/ui_click_probe.gd @@ -51,7 +51,7 @@ func _ready() -> void: var t_ui_ready := Time.get_ticks_msec() - t0 # 调试浮窗 boot 不开(菜单优先——入口=UI 最顶层全局浮层角标, # 任何 UI 不遮挡;浮层角标遮挡判据见设置面段) - if main.ui.modals.debug_overlay.visible: + if main.ui.modals.menu.debug_overlay.visible: _problems.append("调试浮窗 boot 自动开(菜单优先失守)") # ---- 营地购买合成点击路径(选档进档开层→购买→关闭全走真实按钮信号路径; # 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json);陈旧档段首清理)---- @@ -204,11 +204,11 @@ func _ready() -> void: f_c2.close() # 槽2 有档异值(进档链夹具);槽1/3 空 main._open_save_select() await get_tree().process_frame - if not main.ui.modals.save_select_overlay.visible: + if not main.ui.modals.menu.save_select_overlay.visible: _problems.append("[savesel] 选档层未打开(click 段前置)") # sl12 动线序(新档链同拍采样):逐帧可见性次序——首帧选档隐藏时营地层必须已可见、开始层保持隐藏 #(禁先关后开——_close_modal 会恢复开始层=回闪一帧) - var new_btn: Button = main.ui.modals.save_select_overlay._new_btns.get(1) + var new_btn: Button = main.ui.modals.menu.save_select_overlay._new_btns.get(1) if new_btn == null: _problems.append("[savesel] 槽1 新档钮缺失(sl5/sl12 驱动中断)") else: @@ -216,7 +216,7 @@ func _ready() -> void: new_btn.pressed.emit() # 真实按钮信号路径 → _on_save_select_new → create_slot+open_camp for fi in 3: await get_tree().process_frame - samples.append([main.ui.modals.save_select_overlay.visible, main.ui.modals.camp.scene.visible, main.ui.screens.start_overlay.visible]) + samples.append([main.ui.modals.menu.save_select_overlay.visible, main.ui.modals.camp.scene.visible, main.ui.screens.start_overlay.visible]) if samples[0][0]: _problems.append("[savesel] sl12 新档后选档层仍可见(落点开层未发生)") elif not samples[0][1]: @@ -225,8 +225,8 @@ func _ready() -> void: _problems.append("[savesel] sl12 动线序违约:开始层在落点帧恢复(回闪一帧)") if main.save_manager.slots.active_slot() != 1 or not FileAccess.file_exists(csel_tpl % 1): _problems.append("[savesel] sl5 新档链失守(active=%d 应=1 且槽1 白板应落盘)" % main.save_manager.slots.active_slot()) - var occluded: Array = main.ui.modals.flow.stack._occluded.get(main.ui.modals.camp.scene, []) - if not occluded.has(main.ui.modals.save_select_overlay): + var occluded: Array = main.ui.flow.stack._occluded.get(main.ui.modals.camp.scene, []) + if not occluded.has(main.ui.modals.menu.save_select_overlay): _problems.append("[savesel] sl12 选档层被关闭而非栈记账挂起(先关后开形态——遮挡挂起中间态钉死)") if int((JSON.parse_string(FileAccess.get_file_as_string(csel_tpl % 1)) as Dictionary).get("meta", {}).get("keystone_balance", -1)) != 0: _problems.append("[savesel] sl5 新档非白板(槽1 落盘应全零)") @@ -235,7 +235,7 @@ func _ready() -> void: # sl6 进档链:槽2 进档 → 激活=2(camp 数据随激活槽) main._open_save_select() await get_tree().process_frame - var load_btn: Button = main.ui.modals.save_select_overlay._load_btns.get(2) + var load_btn: Button = main.ui.modals.menu.save_select_overlay._load_btns.get(2) if load_btn == null: _problems.append("[savesel] 槽2 进档钮缺失(sl6 驱动中断——夹具应有档)") else: @@ -243,51 +243,51 @@ func _ready() -> void: await get_tree().process_frame if main.save_manager.slots.active_slot() != 2: _problems.append("[savesel] sl6 进档链失守(active=%d 应=2——激活该槽)" % main.save_manager.slots.active_slot()) - if not main.ui.modals.camp.scene.visible or main.ui.modals.save_select_overlay.visible: + if not main.ui.modals.camp.scene.visible or main.ui.modals.menu.save_select_overlay.visible: _problems.append("[savesel] sl6 落点未开营地层或选档未隐藏") main._on_camp_closed() await get_tree().process_frame # sl7 删档二次确认双路径:一击只开窗(文件仍在)→取消只关窗(文件仍在)→再开窗确认才删(槽回空) main._open_save_select() await get_tree().process_frame - var del_btn: Button = main.ui.modals.save_select_overlay._delete_btns.get(2) + var del_btn: Button = main.ui.modals.menu.save_select_overlay._delete_btns.get(2) if del_btn == null: _problems.append("[savesel] 槽2 删档钮缺失(sl7 驱动中断)") else: del_btn.pressed.emit() await get_tree().process_frame - if not main.ui.modals.confirm_dialog.is_open(): + if not main.ui.modals.menu.confirm_dialog.is_open(): _problems.append("[savesel] sl7 一击未只开确认窗(直删形态)") elif not FileAccess.file_exists(csel_tpl % 2): _problems.append("[savesel] sl7 确认前文件已被删(一次点击即删违契)") else: - if not main.ui.modals.flow.stack._occluded.has(main.ui.modals.confirm_dialog): + if not main.ui.flow.stack._occluded.has(main.ui.modals.menu.confirm_dialog): _problems.append("[savesel] sl7 确认窗未经模态栈开(直开旁路——wr2 单口失守)") - elif not main.ui.modals.flow.locks_input(): + elif not main.ui.flow.locks_input(): _problems.append("[savesel] sl7 确认窗开时输入锁未生效(锁域失守——wr2)") - main.ui.modals.confirm_dialog.cancelled.emit() # 取消路径(真实信号——cancelled 钮同源) + main.ui.modals.menu.confirm_dialog.cancelled.emit() # 取消路径(真实信号——cancelled 钮同源) await get_tree().process_frame - if main.ui.modals.confirm_dialog.is_open() or not FileAccess.file_exists(csel_tpl % 2): + if main.ui.modals.menu.confirm_dialog.is_open() or not FileAccess.file_exists(csel_tpl % 2): _problems.append("[savesel] sl7 取消路径失守(应只关窗、文件仍在)") elif main._confirm_consumer != "": _problems.append("[savesel] sl8 取消后消费方状态量未清(分派面残留)") - if main.ui.modals.flow.stack._occluded.has(main.ui.modals.confirm_dialog): + if main.ui.flow.stack._occluded.has(main.ui.modals.menu.confirm_dialog): _problems.append("[savesel] sl7 取消后确认窗遮挡账未清(关侧未走模态栈——wr2)") del_btn.pressed.emit() # 再开窗走确认路径 await get_tree().process_frame - main.ui.modals.confirm_dialog.confirmed.emit() # 确认路径(真实信号——confirmed 钮同源) + main.ui.modals.menu.confirm_dialog.confirmed.emit() # 确认路径(真实信号——confirmed 钮同源) await get_tree().process_frame - if FileAccess.file_exists(csel_tpl % 2) or main.ui.modals.confirm_dialog.is_open(): + if FileAccess.file_exists(csel_tpl % 2) or main.ui.modals.menu.confirm_dialog.is_open(): _problems.append("[savesel] sl7 确认路径失守(应删档+关窗)") elif main.run_scope.is_adventure or main.ui.screens.start_overlay.visible: _problems.append("[savesel] sl8 删档确认误触发开局(双常驻接线形态——分派单口失守)") - elif bool(main.ui.modals.save_select_overlay._occupied_flags.get(2, true)): + elif bool(main.ui.modals.menu.save_select_overlay._occupied_flags.get(2, true)): _problems.append("[savesel] sl7 删后选档面未就地刷新(槽2 应回空态)") elif main._confirm_consumer != "": _problems.append("[savesel] sl8 确认后消费方状态量未清(分派面残留)") main._on_save_select_closed() await get_tree().process_frame - if main.ui.modals.save_select_overlay.visible or not main.ui.screens.start_overlay.visible: + if main.ui.modals.menu.save_select_overlay.visible or not main.ui.screens.start_overlay.visible: _problems.append("[savesel] 关闭后层未隐藏或开始层未恢复(模态栈单口失守)") # ---- 设置面段(隔离 store 换注禁碰真实 user://settings.cfg;真实按钮信号路径)---- @@ -297,19 +297,19 @@ func _ready() -> void: var iso_s = load("res://presentation/settings_store.gd").new("user://smoke_click_probe_settings.cfg") iso_s.load_settings() main.settings_store = iso_s - main.ui.modals.settings_overlay.retarget(iso_s) + main.ui.modals.menu.settings_overlay.retarget(iso_s) var settings_btn: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "设置", "prefix") if settings_btn == null: _problems.append("设置入口按钮缺失(开始层 settingsbtn)") else: settings_btn.pressed.emit() # 真实按钮信号路径 → _open_settings await get_tree().process_frame - if not main.ui.modals.settings_overlay.visible or main.ui.screens.start_overlay.visible: + if not main.ui.modals.menu.settings_overlay.visible or main.ui.screens.start_overlay.visible: _problems.append("设置面未打开(点击路径)") else: # 减动效转正双态(卡④ AC③)——M4 适配:拨杆开关(_motion_btn)语义翻转 on=完整/off=快速, # 状态字随行 _motion_st;查找由文字改成员引用(拨杆无文字) - var motion_btn: Button = main.ui.modals.settings_overlay._motion_btn + var motion_btn: Button = main.ui.modals.menu.settings_overlay._motion_btn if motion_btn == null: _problems.append("设置面减动效开关缺失") else: @@ -326,7 +326,7 @@ func _ready() -> void: if UiKit.motion_scale != 1.0: _problems.append("减动效关未恢复(motion_scale != 1.0)") # 音量实时生效(卡④ AC①):滑杆 30% → master bus db 与 store 双读 - var vol: HSlider = main.ui.modals.settings_overlay._volume + var vol: HSlider = main.ui.modals.menu.settings_overlay._volume vol.value = 30 await get_tree().process_frame var master_idx := AudioServer.get_bus_index("Master") @@ -339,22 +339,22 @@ func _ready() -> void: # 回归语义随调试浮层发布态退役,申报见迭代 38 实现报告 §8.2) if main.ui.screens.debug_badge == null or main.ui.screens.debug_badge.visible: _problems.append("设置面开中调试角标可见(发布态恒隐失守)") - var settings_close: Button = ProbeUiUtil.find_button(main.ui.modals.settings_overlay, "返回", "prefix") + var settings_close: Button = ProbeUiUtil.find_button(main.ui.modals.menu.settings_overlay, "返回", "prefix") if settings_close == null: _problems.append("设置面返回按钮缺失") else: settings_close.pressed.emit() await get_tree().process_frame - if main.ui.modals.settings_overlay.visible or not main.ui.screens.start_overlay.visible: + if main.ui.modals.menu.settings_overlay.visible or not main.ui.screens.start_overlay.visible: _problems.append("设置面关闭后未恢复开始层") # 旧入口零残留断言(卡④ AC③——测试调整板不含减动效开关;14.5 起载体=调试浮窗) - var legacy_motion = ProbeUiUtil.find_check_button(main.ui.modals.debug_overlay, "减动效") + var legacy_motion = ProbeUiUtil.find_check_button(main.ui.modals.menu.debug_overlay, "减动效") if legacy_motion != null: _problems.append("调整板减动效旧入口未退役(设置面唯一入口失守——卡④ AC③)") # D1 断言(二轮):退出游戏只在一级菜单——开始菜单有、设置面无 if ProbeUiUtil.find_button(main.ui.screens.start_overlay, "退出游戏", "prefix") == null: _problems.append("开始菜单缺退出游戏按钮(二轮 D1)") - if ProbeUiUtil.find_button(main.ui.modals.settings_overlay, "退出游戏", "prefix") != null: + if ProbeUiUtil.find_button(main.ui.modals.menu.settings_overlay, "退出游戏", "prefix") != null: _problems.append("设置面仍含退出游戏按钮(二轮 D1——只在一级菜单出现)") # ---- 生涯页段(入口点击链+关闭回返,真实按钮信号路径;本页只读零写, # 值断言归冒烟投影段与 ui_struct 结构族,此处只验开合路径与零跳转)---- @@ -362,29 +362,29 @@ func _ready() -> void: if true: main._open_career() await get_tree().process_frame - if not main.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible: + if not main.ui.modals.menu.career_overlay.visible or main.ui.screens.start_overlay.visible: _problems.append("生涯页未打开(点击路径)") else: # 零跳转契约(提纲 P4 甲):页内除「返回」外零导航钮 for nav in ["营地", "商店", "设置", "退出", "冒险"]: - if ProbeUiUtil.find_button(main.ui.modals.career_overlay, nav, "contains") != null: + if ProbeUiUtil.find_button(main.ui.modals.menu.career_overlay, nav, "contains") != null: _problems.append("生涯页内出现导航入口「%s」(零跳转违约——生涯纯只读)" % nav) # 局内入口断言(提纲 P4 甲):生涯入口只在开始层——战斗屏/地图屏零生涯角标 if main.ui.battle.board_zone.find_child("career_badge", true, false) != null \ - or main.ui.modals.level_screen.find_child("career_badge", true, false) != null: + or main.ui.modals.run.level_screen.find_child("career_badge", true, false) != null: _problems.append("局内屏出现生涯入口件(局内不放生涯入口违约)") - var career_close: Button = ProbeUiUtil.find_button(main.ui.modals.career_overlay, "返回", "prefix") + var career_close: Button = ProbeUiUtil.find_button(main.ui.modals.menu.career_overlay, "返回", "prefix") if career_close == null: _problems.append("生涯页返回按钮缺失") else: career_close.pressed.emit() await get_tree().process_frame - if main.ui.modals.career_overlay.visible or not main.ui.screens.start_overlay.visible: + if main.ui.modals.menu.career_overlay.visible or not main.ui.screens.start_overlay.visible: _problems.append("生涯页关闭后未恢复开始层") # 换回全新零档(营地购买态不泄入后续冒险段——确定性;档案残留下次段首清理兜底) ProbeUiUtil.isolate_save(main, "smoke_click_probe_reset", true, false, true) # 零档换注(继续上局钮已随三入口化撤出——卡② mn8) - main.ui.modals.debug_overlay.set_sliders(400, 200) # 高加成保 L1 可完成——L2 岔路选路段只能经冒险推进到达(单关直入口退役) - main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,行为走查确定性禁依赖默认分支) + main.ui.modals.menu.debug_overlay.set_sliders(400, 200) # 高加成保 L1 可完成——L2 岔路选路段只能经冒险推进到达(单关直入口退役) + main.ui.modals.menu.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,行为走查确定性禁依赖默认分支) var t1 := Time.get_ticks_msec() main._start_adventure_from_menu() await get_tree().process_frame @@ -396,11 +396,11 @@ func _ready() -> void: _problems.append("开场行回归失守:局外加成(pct>0)局的开场行应带「(含面板加成)」(wc3)") # 关卡开始 = 入口选路停点——press_candidate 图上命中路径选入口候选进首战 #(首格可点 AC 口径随之含入口选路一步:菜单点击 → 停点呈现 → pick → 棋盘安装) - if main.ui.modals.level_screen.visible: + if main.ui.modals.run.level_screen.visible: # 独立性断言:独立屏激活期战斗屏宿主整体隐藏 if main.ui.screens.battle_root.visible: _problems.append("独立屏激活期战斗屏宿主未隐藏(零透出契约失守)") - main.ui.modals.level_screen.press_candidate(0) + main.ui.modals.run.level_screen.press_candidate(0) await get_tree().process_frame await get_tree().process_frame var t_entry := Time.get_ticks_msec() - t1 @@ -421,21 +421,21 @@ func _ready() -> void: while not _done and action_budget > 0: action_budget -= 1 await get_tree().process_frame - if main.ui.modals.node_overlay.visible: - main.ui.modals.node_overlay.press_continue() + if main.ui.modals.run.node_overlay.visible: + main.ui.modals.run.node_overlay.press_continue() _seals_placed.clear() continue - if main.ui.modals.curio_overlay.visible: + if main.ui.modals.run.curio_overlay.visible: # 三选一停点(CurioPickOverlay——奖励节点/关卡完成兑付共用面,须与硬锁态适配, # 未适配则走到即卡死)。两态分派:result 态(兑付反馈——点继续推进)/ # 待选态(点首件+确认)。真实信号路径全线。〔无替换子态——装配不可变契约。〕 # 三选一停点输入锁定断言(停点态断言面:待选/兑付两态共用此层)。 - if not _tested_curio_head and main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty() and String(main.ui.modals.curio_overlay._header).contains("关卡奖励点"): + if not _tested_curio_head and main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty() and String(main.ui.modals.run.curio_overlay._header).contains("关卡奖励点"): # 停点态判据 = _node_curio_pending(奖励节点三选一停点住此——原条件查 # _curio_pending〔关卡完成三选一态〕与奖励头部互斥、恒不可命中=拦截能力未证病根, # 卡⑤ d4 转正随修;头部校验串本体零触碰) _tested_curio_head = true - var ch := String(main.ui.modals.curio_overlay._header) + var ch := String(main.ui.modals.run.curio_overlay._header) if not ch.contains("冒险牌库") or not ch.contains("属性值提升与补给") or ch.contains("同权重") or ch.contains("同上限") or ch.contains("提升池"): _problems.append("三选一头部旧话残留或新话缺失(wa4——%s)" % ch.replace(" ", "|")) @@ -444,45 +444,45 @@ func _ready() -> void: var logtext: String = main.ui.battle.log_view.parsed_text() if not logtext.contains("关卡奖励点(三选一——选毕推进)") or logtext.contains("内容待定"): _problems.append("奖励节点战报文案未换代(卡⑤ wr2——旧话残留或新话缺失)") - if not main.ui.modals.flow.locks_input(): + if not main.ui.flow.locks_input(): _problems.append("三选一停点打开时输入未锁定") - if main.ui.modals.curio_overlay._continue_btn.visible: - main.ui.modals.curio_overlay.press_continue() + if main.ui.modals.run.curio_overlay._continue_btn.visible: + main.ui.modals.run.curio_overlay.press_continue() elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty(): - main.ui.modals.curio_overlay.press_card(0) # 奖励节点待选态:点首件+确认(真实按钮信号路径——与关卡完成停点同形) - main.ui.modals.curio_overlay.press_confirm() + main.ui.modals.run.curio_overlay.press_card(0) # 奖励节点待选态:点首件+确认(真实按钮信号路径——与关卡完成停点同形) + main.ui.modals.run.curio_overlay.press_confirm() elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty(): - main.ui.modals.curio_overlay.press_card(0) - main.ui.modals.curio_overlay.press_confirm() + main.ui.modals.run.curio_overlay.press_card(0) + main.ui.modals.run.curio_overlay.press_confirm() else: - main.ui.modals.curio_overlay.press_continue() # 停点已消费的残面——继续关屏 + main.ui.modals.run.curio_overlay.press_continue() # 停点已消费的残面——继续关屏 _seals_placed.clear() continue - if main.ui.modals.shop_overlay.visible: + if main.ui.modals.menu.shop_overlay.visible: # 商店房覆盖层(摆放约束:每终点前一位恰一店,真实路径恒必经一店):零购离开(真实 leave 信号路径)放行选路。 # 分派链序:商店层=选路页上的模态覆盖(停点呈现为 # level_screen+shop 双 visible)——必须先于 level_screen 分派,否则下层候选点击 # 空转烧尽预算(实证:lv2 商店停点双 visible 循环 12000 帧 done=false)。 - main.ui.modals.shop_overlay.leave_pressed.emit() + main.ui.modals.menu.shop_overlay.leave_pressed.emit() _seals_placed.clear() continue - if main.ui.modals.level_screen.visible: + if main.ui.modals.run.level_screen.visible: # 节点图选路口(零自动推进停点)——图上命中路径交替选候选(岔路使用行为;布局决策 2 改造) # 地图屏角标一次性 case:选路前经角标开/关状态面板(层在地图屏之上开合, # 关闭后地图屏选路态零扰动——P2 甲层级语义) - if not _tested_map_status and not main.ui.modals.status_panel.visible: + if not _tested_map_status and not main.ui.modals.run.status_panel.visible: _tested_map_status = true - var mpb: Button = main.ui.modals.level_screen.status_badge + var mpb: Button = main.ui.modals.run.level_screen.status_badge if mpb == null or not mpb.visible or mpb.disabled: _problems.append("地图屏状态角标缺失/未点亮/被置灰(P1 丁)") else: mpb.pressed.emit() - if not main.ui.modals.status_panel.visible or not main.ui.modals.level_screen.visible: + if not main.ui.modals.run.status_panel.visible or not main.ui.modals.run.level_screen.visible: _problems.append("地图屏角标开状态面板失败或地图屏被误藏(P2 甲层级失守)") - var rbtn2: Button = ProbeUiUtil.find_button(main.ui.modals.status_panel, "继续", "prefix") + var rbtn2: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix") if rbtn2 != null: rbtn2.pressed.emit() - if main.ui.modals.status_panel.visible or not main.ui.modals.level_screen.visible: + if main.ui.modals.run.status_panel.visible or not main.ui.modals.run.level_screen.visible: _problems.append("状态面板关闭后地图屏态失守(恢复语义破)") # 地图屏加成帽换代断言(迭代36 卡A wa2——「特性」→「属性值提升」;帽 Label 常在树; # 独立旗标——_tested_map_status 在同拍前置块已置位,禁复用其门控) @@ -490,7 +490,7 @@ func _ready() -> void: _tested_map_cap = true var cap_found := false var ls_labels: Array = [] - _collect_labels_by_text(main.ui.modals.level_screen, ls_labels) + _collect_labels_by_text(main.ui.modals.run.level_screen, ls_labels) for l in ls_labels: if l.text == "属性值提升": cap_found = true @@ -502,16 +502,16 @@ func _ready() -> void: if main.run_scope.runner != null: for nd in main.run_scope.runner.nodes(): var nd_id: int = main.run_scope.runner.node_id_of(nd) - if String(nd.get("type", "")) == "reward" and main.ui.modals.level_screen._cand_ids.has(nd_id): - pick = main.ui.modals.level_screen._cand_ids.find(nd_id) + if String(nd.get("type", "")) == "reward" and main.ui.modals.run.level_screen._cand_ids.has(nd_id): + pick = main.ui.modals.run.level_screen._cand_ids.find(nd_id) break - main.ui.modals.level_screen.press_candidate(pick) + main.ui.modals.run.level_screen.press_candidate(pick) _route_clicks += 1 _seals_placed.clear() continue if main.ui.screens.inter_overlay.visible: # 大奖励屏(冒险级停点——非末关的关卡完成恰一次):首见确认推进 L2(真实按钮信号路径);再见 = L2 已关卡完成,收口 - if not main.ui.modals.flow.locks_input(): + if not main.ui.flow.locks_input(): _problems.append("大奖励屏打开时输入未锁定") _inter_confirms += 1 if _inter_confirms == 1: @@ -530,23 +530,23 @@ func _ready() -> void: break _seals_placed.clear() continue - if main.ui.modals.end_overlay.is_open() and main.run_scope.settlement_stop_timer.is_armed(): + if main.ui.modals.run.end_overlay.is_open() and main.run_scope.settlement_stop_timer.is_armed(): # 战斗胜利总结结算框(真实信号路径——每场普通胜利到达):断言已拍板面后点「下一节点」 - if not main.ui.modals.flow.locks_input(): + if not main.ui.flow.locks_input(): _problems.append("胜利结算框打开时输入未锁定") - var gp_line: String = "%s %s" % [main.ui.modals.end_overlay._gp_name.text, main.ui.modals.end_overlay._gp_val.text] # 两列账行(审计差距④)——名/值列拼回整串断言 + var gp_line: String = "%s %s" % [main.ui.modals.run.end_overlay._gp_name.text, main.ui.modals.run.end_overlay._gp_val.text] # 两列账行(审计差距④)——名/值列拼回整串断言 if not gp_line.contains("GP +%d" % main.run_scope.last_gain_amount) or not gp_line.contains("合计 %d" % int(main.run_scope.runner.gp)): _problems.append("胜利结算框 GP 增量/总量与钱包实态不符(%s|实态 +%d/%d)" % [gp_line, main.run_scope.last_gain_amount, int(main.run_scope.runner.gp)]) - if main.ui.modals.end_overlay.verdict_text() == "": + if main.ui.modals.run.end_overlay.verdict_text() == "": _problems.append("胜利结算框判定行缺失") _settlement_confirms += 1 - main.ui.modals.end_overlay._btn.pressed.emit() # 「下一节点」真实信号路径 + main.ui.modals.run.end_overlay._btn.pressed.emit() # 「下一节点」真实信号路径 await get_tree().process_frame await get_tree().process_frame - if main.run_scope.settlement_stop_timer.is_armed() or not main.ui.modals.level_screen.visible: + if main.run_scope.settlement_stop_timer.is_armed() or not main.ui.modals.run.level_screen.visible: _problems.append("胜利结算框「下一节点」未进选路页(卡⑥ AC②)") continue - if main.ui.modals.end_overlay.is_open(): + if main.ui.modals.run.end_overlay.is_open(): # 冒险终局(失败/放弃/打穿)——无单关直入口,见终局即收口(模态断言照旧) # 三选一链适配:_adventure_mode 仍真且非选路挂起=中途结算面(Boss 胜利 # 总结「继续」后挂三选一→真终局未到)——真实信号路径点继续推进。 @@ -555,13 +555,13 @@ func _ready() -> void: # (_adventure_mode)在真终局仍 true——会把真终局当中途面点掉返回键(实证)。 var _adv_running: bool = main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running" if main.run_scope.is_adventure and _adv_running: - main.ui.modals.end_overlay._btn.pressed.emit() + main.ui.modals.run.end_overlay._btn.pressed.emit() continue - if not main.ui.modals.flow.locks_input(): + if not main.ui.flow.locks_input(): _problems.append("终局 overlay 打开时输入未锁定") _done = true print("[click_probe] 冒险终局:%s · 选路口点击 %d 次" % [ - main.ui.modals.end_overlay.verdict_text().replace("\n", " "), _route_clicks]) + main.ui.modals.run.end_overlay.verdict_text().replace("\n", " "), _route_clicks]) break if main.run_scope.settlement == null or main.run_scope.runner == null: continue @@ -603,36 +603,36 @@ func _ready() -> void: # ---- 预算耗尽收口(T17 硬锁批适配):选件变强后 probe 可能活过预算不再战败——断点段前提 # (冒险终局面在案)须主动满足。收口驱动:任何停点层逐个推进(复用主循环分支语义)至 # level_screen 选路停点→真实放弃二击→终局面。战斗中则继续 autoplay 打完。 - if not _done and not main.ui.modals.end_overlay.is_open(): + if not _done and not main.ui.modals.run.end_overlay.is_open(): var close_guard := 8000 var dbg_frame := 0 # T17 演播后排窗:每场胜利 +~400 帧演播等待(旧同帧 flush 零等待)——守卫按多场战斗放大 while close_guard > 0: close_guard -= 1 await get_tree().process_frame - if main.ui.modals.end_overlay.is_open(): + if main.ui.modals.run.end_overlay.is_open(): # 中途结算面(state=running)=点继续推进;真终局(settling)=断点段前提满足,收口。 if main.run_scope.is_adventure and main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running": - main.ui.modals.end_overlay._btn.pressed.emit() + main.ui.modals.run.end_overlay._btn.pressed.emit() else: break - elif main.ui.modals.curio_overlay.visible: - if main.ui.modals.curio_overlay._continue_btn.visible: - main.ui.modals.curio_overlay.press_continue() + elif main.ui.modals.run.curio_overlay.visible: + if main.ui.modals.run.curio_overlay._continue_btn.visible: + main.ui.modals.run.curio_overlay.press_continue() elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty(): - main.ui.modals.curio_overlay.press_card(0) - main.ui.modals.curio_overlay.press_confirm() + main.ui.modals.run.curio_overlay.press_card(0) + main.ui.modals.run.curio_overlay.press_confirm() else: - main.ui.modals.curio_overlay.press_continue() - elif main.ui.modals.node_overlay.visible: - main.ui.modals.node_overlay.press_continue() - elif main.ui.modals.shop_overlay.visible: - main.ui.modals.shop_overlay.leave_pressed.emit() + main.ui.modals.run.curio_overlay.press_continue() + elif main.ui.modals.run.node_overlay.visible: + main.ui.modals.run.node_overlay.press_continue() + elif main.ui.modals.menu.shop_overlay.visible: + main.ui.modals.menu.shop_overlay.leave_pressed.emit() elif main.ui.screens.inter_overlay.visible: var next_btn2: Button = ProbeUiUtil.find_button(main.ui.screens.inter_overlay, "进入下一层", "prefix") if next_btn2 != null: next_btn2.pressed.emit() - elif main.ui.modals.level_screen.visible: - var abandon_btn: Button = ProbeUiUtil.find_button(main.ui.modals.level_screen, "放弃", "prefix") + elif main.ui.modals.run.level_screen.visible: + var abandon_btn: Button = ProbeUiUtil.find_button(main.ui.modals.run.level_screen, "放弃", "prefix") if abandon_btn != null: _closed_via_abandon = true abandon_btn.pressed.emit() @@ -660,12 +660,12 @@ func _ready() -> void: str(((bpf as Dictionary).get("bonus_deck", []) as Array).size()), str((bpf as Dictionary).get("bonus_pending"))]) _problems.append("断点段:战败结算后断点未清(T3 三结局清断点失守)") - if not main.ui.modals.end_overlay.is_open(): + if not main.ui.modals.run.end_overlay.is_open(): _problems.append("断点段:战败后未出冒险终局面") - if main.ui.modals.end_overlay._btn == null: + if main.ui.modals.run.end_overlay._btn == null: _problems.append("断点段:终局面返回按钮缺失") else: - main.ui.modals.end_overlay._btn.pressed.emit() # 真实按钮信号路径(_btn=继续/返回共用句柄) + main.ui.modals.run.end_overlay._btn.pressed.emit() # 真实按钮信号路径(_btn=继续/返回共用句柄) await get_tree().process_frame if not main.ui.screens.start_overlay.visible: # 豁免:放弃路径收口时终局「返回」后仍挂三选一/关卡推进残留态(停点链), @@ -687,19 +687,19 @@ func _ready() -> void: var bal_gate: int = main._camp_data()["balance"] main.ui.modals.camp.scene.object_pressed.emit(&"gate") await get_tree().process_frame - if not main.ui.modals.confirm_dialog.is_open(): + if not main.ui.modals.menu.confirm_dialog.is_open(): _problems.append("大门无断点:确认弹窗未开(点大门应先出确认不直接进冒险)〔营地图批 V3〕") if main.run_scope.is_adventure: _problems.append("大门无断点:未确认即进冒险〔营地图批 V3〕") - if main.ui.modals.confirm_dialog._secondary_btn.visible: + if main.ui.modals.menu.confirm_dialog._secondary_btn.visible: _problems.append("大门无断点:次钮应隐藏(两出口形态)〔营地图批 V3〕") - main.ui.modals.confirm_dialog.cancelled.emit() + main.ui.modals.menu.confirm_dialog.cancelled.emit() await get_tree().process_frame if not main.ui.modals.camp.scene.visible or main.run_scope.is_adventure or main._camp_data()["balance"] != bal_gate: _problems.append("大门取消:未无变更退回营地(写口零调用/营地根 visible 失守)〔营地图批 V3〕") main.ui.modals.camp.scene.object_pressed.emit(&"gate") await get_tree().process_frame - main.ui.modals.confirm_dialog.confirmed.emit() # 「是」→开始新冒险 + main.ui.modals.menu.confirm_dialog.confirmed.emit() # 「是」→开始新冒险 await get_tree().process_frame if not main.run_scope.is_adventure or main.ui.modals.camp.scene.visible: _problems.append("大门确认:冒险未发起或营地场景屏未隐藏〔营地图批 V3〕〔营地图批 B1〕") @@ -714,9 +714,9 @@ func _ready() -> void: await get_tree().process_frame main.ui.modals.camp.scene.object_pressed.emit(&"gate") await get_tree().process_frame - if not main.ui.modals.confirm_dialog.is_open() or not main.ui.modals.confirm_dialog._secondary_btn.visible: + if not main.ui.modals.menu.confirm_dialog.is_open() or not main.ui.modals.menu.confirm_dialog._secondary_btn.visible: _problems.append("大门有断点:三出口形态失守(次钮应现形)〔营地图批 V3〕") - main.ui.modals.confirm_dialog.secondary_confirmed.emit() # 「开始新冒险」出口 + main.ui.modals.menu.confirm_dialog.secondary_confirmed.emit() # 「开始新冒险」出口 await get_tree().process_frame if not main.run_scope.is_adventure or main.ui.modals.camp.scene.visible: _problems.append("大门有断点次确认:冒险未发起或营地未隐藏〔营地图批 V3〕") @@ -736,8 +736,8 @@ func _ready() -> void: if FileAccess.file_exists(p_ol): DirAccess.remove_absolute(p_ol) ProbeUiUtil.isolate_save(main, "smoke_click_probe_openline", true, false, true) - main.ui.modals.debug_overlay.reset_sliders() # ① 白板反向前提(_start_adventure 只关窗不清值——残值跨段存活防坑) - main.ui.modals.debug_overlay.seed_edit.text = "42" + main.ui.modals.menu.debug_overlay.reset_sliders() # ① 白板反向前提(_start_adventure 只关窗不清值——残值跨段存活防坑) + main.ui.modals.menu.debug_overlay.seed_edit.text = "42" main._start_adventure_from_menu() var ol_l1_wait := 0 while ol_l1_wait < 24 and String(main.ui.battle.log_view._log.get_parsed_text()).count("· 种子 ") < 1: @@ -822,14 +822,14 @@ func _run_modal_probes_once() -> void: if not _tested_legend: _tested_legend = true main.ui.battle.board_zone.ap_bar._btn_legend.pressed.emit() - if not main.ui.modals.legend_overlay.is_open(): + if not main.ui.modals.run.legend_overlay.is_open(): _problems.append("图例打开失败") - if main.ui.modals.end_overlay.is_open() or main.ui.screens.start_overlay.visible: + if main.ui.modals.run.end_overlay.is_open() or main.ui.screens.start_overlay.visible: _problems.append("图例打开时上方遮挡层未隐藏(缺陷⑤复发)") - if not main.ui.modals.flow.locks_input(): + if not main.ui.flow.locks_input(): _problems.append("图例打开时棋盘输入未锁定") var legend_has_seal := false - for e in main.ui.modals.legend_overlay.ENTRIES: + for e in main.ui.modals.run.legend_overlay.ENTRIES: if String(e[1]) == "封印": legend_has_seal = true # 图例封印行换代断言(迭代36 卡A wa5——真实 AP 条图例钮开层路径) @@ -838,13 +838,13 @@ func _run_modal_probes_once() -> void: ", "|")) if not legend_has_seal: _problems.append("图例缺『封印』格态条目(格态图例基线)") - main.ui.modals.legend_overlay.close() - if main.ui.modals.legend_overlay.is_open() or main.ui.modals.flow.locks_input(): + main.ui.modals.run.legend_overlay.close() + if main.ui.modals.run.legend_overlay.is_open() or main.ui.flow.locks_input(): _problems.append("图例关闭后未恢复输入/可见性") # ②/②b 明细回看模态无探针(M9/P-C7——冒险中回看归「生涯」,06 登记) # ⑤ 状态面板(P1 丁角标真实信号路径 + P2 甲模态语义 + P1 守卫 + R3 锁恢复): # 角标打开 → 只读层可见+输入锁定+余额徽非空 → 「继续」关闭恢复;图例开着 → 角标置灰且点击被守卫兜住 - if not _tested_status and not main.ui.modals.legend_overlay.is_open() and not main.ui.modals.status_panel.visible: + if not _tested_status and not main.ui.modals.run.legend_overlay.is_open() and not main.ui.modals.run.status_panel.visible: _tested_status = true var pbtn: Button = main.ui.battle.board_zone.status_badge if pbtn == null or not pbtn.visible or pbtn.disabled: @@ -852,17 +852,17 @@ func _run_modal_probes_once() -> void: % [str(pbtn != null and pbtn.visible), str(pbtn != null and pbtn.disabled)]) else: pbtn.pressed.emit() # 真实按钮信号路径 → 组合根 _on_status_badge_pressed - if not main.ui.modals.status_panel.visible: + if not main.ui.modals.run.status_panel.visible: _problems.append("状态面板未随角标打开(组合根接线失守)") - if not main.ui.modals.flow.locks_input(): + if not main.ui.flow.locks_input(): _problems.append("状态面板打开时输入未锁定(R3)") - var bal_txt: String = main.ui.modals.status_panel._balance_badge.value_label.text + var bal_txt: String = main.ui.modals.run.status_panel._balance_badge.value_label.text if not bal_txt.begins_with("钥石 "): _problems.append("状态面板余额徽投影缺失(%s)" % bal_txt) # ---- 本局信息面板换代断言(迭代36 卡③——真实入口三断言)---- # ① 面板子树零旧段文案(跨关概览退役零残留) var sv_labels: Array = [] - _collect_labels_by_text(main.ui.modals.status_panel, sv_labels) + _collect_labels_by_text(main.ui.modals.run.status_panel, sv_labels) for l in sv_labels: if String(l.text).contains("道具工坊") or String(l.text).contains("天赋树"): _problems.append("状态面板子树残留旧段文案「%s」(sv6①——零旧段失守)" % String(l.text)) @@ -874,8 +874,8 @@ func _run_modal_probes_once() -> void: var attr_expect := "生命 %d/%d · 攻击 %d · 护甲 AC %d · GP %d" % [ int(rnr.hero_hp), rnr.effective_max_hp(), rnr.effective_atk(), rnr.effective_ac(), int(rnr.gp)] - if String(main.ui.modals.status_panel._attrs_label.text) != attr_expect: - _problems.append("状态面板属性行投影失真(sv6②——期望 %s 实测 %s)" % [attr_expect, main.ui.modals.status_panel._attrs_label.text]) + if String(main.ui.modals.run.status_panel._attrs_label.text) != attr_expect: + _problems.append("状态面板属性行投影失真(sv6②——期望 %s 实测 %s)" % [attr_expect, main.ui.modals.run.status_panel._attrs_label.text]) # ③ _status_data() 直调对拍(sv7——键集恰八键+curios/feats 独立 oracle) var sd: Dictionary = main._status_data() var keys := ["balance", "hero_hp", "max_hp", "atk", "ac", "gp", "curios", "feats"] @@ -899,22 +899,22 @@ func _run_modal_probes_once() -> void: var feats_expect: Array = load("res://presentation/hud_presenter.gd").feat_holdings_lines(main.db, rnr.feats_held()) if str((sd as Dictionary).get("feats", [])) != str(feats_expect): _problems.append("_status_data().feats 对拍 feat_holdings_lines 失守(sv7)") - var resume_btn: Button = ProbeUiUtil.find_button(main.ui.modals.status_panel, "继续", "prefix") + var resume_btn: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix") if resume_btn == null: _problems.append("状态面板关闭钮缺失(「继续」)") else: resume_btn.pressed.emit() - if main.ui.modals.status_panel.visible or main.ui.modals.flow.locks_input(): + if main.ui.modals.run.status_panel.visible or main.ui.flow.locks_input(): _problems.append("状态面板关闭后未恢复(visible=%s locked=%s——R3 残留)" - % [str(main.ui.modals.status_panel.visible), str(main.ui.modals.flow.locks_input())]) + % [str(main.ui.modals.run.status_panel.visible), str(main.ui.flow.locks_input())]) # P1 守卫:图例开着 → 两处入口角标置灰;置灰态点击被处理器兜住(探针 emit 绕过 disabled) main.ui.battle.board_zone.ap_bar._btn_legend.pressed.emit() if not main.ui.battle.board_zone.status_badge.disabled: _problems.append("图例开着时战斗屏状态角标未置灰(P1 守卫失守)") main.ui.battle.board_zone.status_badge.pressed.emit() - if main.ui.modals.status_panel.visible: + if main.ui.modals.run.status_panel.visible: _problems.append("置灰态仍可开状态面板(守卫兜底失守)") - main.ui.modals.legend_overlay.close() + main.ui.modals.run.legend_overlay.close() # ③ tooltip(状态效果槽悬停——来源+语义+剩余):有槽可见时触发 if not _tested_tooltip: var row = main.ui.battle.stage.hero_standee.slot_row # 状态 tooltip 载体(B 区站桩) @@ -1045,7 +1045,7 @@ func _locktest_tick() -> void: if settlement == null or settlement.minesweeper.current_board() == null: return if _locktest_state == 0: - if main.ui.screens.row_pacer.pending_rows().is_empty() or main.ui.modals.flow.locks_input(): + if main.ui.screens.row_pacer.pending_rows().is_empty() or main.ui.flow.locks_input(): return if String(settlement.battle_result) != "" or String(settlement.action_economy.phase) != "Active": return @@ -1067,7 +1067,7 @@ func _locktest_tick() -> void: return _locktest_state = 1 elif _locktest_state == 1: - if main.ui.modals.curio_overlay.visible or main.ui.screens.inter_overlay.visible or main.ui.modals.end_overlay.is_open() \ + if main.ui.modals.run.curio_overlay.visible or main.ui.screens.inter_overlay.visible or main.ui.modals.run.end_overlay.is_open() \ or String(settlement.battle_result) != "": _locktest_state = 2 # 前提消失守卫:终结演播竞态——静默弃测 return diff --git a/game/probe/ui_input_probe.gd b/game/probe/ui_input_probe.gd index 9e2285b0..0909c65c 100644 --- a/game/probe/ui_input_probe.gd +++ b/game/probe/ui_input_probe.gd @@ -31,15 +31,15 @@ func _ready() -> void: # 同 ui_click_probe 隔离纪律。首次冒险布尔位置位=对齐真实档游玩史口径,模板抽签路径不变; # isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退) ProbeUiUtil.isolate_save(main, "smoke_input_probe_save", true, false, true) - main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,探针确定性禁依赖默认分支) + main.ui.modals.menu.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,探针确定性禁依赖默认分支) main._start_adventure_from_menu() await get_tree().process_frame # ---- ⑫ 图上命中真实输入(回归闸):入口停点候选节点中心 # push_input 真实鼠标路径 motion+click → 悬停预承诺置位 → route_chosen 到达组合根。 # 由来=真机 bug:深层节点(L1 Boss 内容 x=802)在固定画布命中区外,press_candidate 直调 # 命中函数探针全绿、真鼠标点不中——本 case 封住输入管线,⑬封住几何拟合。 - if main.ui.modals.level_screen.visible: - var ro = main.ui.modals.level_screen + if main.ui.modals.run.level_screen.visible: + var ro = main.ui.modals.run.level_screen var cands: Array = main.run_scope.runner.next_candidate_ids() # ---- ⑱ 信息迷雾呈现接线(回归闸):真机 display_probe 读实际绘制记录(雾下节点类型未画、 # Boss 描边画了、候选类型画了)+点击雾下非候选节点零提交(雾不扩大可点域——候选集即合法集)---- @@ -90,9 +90,9 @@ func _ready() -> void: await get_tree().process_frame await get_tree().process_frame ro.route_chosen.disconnect(on_route) - if not submitted.is_empty() or not main.ui.modals.level_screen.visible or not main.run_scope.runner.awaiting_choice(): + if not submitted.is_empty() or not main.ui.modals.run.level_screen.visible or not main.run_scope.runner.awaiting_choice(): _pre_fail("case⑱ 雾下节点点击产生提交(提交=%s visible=%s 选路态=%s——雾不扩大可点域)〔fv9〕" - % [str(submitted), str(main.ui.modals.level_screen.visible), str(main.run_scope.runner.awaiting_choice())]) + % [str(submitted), str(main.ui.modals.run.level_screen.visible), str(main.run_scope.runner.awaiting_choice())]) return var nds: Array = main.run_scope.runner.nodes() var tgt := {} @@ -116,13 +116,13 @@ func _ready() -> void: main.get_viewport().push_input(evc, true) await get_tree().process_frame await get_tree().process_frame - if main.ui.modals.level_screen.visible or main.run_scope.runner.awaiting_choice(): - _pre_fail("图上真实点击未提交选路(overlay 可见=%s 选路态=%s)" % [str(main.ui.modals.level_screen.visible), str(main.run_scope.runner.awaiting_choice())]) + if main.ui.modals.run.level_screen.visible or main.run_scope.runner.awaiting_choice(): + _pre_fail("图上真实点击未提交选路(overlay 可见=%s 选路态=%s)" % [str(main.ui.modals.run.level_screen.visible), str(main.run_scope.runner.awaiting_choice())]) for i in range(3): await get_tree().process_frame # ---- ⑬ 画布随图拟合(同 bug 的几何面回归):复刻分层公式独立推导最深节点内容 x, # 断言画布 min 尺寸覆盖之(open_choices 每 stop 拟合——L1 单链 Boss x=70+6*122=802 曾越界) - var ro2 = main.ui.modals.level_screen + var ro2 = main.ui.modals.run.level_screen var nds2: Array = main.run_scope.runner.nodes() var layer2 := {} var inbound2 := {} @@ -522,24 +522,24 @@ func _ready() -> void: _push_click(bpos, MOUSE_BUTTON_LEFT) _push_release(bpos, MOUSE_BUTTON_LEFT) # Button pressed 信号走 release 沿——按压+释放成对 await get_tree().process_frame - if not main.ui.modals.status_panel.visible or not main.ui.modals.flow.locks_input(): + if not main.ui.modals.run.status_panel.visible or not main.ui.flow.locks_input(): _problems.append("case⑯ 角标真点击未开状态面板(visible=%s locked=%s——入口可达性失守)" - % [str(main.ui.modals.status_panel.visible), str(main.ui.modals.flow.locks_input())]) + % [str(main.ui.modals.run.status_panel.visible), str(main.ui.flow.locks_input())]) else: _push_click(_pos(bv, status_dig), MOUSE_BUTTON_LEFT) await get_tree().process_frame await get_tree().process_frame if _has_action(status_dig, "dig") or ms.is_revealed(status_dig): _problems.append("case⑯ 状态面板开启时挖掘穿透(R3 输入锁破——目标=%s)" % status_dig) - var resume_btn: Button = ProbeUiUtil.find_button(main.ui.modals.status_panel, "继续", "prefix") + var resume_btn: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix") if resume_btn == null: _problems.append("case⑯ 状态面板关闭钮缺失") else: resume_btn.pressed.emit() await get_tree().process_frame - if main.ui.modals.status_panel.visible or main.ui.modals.flow.locks_input(): + if main.ui.modals.run.status_panel.visible or main.ui.flow.locks_input(): _problems.append("case⑯ 关闭后未恢复(visible=%s locked=%s——R3 残留)" - % [str(main.ui.modals.status_panel.visible), str(main.ui.modals.flow.locks_input())]) + % [str(main.ui.modals.run.status_panel.visible), str(main.ui.flow.locks_input())]) _push_click(_pos(bv, status_dig), MOUSE_BUTTON_LEFT) await get_tree().process_frame await get_tree().process_frame diff --git a/game/probe/ui_probe.gd b/game/probe/ui_probe.gd index 640d3a81..2e6295ca 100644 --- a/game/probe/ui_probe.gd +++ b/game/probe/ui_probe.gd @@ -16,7 +16,7 @@ func _ready() -> void: await get_tree().process_frame var problems: Array = [] # ---- 放弃入口(台账 119):默认隐藏(非冒险模式无冒险可放弃——开始层/调试两层)---- - if main.ui.modals.level_screen.abandon_btn == null or main.ui.modals.level_screen.abandon_btn.visible: + if main.ui.modals.run.level_screen.abandon_btn == null or main.ui.modals.run.level_screen.abandon_btn.visible: print("[ui_probe] FAIL — 放弃按钮默认应隐藏(非冒险模式关)") get_tree().quit(1) return @@ -65,14 +65,14 @@ func _ready() -> void: problems.append("营地关闭后未恢复开始层(开合往返失守)〔句柄子容器批 V1〕") # 换回全新零档(营地段购买态不泄入后续冒险段——确定性;先删后建防陈旧档泄入) ProbeUiUtil.isolate_save(main, "smoke_ui_probe_camp", true, false, true) - main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机;本探针 start_single 第五路同吃此框,确定性禁依赖默认分支) + main.ui.modals.menu.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机;本探针 start_single 第五路同吃此框,确定性禁依赖默认分支) main._start_adventure_from_menu() await get_tree().process_frame await get_tree().process_frame # ---- 入口选路停点(关卡开始即出节点图——level_started 后、首战前弹选路层; # press_candidate 图上命中路径 pick 入口单候选进首战)---- # ---- 独立性断言(独立屏激活期战斗屏宿主整体隐藏,零透出)---- - if not main.ui.modals.level_screen.visible: + if not main.ui.modals.run.level_screen.visible: problems.append("关卡开始未弹入口选路停点(level_screen 应可见)") elif main.ui.screens.battle_root.visible: problems.append("独立屏激活期战斗屏宿主未隐藏(零透出契约失守)") @@ -80,16 +80,16 @@ func _ready() -> void: or main.run_scope.runner.next_candidate_ids().size() != 1: problems.append("入口停点状态失守(选路态 + 入口单候选)") else: - main.ui.modals.level_screen.press_candidate(0) + main.ui.modals.run.level_screen.press_candidate(0) await get_tree().process_frame await get_tree().process_frame - if not main.ui.screens.battle_root.visible or main.ui.modals.level_screen.visible: + if not main.ui.screens.battle_root.visible or main.ui.modals.run.level_screen.visible: problems.append("选路提交战斗节点后未切回战斗屏(屏级互斥恢复失守)") # ---- 放弃入口·冒险模式可见 + 战斗中按下被拒(真实按钮路径——入口挂选路停点,战斗中拒绝为防御分支)---- - if main.ui.modals.level_screen.abandon_btn == null or not main.ui.modals.level_screen.abandon_btn.visible: + if main.ui.modals.run.level_screen.abandon_btn == null or not main.ui.modals.run.level_screen.abandon_btn.visible: problems.append("冒险模式放弃按钮未开放(选路停点入口在案)") else: - main.ui.modals.level_screen.abandon_btn.pressed.emit() + main.ui.modals.run.level_screen.abandon_btn.pressed.emit() await get_tree().process_frame if main.run_scope.adventure == null or String(main.run_scope.adventure.state) != "running": problems.append("首战中放弃未被拒(战斗域单事务在飞——state 应保持 running)") @@ -233,12 +233,12 @@ func _ready() -> void: main._on_adventure_finished({"result": "completed", "cleared": true, "levels_cleared": 3, "stopped_at": 0, "keystone_total": 30, "master_seed": 42, "gp_final": 57}) main.meta.save = meta_saved - if not main.ui.modals.end_overlay.is_open(): + if not main.ui.modals.run.end_overlay.is_open(): problems.append("冒险终局面板未开(合成摘要路径)") - elif not main.ui.modals.end_overlay._gp_row.visible or not main.ui.modals.end_overlay._gp_val.text.contains("57"): - problems.append("终局面板 GP 行缺失或取数不对(%s)" % main.ui.modals.end_overlay._gp_val.text) - main.ui.modals.end_overlay.close() - main.ui.modals.flow.close(main.ui.modals.end_overlay) + elif not main.ui.modals.run.end_overlay._gp_row.visible or not main.ui.modals.run.end_overlay._gp_val.text.contains("57"): + problems.append("终局面板 GP 行缺失或取数不对(%s)" % main.ui.modals.run.end_overlay._gp_val.text) + main.ui.modals.run.end_overlay.close() + main.ui.flow.close(main.ui.modals.run.end_overlay) if problems.is_empty(): print("[ui_probe] PASS — cells=%d board=%dx%d hp='%s' enemies=%d ap_dots=%d" % [ main.ui.battle.board_zone.board_view.cells_probe().size(), main.ui.battle.board_zone.board_view.board().width, main.ui.battle.board_zone.board_view.board().height, diff --git a/game/probe/ui_struct_probe.gd b/game/probe/ui_struct_probe.gd index aa2e73e4..0a77e69c 100644 --- a/game/probe/ui_struct_probe.gd +++ b/game/probe/ui_struct_probe.gd @@ -34,7 +34,7 @@ func _ready() -> void: main._open_settings() _settle() - await _audit("settings", [main.ui.modals.settings_overlay]) + await _audit("settings", [main.ui.modals.menu.settings_overlay]) main._on_settings_closed() _settle() @@ -94,22 +94,22 @@ func _ready() -> void: _settle() main.ui.modals.camp.scene.object_pressed.emit(&"notebook") _settle() - if not main.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible: + if not main.ui.modals.menu.career_overlay.visible or main.ui.screens.start_overlay.visible: problems.append("[career] 生涯页未打开或开始层未隐藏(笔记本改挂路径断裂)〔营地图批 V5〕") - await _audit("career", [main.ui.modals.career_overlay]) + await _audit("career", [main.ui.modals.menu.career_overlay]) _career_readonly_contract(main) _career_zero_motion_contract(main) _career_nojump_contract(main) main._on_career_closed() _settle() - if main.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible: + if main.ui.modals.menu.career_overlay.visible or main.ui.screens.start_overlay.visible: problems.append("[career] 关闭后页未隐藏或开始层回弹(营地语境分支失守)〔营地图批 V5〕") # ---- Esc 浮窗结构段(卡⑤——三钮在位且仅三+零动效契约+登记序在册+debug_badge 不波及)---- main._camp_pres.open_camp() _settle() main.ui.modals.camp.scene.object_pressed.emit(&"gate") _settle() - main.ui.modals.confirm_dialog.cancelled.emit() # 关大门弹窗(清场——浮窗段前提) + main.ui.modals.menu.confirm_dialog.cancelled.emit() # 关大门弹窗(清场——浮窗段前提) _settle() var push_esc := func() -> void: var ev := InputEventKey.new() @@ -175,37 +175,37 @@ func _ready() -> void: problems.append("[savesel] 开层前激活态非零(sl9 重启链——boot/注入面后应无激活)") main._open_save_select() _settle() - if not main.ui.modals.save_select_overlay.visible or main.ui.screens.start_overlay.visible: + if not main.ui.modals.menu.save_select_overlay.visible or main.ui.screens.start_overlay.visible: problems.append("[savesel] 选档层未打开或开始层未隐藏(开层函数断裂)") - await _audit("savesel", [main.ui.modals.save_select_overlay]) + await _audit("savesel", [main.ui.modals.menu.save_select_overlay]) _save_select_contract(main, ssel_tpl) main._on_save_select_closed() _settle() - if main.ui.modals.save_select_overlay.visible or not main.ui.screens.start_overlay.visible: + if main.ui.modals.menu.save_select_overlay.visible or not main.ui.screens.start_overlay.visible: problems.append("[savesel] 关闭后层未隐藏或开始层未恢复(模态栈单口失守)") - main.ui.modals.confirm_dialog.open("结构探针采样", "确认") + main.ui.modals.menu.confirm_dialog.open("结构探针采样", "确认") _settle() - await _audit("confirm", [main.ui.modals.confirm_dialog]) - main.ui.modals.confirm_dialog.close() + await _audit("confirm", [main.ui.modals.menu.confirm_dialog]) + main.ui.modals.menu.confirm_dialog.close() _settle() main._show_debug_overlay() _settle() - await _audit("debug", [main.ui.modals.debug_overlay]) + await _audit("debug", [main.ui.modals.menu.debug_overlay]) main._close_debug_overlay() _settle() # ---- 冒险簇(真流推进:开冒险→地图选路态→选战斗节点→战斗 HUD)---- ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_adv", true, false, true) - main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,结构采样确定性禁依赖默认分支) + main.ui.modals.menu.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,结构采样确定性禁依赖默认分支) main._start_adventure_from_menu() _settle(4) - await _audit("level_map", [main.ui.modals.level_screen]) + await _audit("level_map", [main.ui.modals.run.level_screen]) var battle_id := _pick_battle_node(main) if battle_id >= 0: - main.ui.modals.level_screen.route_chosen.emit(battle_id) + main.ui.modals.run.level_screen.route_chosen.emit(battle_id) _settle(6) _audit("battle_hud", [main.ui.battle.board_zone, main.ui.battle.info_panel, main.ui.battle.stage]) _standeefit_contract(main) @@ -215,12 +215,12 @@ func _ready() -> void: # 一律红,须先入册审查);白名单={视图自身 _emit_closed, 组合根 _on_status_closed}。 main._open_status_panel() _settle() - await _audit("status", [main.ui.modals.status_panel]) + await _audit("status", [main.ui.modals.run.status_panel]) _status_readonly_contract(main) _status_zero_motion_contract(main) main._on_status_closed() _settle() - if main.ui.modals.status_panel.visible: + if main.ui.modals.run.status_panel.visible: problems.append("[status] 关闭后层未隐藏(模态栈单口失守)") # ---- 图例层(旧模态、首过结构族):ap_bar legend_toggled @@ -228,61 +228,61 @@ func _ready() -> void: # closed 信号真路径关(close()→组合根 _on_modal_closed)。 main.ui.battle.board_zone.legend_toggled.emit() _settle() - if not main.ui.modals.legend_overlay.visible: + if not main.ui.modals.run.legend_overlay.visible: problems.append("[legend] 图例层未打开(legend_toggled 路径中断)") - await _audit("legend", [main.ui.modals.legend_overlay]) - main.ui.modals.legend_overlay.close() + await _audit("legend", [main.ui.modals.run.legend_overlay]) + main.ui.modals.run.legend_overlay.close() _settle() - if main.ui.modals.legend_overlay.visible: + if main.ui.modals.run.legend_overlay.visible: problems.append("[legend] 关闭后层未隐藏(模态栈单口失守)") else: - problems.append("[battle_hud] 候选中无战斗节点(驱动中断——候选=%s)" % str(main.ui.modals.level_screen._cand_ids)) + problems.append("[battle_hud] 候选中无战斗节点(驱动中断——候选=%s)" % str(main.ui.modals.run.level_screen._cand_ids)) # ---- 兑付屏(真实 open 面 + 合成最小数据——结构断言只看矩形/宽度/可见性)---- - main.ui.modals.end_overlay.open_victory("结构探针·胜利演算", "返回", Rect2(200, 200, 400, 300), {"gp_summary": "GP 0"}) + main.ui.modals.run.end_overlay.open_victory("结构探针·胜利演算", "返回", Rect2(200, 200, 400, 300), {"gp_summary": "GP 0"}) _settle() - await _audit("battle_end", [main.ui.modals.end_overlay]) - main.ui.modals.end_overlay.close() + await _audit("battle_end", [main.ui.modals.run.end_overlay]) + main.ui.modals.run.end_overlay.close() _settle() # ---- 节点结算层(旧模态、首过结构族):rest 兑付真路径开 #(组合根 _open_modal 登记册+show_rest 合成喂入)→结构断言族→continue_pressed 信号真路径关 #(press_continue→组合根 _on_node_continue→_close_modal)。 - main.ui.modals.flow.open(main.ui.modals.node_overlay) - main.ui.modals.node_overlay.show_rest(3, 15, 20) + main.ui.flow.open(main.ui.modals.run.node_overlay) + main.ui.modals.run.node_overlay.show_rest(3, 15, 20) _settle() - if not main.ui.modals.node_overlay.visible: + if not main.ui.modals.run.node_overlay.visible: problems.append("[node] 节点结算层未打开(rest 兑付路径中断)") - await _audit("node_overlay", [main.ui.modals.node_overlay]) - main.ui.modals.node_overlay.press_continue() + await _audit("node_overlay", [main.ui.modals.run.node_overlay]) + main.ui.modals.run.node_overlay.press_continue() _settle() - if main.ui.modals.node_overlay.visible: + if main.ui.modals.run.node_overlay.visible: problems.append("[node] 关闭后层未隐藏(continue_pressed 路径中断)") - main.ui.modals.curio_overlay.open_pick("结构探针·三选一", [ + main.ui.modals.run.curio_overlay.open_pick("结构探针·三选一", [ {"id": "probe_heal", "text": "回血\n恢复 3 点"}, {"id": "probe_magic", "text": "爆裂轰击\n下一击 +5"}, {"id": "probe_pierce", "text": "穿甲\n无视护甲"}, ]) _settle() - await _audit("curio_pick", [main.ui.modals.curio_overlay]) - main.ui.modals.curio_overlay.visible = false + await _audit("curio_pick", [main.ui.modals.run.curio_overlay]) + main.ui.modals.run.curio_overlay.visible = false _settle() - main.ui.modals.shop_overlay.open_shop([ + main.ui.modals.menu.shop_overlay.open_shop([ {"product_id": "probe_p1", "display_name": "探针商品甲", "attr": "atk", "flat_value": 1, "price": 10, "purchased": 0}, {"product_id": "probe_p2", "display_name": "探针商品乙", "attr": "hp", "flat_value": 2, "price": 20, "purchased": 0}, ], 100) _settle() - await _audit("shop", [main.ui.modals.shop_overlay]) + await _audit("shop", [main.ui.modals.menu.shop_overlay]) # ---- 术语传导:货架卡与三选一卡片的玩家面文案(物品名=专长获取、分类=专长凭证) - main.ui.modals.shop_overlay.open_shelf([ + main.ui.modals.menu.shop_overlay.open_shelf([ {"kind_id": "feat_ticket", "display_name": "专长获取", "value": 0, "price": 5}, {"kind_id": "heal", "display_name": "应急回复", "value": 6, "price": 5}, ], 20) _settle() - var ticket_row: Dictionary = main.ui.modals.shop_overlay._shelf_rows[0] + var ticket_row: Dictionary = main.ui.modals.menu.shop_overlay._shelf_rows[0] if String(main.db.run.shop.pool_row("feat_ticket").get("display_name", "")) != "专长获取": problems.append("[shop_term] shop.tres 专长凭证件名未换代(现=%s)" % String(main.db.run.shop.pool_row("feat_ticket").get("display_name", ""))) if ticket_row.card.name_l.text != "专长获取": @@ -293,7 +293,7 @@ func _ready() -> void: problems.append("[shop_term] 专长凭证件描述未用规范流程句(现=%s)" % ticket_row.card.desc_l.text) if ticket_row.card.foot_l.text != "专长凭证": problems.append("[shop_term] 专长凭证件分类标注未用「专长凭证」(现=%s)" % ticket_row.card.foot_l.text) - var heal_row: Dictionary = main.ui.modals.shop_overlay._shelf_rows[1] + var heal_row: Dictionary = main.ui.modals.menu.shop_overlay._shelf_rows[1] if heal_row.card.desc_l.text != "回复 +6(本冒险)": problems.append("[shop_term] 回复件描述漏了 heal 词表(现=%s)" % heal_row.card.desc_l.text) if heal_row.card.foot_l.text != "回复": @@ -305,7 +305,7 @@ func _ready() -> void: problems.append("[curio_term] 专长凭证三选一卡未用物品名/规范流程句(现=%s)" % ticket_text) if ticket_text.contains("门票") or ticket_text.contains("T5") or ticket_text.contains("持有集") or ticket_text.contains("奇物三选一"): problems.append("[curio_term] 专长凭证三选一卡残留退役词/内部任务号(现=%s)" % ticket_text) - main.ui.modals.shop_overlay.visible = false + main.ui.modals.menu.shop_overlay.visible = false _settle() # ---- ⑦resume 专项:续玩停点恢复(main.gd:717 报错回归闸)---- @@ -316,11 +316,11 @@ func _ready() -> void: main.run_scope.runner = null # 全新启动等价态(真机=菜单直进续玩,无前置冒险——runner 从未赋值;探针此前推流会残留旧 runner) main._resume_from_menu() _settle(8) - if not main.ui.modals.end_overlay.visible: + if not main.ui.modals.run.end_overlay.visible: problems.append("[resume] 续玩恢复后胜利总结面未开(三选一停点分支中断)") else: var found := {"ok": false} - _find_label_text(main.ui.modals.end_overlay, "合计 24", found) + _find_label_text(main.ui.modals.run.end_overlay, "合计 24", found) if not found.ok: problems.append("[resume] 胜利总结面 GP 行未见快照合计「合计 24」(runner 缺位守卫缺位——717 回归)") var s: String = main._gp_summary_text({"gp_dropped": 0}) @@ -362,7 +362,7 @@ func _find_label_text(node: Node, needle: String, found: Dictionary) -> void: func _pick_battle_node(main: Node) -> int: # 入口/兑付停点走 level_screen.open_choices 直传(不写 _pending_candidates)—— # 候选从视图态取:_cand_ids × runner.nodes() 的 type(编号解析=node_id_of 单一出口) - var ls: Node = main.ui.modals.level_screen + var ls: Node = main.ui.modals.run.level_screen if ls._map._runner == null: return -1 for nd in ls._map._runner.nodes(): @@ -388,7 +388,7 @@ const _PAUSE_READONLY_SIGNALS: Array[String] = [ func _status_readonly_contract(main: Node) -> void: - var overlay: Control = main.ui.modals.status_panel + var overlay: Control = main.ui.modals.run.status_panel var stack: Array = [overlay] var conn_count := 0 while not stack.is_empty(): @@ -414,7 +414,7 @@ func _status_readonly_contract(main: Node) -> void: # ⑨status 零动效契约(R5/P4 甲——零动效直开直关路径存在断言): # 子树零 AnimationPlayer + 视图源码零 tween/动效包装调用(源码 token 扫描=ui_contrast 静态段同手法) func _status_zero_motion_contract(main: Node) -> void: - var overlay: Control = main.ui.modals.status_panel + var overlay: Control = main.ui.modals.run.status_panel var stack: Array = [overlay] while not stack.is_empty(): var n: Node = stack.pop_back() @@ -434,7 +434,7 @@ func _status_zero_motion_contract(main: Node) -> void: # 清单外零连接。白名单={视图自身 _emit_closed〔返回钮〕, 组合根 _on_career_closed};页内任何 # 购买/导航/写入方向的新连接一律红,须先入册审查。 func _career_readonly_contract(main: Node) -> void: - var overlay: Control = main.ui.modals.career_overlay + var overlay: Control = main.ui.modals.menu.career_overlay var stack: Array = [overlay] var conn_count := 0 while not stack.is_empty(): @@ -459,7 +459,7 @@ func _career_readonly_contract(main: Node) -> void: # ⑪career 零动效契约(状态面板 ⑨ 同形制——P4 甲直开直关) func _career_zero_motion_contract(main: Node) -> void: - var overlay: Control = main.ui.modals.career_overlay + var overlay: Control = main.ui.modals.menu.career_overlay var stack: Array = [overlay] while not stack.is_empty(): var n: Node = stack.pop_back() @@ -480,7 +480,7 @@ func _career_zero_motion_contract(main: Node) -> void: # _career_registry_contract 的断言(career=状态面板后/确认弹窗前、树序同步)由本全量合同覆盖) # ⑭选档界面契约(卡③ sl1/sl2/sl3/sl4/sl9——呈现面全部来自喂入数据,视图零自判) func _save_select_contract(main: Node, tpl: String) -> void: - var sso: Node = main.ui.modals.save_select_overlay + var sso: Node = main.ui.modals.menu.save_select_overlay # sl1 三槽呈现:槽卡计数==3+零滚动容器(禁滚动长列表形态——契约 1) var rows: VBoxContainer = sso._rows_box var live_rows := 0 @@ -559,7 +559,7 @@ func _collect_labels(node: Node, out: Array[String]) -> void: func _modal_registry_contract(main: Node) -> void: - var list: Array = main.ui.modals.flow.stack._list + var list: Array = main.ui.flow.stack._list # ① 登记册序==合同模态序(逐位点名——合同是登记序唯一事实源) var contract_modals: Array = [] for entry: Dictionary in main.ui.LAYER_CONTRACT: @@ -592,23 +592,42 @@ func _modal_registry_contract(main: Node) -> void: prev.name, node.name, node.get_index(), prev.get_index()]) prev = node # ③ 宿住层特例:level_screen 宿主=冒险屏壳且在全直接子模态之下(遮挡语义归模态栈 screen_state) - var lv: Node = main.ui.modals.level_screen + var lv: Node = main.ui.modals.run.level_screen if lv.get_parent() != main.ui.screens.adventure_screen: problems.append("[registry] level_screen 宿主应=冒险屏壳(实测 %s)〔装配生命批 V1〕" % lv.get_parent().name) - elif lv.get_parent().get_index() >= main.ui.modals.legend_overlay.get_index(): + elif lv.get_parent().get_index() >= main.ui.modals.run.legend_overlay.get_index(): problems.append("[registry] 冒险屏壳应在全模态之下(宿主 idx=%d legend idx=%d)〔装配生命批 V1〕" % [ - lv.get_parent().get_index(), main.ui.modals.legend_overlay.get_index()]) + lv.get_parent().get_index(), main.ui.modals.run.legend_overlay.get_index()]) # ④ 营地同槽特例:camp_screen 屏壳树序夹商店层与设置层之间(成员 camp_scene 登记位代言) var cs: Node = main.ui.screens.camp_screen - if cs.get_index() <= main.ui.modals.shop_overlay.get_index() or cs.get_index() >= main.ui.modals.settings_overlay.get_index(): + if cs.get_index() <= main.ui.modals.menu.shop_overlay.get_index() or cs.get_index() >= main.ui.modals.menu.settings_overlay.get_index(): problems.append("[registry] camp_screen 屏壳树序应夹商店与设置层之间(idx=%d shop=%d settings=%d)〔装配生命批 V1〕" % [ - cs.get_index(), main.ui.modals.shop_overlay.get_index(), main.ui.modals.settings_overlay.get_index()]) + cs.get_index(), main.ui.modals.menu.shop_overlay.get_index(), main.ui.modals.menu.settings_overlay.get_index()]) + # ⑤ 分域结构(句柄分域批 T1——两新子容器在座/平铺面清零/编排口独立) + # V1a 分域在座:run/menu 各六句柄经新二段式寻址逐件非 null(12 检) + var run_names: Array[String] = ["legend_overlay", "node_overlay", "level_screen", "curio_overlay", "end_overlay", "status_panel"] + var menu_names: Array[String] = ["shop_overlay", "settings_overlay", "career_overlay", "save_select_overlay", "confirm_dialog", "debug_overlay"] + for n in run_names: + if main.ui.modals.run.get(n) == null: + problems.append("[registry] run 域句柄缺失: %s〔句柄分域批 V1〕" % n) + for n in menu_names: + if main.ui.modals.menu.get(n) == null: + problems.append("[registry] menu 域句柄缺失: %s〔句柄分域批 V1〕" % n) + # V1b 平铺面清零:12 旧平铺名经 get() 逐件为 null(旧寻址成员不存在——纯搬家零别名) + for n in run_names + menu_names: + if main.ui.modals.get(n) != null: + problems.append("[registry] 平铺残留: modals.%s 应已迁域(get 非 null)〔句柄分域批 V1〕" % n) + # V1c 编排口独立:ModalFlow 直驻注册面(ui.flow 非 null)且 modals 无 flow 成员 + if main.ui.flow == null: + problems.append("[registry] ui.flow 直驻缺失〔句柄分域批 V1〕") + if main.ui.modals.get("flow") != null: + problems.append("[registry] modals.flow 残留(应升格独立持有)〔句柄分域批 V1〕") # ⑬career 零跳转契约(提纲 P4 甲——页内零「前往他处」入口:除「返回」外任何导航钮=红) func _career_nojump_contract(main: Node) -> void: for nav in ["营地", "商店", "设置", "冒险", "继续上局", "退出", "前往"]: - if ProbeUiUtil.find_button(main.ui.modals.career_overlay, nav, "contains") != null: + if ProbeUiUtil.find_button(main.ui.modals.menu.career_overlay, nav, "contains") != null: problems.append("[career][nojump] 页内出现导航入口「%s」(零跳转违约——生涯纯只读)" % nav) diff --git a/game/probe/ui_tune_probe.gd b/game/probe/ui_tune_probe.gd index dab76e35..c3eb8d13 100644 --- a/game/probe/ui_tune_probe.gd +++ b/game/probe/ui_tune_probe.gd @@ -45,33 +45,33 @@ func _ready() -> void: # 悬浮于菜单之上——开始层不隐藏;树序新基准=登记册末位(确认弹窗之后=绘制序最上)) main.ui.screens.debug_badge.pressed.emit() await get_tree().process_frame - if not main.ui.modals.debug_overlay.visible: + if not main.ui.modals.menu.debug_overlay.visible: problems.append("调试浮层角标点击后浮窗未开(全局浮层入口失守)") if not main.ui.screens.start_overlay.visible: problems.append("开始覆盖层被隐藏——浮窗应悬浮于菜单之上(14.5 形态失守)") - if main.ui.modals.debug_overlay.get_index() < main.ui.modals.confirm_dialog.get_index(): + if main.ui.modals.menu.debug_overlay.get_index() < main.ui.modals.menu.confirm_dialog.get_index(): problems.append("调试浮窗树序不在确认弹窗之后——登记册末位失守") # ② 滑条 → 生效预览联动 - main.ui.modals.debug_overlay.set_sliders(100, 50) - if str(boosted_atk) not in main.ui.modals.debug_overlay.effect_text() or str(boosted_hp) not in main.ui.modals.debug_overlay.effect_text(): - problems.append("生效预览未联动(%s)" % main.ui.modals.debug_overlay.effect_text()) + main.ui.modals.menu.debug_overlay.set_sliders(100, 50) + if str(boosted_atk) not in main.ui.modals.menu.debug_overlay.effect_text() or str(boosted_hp) not in main.ui.modals.menu.debug_overlay.effect_text(): + problems.append("生效预览未联动(%s)" % main.ui.modals.menu.debug_overlay.effect_text()) # ③ 应用(真实信号路径:apply_requested → 组合根)——E4 后加成零表写,生效断言移至 ⑧ 战斗快照 - main.ui.modals.debug_overlay.press_apply_for_probe() + main.ui.modals.menu.debug_overlay.press_apply_for_probe() # ④ 重置(重开浮窗 → 重置按钮真实路径;面板百分比归零——下一场战斗生效值回到基线) main._show_debug_overlay() - main.ui.modals.debug_overlay.press_reset_for_probe() + main.ui.modals.menu.debug_overlay.press_reset_for_probe() # ⑤ 批量模拟全链路(N=10——走 level_autoplay 与 headless 扫描同口径;表基线由组合根复位; # 菜单态上下文未锁定=按钮可点,is_running 语义=模拟在跑〔18卡⑦ 与对局禁用态拆分〕) - main.ui.modals.debug_overlay.set_sliders(100, 0) - main.ui.modals.debug_overlay.set_sim_count(10) - main.ui.modals.debug_overlay.press_simulate_for_probe() + main.ui.modals.menu.debug_overlay.set_sliders(100, 0) + main.ui.modals.menu.debug_overlay.set_sim_count(10) + main.ui.modals.menu.debug_overlay.press_simulate_for_probe() var guard := SIM_GUARD_FRAMES - while main.ui.modals.debug_overlay.is_running() and guard > 0: + while main.ui.modals.menu.debug_overlay.is_running() and guard > 0: guard -= 1 await get_tree().process_frame - if main.ui.modals.debug_overlay.is_running(): + if main.ui.modals.menu.debug_overlay.is_running(): problems.append("模拟未在预算帧内完成(疑似挂死)") - var out: String = main.ui.modals.debug_overlay.output_text() + var out: String = main.ui.modals.menu.debug_overlay.output_text() if not out.contains("关卡完成率"): problems.append("模拟输出缺关卡完成率") # 口径换代 V7:胜局统计行措辞钉子(atk+100% 加成档 10 局必有胜局——hp_left_win 非空路径恒出) @@ -83,7 +83,7 @@ func _ready() -> void: problems.append("模拟输出 N 标注错误(%s)" % out.substr(0, 40)) # ⑥ 关闭 → 浮窗收起、开始层全程在屏 main._close_debug_overlay() - if main.ui.modals.debug_overlay.visible or not main.ui.screens.start_overlay.visible: + if main.ui.modals.menu.debug_overlay.visible or not main.ui.screens.start_overlay.visible: problems.append("关闭后层级状态错误") # ⑦ hero 表运行期不可变(E4 收口回归断言:任何代码 reintroduce 表写在此被抓获——原"表值残留"断言语义升级) if int(main.db.combat.hero.atk) != base_atk or int(main.db.combat.hero.max_hp) != base_hp: @@ -93,20 +93,20 @@ func _ready() -> void: # 存档隔离(停点断点写盘随 main.save_manager;探针禁碰真实档,同族探针隔离纪律; # isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退) ProbeUiUtil.isolate_save(main, "smoke_tune_probe_save", true, false, true) # 末参=swap_save(卡②签名换代——断点写盘经 autoload 落隔离档) - main.ui.modals.debug_overlay.set_sliders(100, 50) - main.ui.modals.debug_overlay.press_apply_for_probe() - main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(18卡⑦审计🔴——空框=非首局随机,显式优先经真实路径断言 master_seed) + main.ui.modals.menu.debug_overlay.set_sliders(100, 50) + main.ui.modals.menu.debug_overlay.press_apply_for_probe() + main.ui.modals.menu.debug_overlay.seed_edit.text = "42" # 种子钉死(18卡⑦审计🔴——空框=非首局随机,显式优先经真实路径断言 master_seed) main._start_adventure_from_menu() # 完整冒险路径(唯一试玩入口;_start_single 按语义清零滑条 = 恒基线,不承载加成断言) await get_tree().process_frame await get_tree().process_frame # ⑧b 开冒险路径显式关浮窗(18卡⑦ T2 防护——调试会话默认开态下开局不带窗,对局中经角标再开) - if main.ui.modals.debug_overlay.visible: + if main.ui.modals.menu.debug_overlay.visible: problems.append("开冒险路径未显式关浮窗(T2 防护失守)") if int(main.run_scope.adventure.master_seed) != 42: problems.append("显式种子未达冒险(master_seed=%s ≠ 42——真实路径显式优先失守)" % str(main.run_scope.adventure.master_seed)) # 关卡开始 = 入口选路停点——真实按钮路径选入口候选进首战再读呈现快照 - if main.ui.modals.level_screen.visible: - main.ui.modals.level_screen.press_candidate(0) + if main.ui.modals.run.level_screen.visible: + main.ui.modals.run.level_screen.press_candidate(0) await get_tree().process_frame await get_tree().process_frame var hero: Dictionary = main.run_scope.settlement.presentation_snapshot().hero @@ -121,18 +121,18 @@ func _ready() -> void: problems.append("对局中调试浮层角标非恒隐(发布态语义失守——卡⑦ 删写死行批)") else: main._show_debug_overlay() # 对局中开浮窗回归经直调承载(:61 既有直调先例——发布态角标恒隐,按钮信号路径退役) - if not main.ui.modals.debug_overlay.visible: + if not main.ui.modals.menu.debug_overlay.visible: problems.append("对局中角标开浮窗失败(P2 甲对局中可达失守)") - if main.ui.modals.flow.locks_input(): + if main.ui.flow.locks_input(): problems.append("对局中开浮窗后棋盘输入锁死(T2 防护失守——S1+ 停机点)") if main.ui.battle.board_zone.status_badge.disabled: problems.append("对局中开浮窗后状态角标置灰(T2 防护失守——S1+ 停机点)") - if main.ui.modals.debug_overlay.is_running(): + if main.ui.modals.menu.debug_overlay.is_running(): problems.append("对局中上下文锁定误判为模拟在跑(is_running 语义互扰)") - if not main.ui.modals.debug_overlay._sim_btn.disabled or not main.ui.modals.debug_overlay._dbg_btn.disabled: + if not main.ui.modals.menu.debug_overlay._sim_btn.disabled or not main.ui.modals.menu.debug_overlay._dbg_btn.disabled: problems.append("对局中批量模拟/两层调试按钮未禁用(T2 并存态不开放失守)") # 当局数据六项值对(T3——数值与只读访问器逐项一致;种子=adventure.master_seed 现值非 _seed_value 重算) - var rd: Dictionary = main.ui.modals.debug_overlay.run_data_probe() + var rd: Dictionary = main.ui.modals.menu.debug_overlay.run_data_probe() var r = main.run_scope.adventure.runner() if String(rd.get("seed", "")) != "42": problems.append("当局数据种子 ≠ master_seed 现值(%s)" % str(rd.get("seed"))) @@ -146,27 +146,27 @@ func _ready() -> void: problems.append("当局数据 GP 失对(%s ≠ %s)" % [str(rd.get("gp")), str(r.gp)]) if int(rd.get("keystone", -1)) != main.run_scope.adventure.journal().keystone_gained(): problems.append("当局数据钥石失对(%s ≠ %s)" % [str(rd.get("keystone")), str(main.run_scope.adventure.journal().keystone_gained())]) - var close_btn: Button = ProbeUiUtil.find_button(main.ui.modals.debug_overlay, "关闭", "prefix") + var close_btn: Button = ProbeUiUtil.find_button(main.ui.modals.menu.debug_overlay, "关闭", "prefix") if close_btn == null: problems.append("浮窗关闭钮缺失(真实收窗路径中断)") else: close_btn.pressed.emit() - if main.ui.modals.debug_overlay.visible or main.ui.modals.flow.locks_input(): + if main.ui.modals.menu.debug_overlay.visible or main.ui.flow.locks_input(): problems.append("对局中关闭浮窗后态失守(visible=%s locked=%s)" - % [str(main.ui.modals.debug_overlay.visible), str(main.ui.modals.flow.locks_input())]) + % [str(main.ui.modals.menu.debug_overlay.visible), str(main.ui.flow.locks_input())]) # ⑨b 只读巡检(17卡③形制)+零动效契约(17卡③ R5——零 AnimationPlayer+源码零 tween) _overlay_readonly_contract(main, problems) _overlay_zero_motion_contract(main, problems) # ⑩ 种子三分单点断言(18卡⑦ C1——判定只落 _seed_value() 单点;对局态不影响本判定面): # 显式合法整数全场景优先(含首局)→ 首局(存档布尔 false)恒 42 → 非首局空框引擎熵连抽不全等 42 - main.ui.modals.debug_overlay.seed_edit.text = "777" + main.ui.modals.menu.debug_overlay.seed_edit.text = "777" if main._seed_value() != 777: problems.append("显式种子输入未优先(_seed_value != 777——C1 ①失守)") # SaveManager 是 autoload 单例——换注走原地 attach(首局态挂载+恢复隔离档); # --script 模式无裸 autoload 标识符——经 main 节点路径解析 var sm = main.get_node("/root/SaveManager") sm.slots.attach_path("user://smoke_tune_probe_seed.json") # 全新空档——first_adventure_done=false(首局态;新档缺省面) - main.ui.modals.debug_overlay.seed_edit.text = "" + main.ui.modals.menu.debug_overlay.seed_edit.text = "" if main._seed_value() != 42: problems.append("首局空框种子 != 42(P5 甲失守——教学态可复现破约)") sm.slots.attach_path("user://smoke_tune_probe_save.json") # 恢复隔离档(非首局态——isolate_save 已建档置位) @@ -181,7 +181,7 @@ func _ready() -> void: all42 = false if all42: problems.append("非首局空框连抽 4 次全等 42(随机化失守——C1 ③)") - main.ui.modals.debug_overlay.seed_edit.text = "42" # 钉回显式值(现场整洁) + main.ui.modals.menu.debug_overlay.seed_edit.text = "42" # 钉回显式值(现场整洁) for f0 in ["user://smoke_tune_probe_seed.json", "user://smoke_tune_probe_seed.json.tmp"]: if FileAccess.file_exists(f0): DirAccess.remove_absolute(f0) @@ -196,11 +196,11 @@ func _ready() -> void: # 只读巡检(18卡⑦ T3——17卡③「白名单外零连接」形制沿用):浮窗子树全控件 × 游戏语义信号名清单 -# 全连接巡检——白名单={视图自身八方法 + 注册面模态编排口 ui.modals.flow.close(closed 外发消费—— +# 全连接巡检——白名单={视图自身八方法 + 注册面模态编排口 ui.flow.close(closed 外发消费—— # 注册面批把该连接目标从组合根转发壳改为 flow 真方法,机检白名单随批登记同一连接)}; # 当局数据区块=纯标签零连接在此被一并约束(新增任何连接=红须入册审查)。 func _overlay_readonly_contract(main: Node, problems: Array) -> void: - var overlay: Control = main.ui.modals.debug_overlay + var overlay: Control = main.ui.modals.menu.debug_overlay var stack: Array = [overlay] var conn_count := 0 while not stack.is_empty(): @@ -211,7 +211,7 @@ func _overlay_readonly_contract(main: Node, problems: Array) -> void: var cb: Callable = conn["callable"] var host: Object = cb.get_object() var m := String(cb.get_method()) - var flow_host: Object = main.ui.modals.flow # 注册面模态编排口(closed 外发消费白名单宿主) + var flow_host: Object = main.ui.flow # 注册面模态编排口(closed 外发消费白名单宿主) var ok := (host == overlay and m in _OVERLAY_VIEW_METHODS) \ or (host == flow_host and m == "close") \ or (host is VScrollBar or host is HScrollBar) # 引擎滚动机构内部 drag 接线(RichTextLabel 自动滚动条——17卡③布局生命线同类豁免,零游戏写通路) @@ -226,7 +226,7 @@ func _overlay_readonly_contract(main: Node, problems: Array) -> void: # 零动效契约(17卡③ R5 形制——子树零 AnimationPlayer + 视图源码零 tween/动效包装调用) func _overlay_zero_motion_contract(main: Node, problems: Array) -> void: - var overlay: Control = main.ui.modals.debug_overlay + var overlay: Control = main.ui.modals.menu.debug_overlay var stack: Array = [overlay] while not stack.is_empty(): var n: Node = stack.pop_back() diff --git a/game/tests/smoke_boot.gd b/game/tests/smoke_boot.gd index 172017f8..25f1960a 100644 --- a/game/tests/smoke_boot.gd +++ b/game/tests/smoke_boot.gd @@ -71,5 +71,5 @@ func _check_main_scene(main: Node) -> void: check(start_overlay != null and start_overlay.visible, "启动态菜单可见(boot 后玩家见菜单)") check(main.get("settings_store") != null, "系统设置存储已装配(settings_store)") check(main.get("ui").screens.debug_badge != null, "调试入口角标已装配(debug_badge)") - check(main.get("ui").modals.legend_overlay != null, "图例覆盖层已装配(legend_overlay)") - check(main.get("ui").modals.status_panel != null, "局内状态面板已装配(status_panel)") + check(main.get("ui").modals.run.legend_overlay != null, "图例覆盖层已装配(legend_overlay)") + check(main.get("ui").modals.run.status_panel != null, "局内状态面板已装配(status_panel)") diff --git a/game/tests/system_test.gd b/game/tests/system_test.gd index 439c7455..6adb2c54 100644 --- a/game/tests/system_test.gd +++ b/game/tests/system_test.gd @@ -105,7 +105,7 @@ func _smoke_scene_split() -> void: chain_prev = node check(chain_ok, "V1c 合同树序链逐对保序(运行期树序==LAYER_CONTRACT 序;违例:%s)〔装配生命批 V1〕〔句柄子容器批 V2〕" % chain_detail) # ③ 探针挂载假设句柄在座(main.X 一级路径——探针迁移面收口证明) - check(main.ui.screens.start_overlay == main.ui.screens.menu_screen.start_overlay and main.ui.modals.camp.scene == main.ui.screens.camp_screen.camp_scene and main.ui.modals.level_screen == main.ui.screens.adventure_screen.level_screen and main.ui.screens.battle_root == main.ui.screens.adventure_screen.battle_root and main.ui.battle.board_zone.board_view == main.ui.screens.adventure_screen.board_view, + check(main.ui.screens.start_overlay == main.ui.screens.menu_screen.start_overlay and main.ui.modals.camp.scene == main.ui.screens.camp_screen.camp_scene and main.ui.modals.run.level_screen == main.ui.screens.adventure_screen.level_screen and main.ui.screens.battle_root == main.ui.screens.adventure_screen.battle_root and main.ui.battle.board_zone.board_view == main.ui.screens.adventure_screen.board_view, "V2 组合根句柄指向屏壳成员(探针挂载假设 main.X 零迁移)〔句柄子容器批 V1〕") check(main.run_scope != null and not main.run_scope.is_adventure, "V2 闲时会话自 _ready 即在(永非 null;迭代 27 T3——gp 影子账退役无账本成员)") @@ -117,9 +117,9 @@ func _smoke_scene_split() -> void: # camp 壳 boot 态 vis=true 但成员恒隐,裸树遍历自顶向下会先命中透明壳) func _topmost_visible_layer(main: Node) -> Node: var candidates: Array[Node] = [main.ui.screens.start_overlay, main.ui.screens.battle_root, main.ui.screens.inter_overlay, - main.ui.modals.legend_overlay, main.ui.modals.node_overlay, main.ui.modals.level_screen, main.ui.modals.curio_overlay, - main.ui.modals.end_overlay, main.ui.modals.shop_overlay, main.ui.modals.camp.scene, main.ui.modals.settings_overlay, - main.ui.modals.status_panel, main.ui.modals.career_overlay, main.ui.modals.confirm_dialog, main.ui.modals.debug_overlay] + main.ui.modals.run.legend_overlay, main.ui.modals.run.node_overlay, main.ui.modals.run.level_screen, main.ui.modals.run.curio_overlay, + main.ui.modals.run.end_overlay, main.ui.modals.menu.shop_overlay, main.ui.modals.camp.scene, main.ui.modals.menu.settings_overlay, + main.ui.modals.run.status_panel, main.ui.modals.menu.career_overlay, main.ui.modals.menu.confirm_dialog, main.ui.modals.menu.debug_overlay] var best: Node = null var best_x := -1 var best_y := -1 @@ -199,7 +199,7 @@ func _smoke_boot_chain() -> void: "V2e run_scope 非空且为闲时会话(is_adventure=false+is_inside_tree)〔装配生命批 V2〕") # ⑥ 调试浮窗 boot 不开+菜单角标在(菜单优先——boot 不自动开浮窗, # 入口=开始屏右下角「调试」角标+对局中两屏角标) - check(not main.ui.modals.debug_overlay.visible, + check(not main.ui.modals.menu.debug_overlay.visible, "V2f 调试浮窗 boot 不自动开(菜单优先)〔2026-10-16 用户令〕") check(main.ui.screens.debug_badge != null and not main.ui.screens.debug_badge.visible, "V2f 调试浮层角标发布态恒隐(2026-10-25 删写死行批)") @@ -223,7 +223,7 @@ func _smoke_run_lifecycle() -> void: sm.slots.attach_path("user://smoke_run_lifecycle.json") sm.meta_state.mark_first_adventure_done() # ---- 第一局:开局→注脏→放弃→终局出口→会话收束 ---- - main.ui.modals.debug_overlay.seed_edit.text = "42" + main.ui.modals.menu.debug_overlay.seed_edit.text = "42" main._start_adventure_from_menu() await process_frame check(main.run_scope != null and main.run_scope.is_adventure and main.run_scope.adventure != null, @@ -243,7 +243,7 @@ func _smoke_run_lifecycle() -> void: break await process_frame check(finished.done, "V3 冒险终局到达(abandon→adventure_finished 同步链)") - check(main.ui.modals.end_overlay.is_open(), "V3 终局面板弹起") + check(main.ui.modals.run.end_overlay.is_open(), "V3 终局面板弹起") main._on_battle_end_continue() # 终局出口(回菜单+会话收束——手工置空清单已消亡) await process_frame check(not main.run_scope.is_adventure and main.run_scope.adventure == null and main.run_scope.runner == null, @@ -253,7 +253,7 @@ func _smoke_run_lifecycle() -> void: check(main.ui.screens.start_overlay.visible, "V3 回开始菜单(终局出口路径)") # ---- 第二局:放弃终局无断点残留→直进开局→全新会话 ---- check(not sm.has_breakpoint(), "V3 放弃终局清断点(无残留断点)") - main.ui.modals.debug_overlay.seed_edit.text = "43" + main.ui.modals.menu.debug_overlay.seed_edit.text = "43" main._start_adventure_from_menu() await process_frame check(main.run_scope.is_adventure and main.run_scope.adventure != null and main.run_scope.hints_shown.is_empty(), @@ -288,7 +288,7 @@ func _smoke_sim_guards() -> void: "模拟运行态下调试两层入口守卫早退(开始层不收起)〔wd6〕") main._tune_sim._running = false # wd7:真实冒险关间态(is_adventure=true、settlement==null)→ 模拟入口守卫早退 - main.ui.modals.debug_overlay.seed_edit.text = "42" + main.ui.modals.menu.debug_overlay.seed_edit.text = "42" main._start_adventure_from_menu() await process_frame check(main.run_scope.is_adventure and main.run_scope.settlement == null, @@ -313,7 +313,7 @@ func _face_snapshot(main: Node) -> Dictionary: var tu = main._treasure_ui var bo = main._bonus var cp = main._camp_pres - var mf = main.ui.modals.flow + var mf = main.ui.flow var rp = main.ui.screens.row_pacer return { "treasure_settlement": tu._settlement_get.call(), @@ -381,10 +381,10 @@ func _stage_faces(main: Node, tag: String, adv: String, run: String, set: String if bool(snap["mf_rows_empty"]) != bool(extra[k]): bad += " modal_flow.rows_empty_get 应=%s" % str(extra[k]) "level_vis": - if main.ui.modals.level_screen.visible != bool(extra[k]): + if main.ui.modals.run.level_screen.visible != bool(extra[k]): bad += " level_screen.visible 应=%s" % str(extra[k]) "end_open": - if main.ui.modals.end_overlay.is_open() != bool(extra[k]): + if main.ui.modals.run.end_overlay.is_open() != bool(extra[k]): bad += " _end_overlay.is_open 应=%s" % str(extra[k]) "start_vis": if main.ui.screens.start_overlay.visible != bool(extra[k]): @@ -419,7 +419,7 @@ func _smoke_lifecycle_matrix() -> void: if FileAccess.file_exists(p): DirAccess.remove_absolute(p) sm.slots.attach_path("user://smoke_lifecycle.json") - main.ui.modals.debug_overlay.seed_edit.text = "42" + main.ui.modals.menu.debug_overlay.seed_edit.text = "42" # 阶段①闲时 await _stage_faces(main, "V3a 闲时", "null", "null", "null", {"mf_start": true, "mf_inter": false, "mf_rows_empty": true, "start_vis": true}) @@ -430,7 +430,7 @@ func _smoke_lifecycle_matrix() -> void: await _stage_faces(main, "V3b 开局停点", "alive", "alive", "null", {"mf_start": false, "level_vis": true}) # 阶段③地图选路→战斗中(真实选路提交 press_candidate) - main.ui.modals.level_screen.press_candidate(0) + main.ui.modals.run.level_screen.press_candidate(0) for i in range(10): await process_frame await _stage_faces(main, "V3c 战斗中", "alive", "alive", "alive", {"mf_start": false}) @@ -467,7 +467,7 @@ func _smoke_lifecycle_matrix() -> void: await process_frame main.ui.screens.row_pacer.flush() for i in range(12): - if main.ui.modals.end_overlay.is_open(): + if main.ui.modals.run.end_overlay.is_open(): break await process_frame # 分支甲:战斗胜利停点(结算后)→确认进选路→放弃→终局面 @@ -484,11 +484,11 @@ func _smoke_lifecycle_matrix() -> void: await process_frame main.ui.screens.row_pacer.flush() for i in range(8): - if main.ui.modals.end_overlay.is_open(): + if main.ui.modals.run.end_overlay.is_open(): break await process_frame # 阶段⑤终局面(败北/放弃皆达) - check(finished.done and main.ui.modals.end_overlay.is_open(), + check(finished.done and main.ui.modals.run.end_overlay.is_open(), "V3e 终局面到达(adventure_finished+终局面板开)〔装配生命批 V3〕") await _stage_faces(main, "V3e 终局面", "alive", "alive", "null", {"mf_start": false, "end_open": true}) # 阶段⑥回菜单收束(旧局消亡+新会话即驻+无真空窗) @@ -515,7 +515,7 @@ func _smoke_lifecycle_matrix() -> void: main._open_save_select() for i in range(2): await process_frame - var ssel_new: Button = main.ui.modals.save_select_overlay._new_btns.get(1) + var ssel_new: Button = main.ui.modals.menu.save_select_overlay._new_btns.get(1) if ssel_new == null: check(false, "V3g 营地入口缺失(选档层新档钮驱动中断)〔装配生命批 V3〕") continue @@ -551,7 +551,7 @@ func _smoke_lifecycle_matrix() -> void: DirAccess.remove_absolute(slot_p) # 阶段⑧两连局(第二局全新会话——旧局对象不复用、脏态零残留) var first_adv = main.run_scope.adventure - main.ui.modals.debug_overlay.seed_edit.text = "43" + main.ui.modals.menu.debug_overlay.seed_edit.text = "43" main._start_adventure_from_menu() for i in range(6): await process_frame @@ -560,12 +560,12 @@ func _smoke_lifecycle_matrix() -> void: await _stage_faces(main, "V3h 两连局·开局停点", "alive", "alive", "null", {"mf_start": false, "level_vis": true}) check(main.run_scope.adventure.abandon(), "V3h 放弃受理(第二局)〔装配生命批 V3〕") for i in range(12): - if main.ui.modals.end_overlay.is_open(): + if main.ui.modals.run.end_overlay.is_open(): break await process_frame main.ui.screens.row_pacer.flush() for i in range(8): - if main.ui.modals.end_overlay.is_open(): + if main.ui.modals.run.end_overlay.is_open(): break await process_frame await _stage_faces(main, "V3h 两连局·终局面", "alive", "alive", "null", {"mf_start": false, "end_open": true}) diff --git a/game/tests/unit/presentation/pacing_queue_test.gd b/game/tests/unit/presentation/pacing_queue_test.gd index 2da70b5f..fdf2a6bc 100644 --- a/game/tests/unit/presentation/pacing_queue_test.gd +++ b/game/tests/unit/presentation/pacing_queue_test.gd @@ -58,7 +58,7 @@ func _init() -> void: main._start_adventure_from_menu() await process_frame # 入口停点选路进首战(真实停点链) - if not main.ui.modals.level_screen.visible: + if not main.ui.modals.run.level_screen.visible: _problems.append("入口停点未呈现(前置失守)") _finish() return diff --git a/game/tests/unit/presentation/theme_projection_test.gd b/game/tests/unit/presentation/theme_projection_test.gd index 03d3812a..71dacd2b 100644 --- a/game/tests/unit/presentation/theme_projection_test.gd +++ b/game/tests/unit/presentation/theme_projection_test.gd @@ -716,7 +716,7 @@ func _smoke_diag_button_retired() -> void: Kit.check("V6 主区无 build_diag_button 调用残留(旧实现同批清理)", not main_src.contains("build_diag_button")) Kit.check("V6 转储入口连接随迁(组合根经注册面接浮窗 diagnose_requested 信号)〔注册面 V2〕", - main_src.contains("ui.modals.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed)")) + main_src.contains("ui.modals.menu.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed)")) Kit.check("V6 转储编排承接函数在位(dump_diagnosis 卡死取证保留)", main_src.contains("func _on_diagnose_pressed()")) var asm_src: String = FileAccess.get_file_as_string("res://presentation/ui_assembler.gd") From 041d509b4d8a17d9b011d660ae75295405177b49 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 2 Nov 2026 07:30:34 +0800 Subject: [PATCH 3/4] =?UTF-8?q?iter49-=E5=8D=A13=EF=BC=9A=E8=90=A5?= =?UTF-8?q?=E5=9C=B0=E4=BA=94=E7=95=8C=E9=9D=A2=E7=9C=9F=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E5=8C=96=E6=89=B9(=E7=AA=97=E5=A3=B3=E5=BD=A2=E6=80=81?= =?UTF-8?q?=E6=8D=A2=E8=A3=85):WINDOW=5FSIZE=20720=C3=97640=E5=8D=95?= =?UTF-8?q?=E7=82=B9+modal=5Fshell=E7=AA=97=E5=A3=B3=E9=AA=A8=E6=9E=B6+car?= =?UTF-8?q?eer=E5=90=8C=E5=A3=B3+screen=5Fflow=5Fprobe=20V1-V8=E7=AA=97?= =?UTF-8?q?=E5=A3=B3=E5=BD=A2=E6=80=81=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/engineering/test-conventions.md | 2 +- design/gdd/systems/跨局循环/锻造工坊.md | 16 +- game/core/battle_fx_host.gd | 4 +- game/presentation/main.gd | 10 - game/presentation/views/camp_sub_view_base.gd | 32 ++- game/presentation/views/career_overlay.gd | 60 ++++-- game/presentation/views/ui_kit.gd | 2 +- game/probe/screen_flow_probe.gd | 204 ++++++++++++++++++ .../golden/palette_baseline/art_snapshot.png | Bin 0 -> 2572 bytes .../golden/palette_baseline/ui_tokens.png | Bin 0 -> 337 bytes reports/工程改动报告-工程清理批-2026-11-02.md | 75 +++++++ tools/capability_audit.py | 1 - tools/extract_checks.py | 43 ++-- tools/gen_palette_baseline.py | 105 +++++++++ tools/palette_check.py | 13 +- tools/path_check.py | 2 +- tools/render_sample_sheet_v3.py | 12 ++ tools/run_all_gates.sh | 8 + 待办清单.md | 67 +----- 19 files changed, 524 insertions(+), 132 deletions(-) create mode 100644 game/tests/golden/palette_baseline/art_snapshot.png create mode 100644 game/tests/golden/palette_baseline/ui_tokens.png create mode 100644 reports/工程改动报告-工程清理批-2026-11-02.md create mode 100644 tools/gen_palette_baseline.py diff --git a/design/engineering/test-conventions.md b/design/engineering/test-conventions.md index 09c7c39e..039058c1 100644 --- a/design/engineering/test-conventions.md +++ b/design/engineering/test-conventions.md @@ -35,7 +35,7 @@ 需求卡片 §8「改动—验收对照表」每一条都要落成可执行的验证;本文定统一写法,卡里定每批的具体内容(测什么/注入哪里)。写卡期查不出执行载体的条目=不可测,当场呈报,禁拖到实现期。 1. **执行载体五选一,逐条声明**:单元测试(`tests/run_all`)/系统测试断言(`tests/system_test.gd`)/场景探针(`probe/` 下 `*_probe`)/MC 同种子逐项比对/人工核对(仅限无法自动化的纪律类需求点,如命名锁定、数值占位)。行为类条目以自动化载体为主。 -2. **粒度与对应关系**:每条清单条目至少一个自动断言,且断言必须从该条目的真实入口进入;多条条目可合用一个测试函数,但每条都要有自己单独可判红的判据(取证口径见 §8.4 唯一口径——禁变异/禁撤销)。断言失败消息末尾带编号尾缀〔卡短名 Vn〕(如〔秘宝批 V3〕)——断言与卡片清单可互相 grep 查找。**工具面局限备案(2026-10-27 迭代 39 验收审计)**:`Kit.check_quiet` 静默收集形态(计数不入判定即打印、失败消息进 problems 汇总)的断言消息不进 `tools/extract_checks.py` 扫描面——验收尾缀 grep 对该通道失效;如需覆盖属提取器改版(工程批)。 +2. **粒度与对应关系**:每条清单条目至少一个自动断言,且断言必须从该条目的真实入口进入;多条条目可合用一个测试函数,但每条都要有自己单独可判红的判据(取证口径见 §8.4 唯一口径——禁变异/禁撤销)。断言失败消息末尾带编号尾缀〔卡短名 Vn〕(如〔秘宝批 V3〕)——断言与卡片清单可互相 grep 查找。`Kit.check_quiet` 静默收集形态(计数不入判定即打印、失败消息进 problems 汇总)的断言已入 `tools/extract_checks.py` 扫描面(2026-11-02 提取器改版:三通道同口径提取,静默通道条目 quiet 列记 `channel:check_quiet`),验收尾缀 grep 对该通道有效。 3. **断言词汇按验证对象五选一**: | 词汇 | 适用场景 | diff --git a/design/gdd/systems/跨局循环/锻造工坊.md b/design/gdd/systems/跨局循环/锻造工坊.md index 679e80ab..8319cdb8 100644 --- a/design/gdd/systems/跨局循环/锻造工坊.md +++ b/design/gdd/systems/跨局循环/锻造工坊.md @@ -1,7 +1,7 @@ # 锻造工坊 > 一句话:营地的武器与护甲成长——武器锻造区三条路线、护甲锻造区三条路线各练各的,钥石消费,一次冒险各选一件,满级特效留位本期不做 -> 状态:**v0.10(2026-10-29 实现内容清理:删「现役代码事实」「实装边界」两节与正文代码指证,规则与数值零变化)**——全部数值 provisional,终值归 Alpha 收官 M4 数值批〔未排期〕 +> 状态:**v0.11(2026-11-02 补验收标准节:九条结构面判据落文,规则与数值零变化)**——全部数值 provisional,终值归 Alpha 收官 M4 数值批〔未排期〕 ## 设计目的 用户原案(playnote 2026-10-06):把现役"营地花钱买攻击 +1"改造成有等级、有成长、有满级特效的**武器升级路线**;新增长枪(穿刺)与法杖(魔法)实现武器间差异化;护甲同理——初始只有轻甲(升级只提 AC),最高等级后解锁重甲/魔法护甲,基础 AC 各异、各带满级特效。 @@ -209,3 +209,17 @@ ### 10 · 数值与验证 全部数值 provisional 归 Alpha 收官 M4 数值批;实装批必须带改前 / 改后同随机种子配对读数验证难度影响。 + +## 验收标准 + +本节判据全部绑结构与关系、不绑具体数值:价格、增量、等级上限等行值 provisional 归 M4 数值批,改值不推翻判据;判据失效只发生在结构关系本身被改时。 + +- **柜台解锁与默认值解耦**:天赋树的锻造工坊钥匙节点(见[天赋树](天赋树.md))点亮后本柜台开放、可购买;未点亮时柜台锁定、不存在任何购买途径;未点亮期间武器/甲默认值照常经参数面注入冒险,不存在「未解锁就读不到默认值」的窗口(2026-10-11 树侧钥匙裁)。 +- **路线解锁双通道并联**:长枪、法杖、重甲、魔法护甲四条路线的解锁条件全部在本系统内部、与关卡进度零绑定;每条路线同时开两条互不影响的通道——练满前一档、或支付一次性解锁价直购——任一通道成立即解锁(4b/4b-1 裁)。 +- **窗口内容**:营地四柜台并排呈现;本柜台分武器、护甲两区,各列三条路线行,行名=件名+锻造;出场行显示当前选中(武器+甲);满级特效无窗口入口、无文案(本期不做)。 +- **单选注入与树零攻防**:攻击加值唯一来源=当前选中武器路线、AC 加值唯一来源=当前选中甲路线;0 级=未练合法态、零加值;天赋树淬炼对攻击与 AC 零贡献;开局注入按「账号层基础值+树淬炼 flat+锻造加值+局内加算区」分层可加,锻造加值只取当前选中(2026-10-11 所有权切分裁)。 +- **切换语义**:营地内任意武器×甲组合切换免费、即时生效;换装不丢级不生效——旧选中等级保留、加值待命;一次冒险=一个武器类型+一个甲型;冒险开始后锁死、局内不存在任何换装途径(1a/1b/1c 裁)。 +- **定价结构**:每级价格随已购级数几何递增(第 N+1 级价格 > 第 N 级价格,基价与公比读表单点);路线解锁价一次性收取;钥石不足时购买被拒、余额与等级零变化;GP 全程不可用于本柜台(5b 裁)。 +- **存档闭合**:六条路线等级+当前武器/甲选中标记写入存档,读档恢复与写入前一致;存档域无特效域(6 裁连带)。 +- **祝福件负断言**:局内商店购买攻击/护甲祝福后,本柜台路线等级与选中态零变化(8a 裁连带——货架行名带「攻击」「护甲」字样,属性祝福与锻造升级是两条互不相通的通道)。 +- **甲型双轴可感**:切换甲型后基础 AC 与挨普通攻击的克制修正(轻甲 0/重甲 −1/魔法护甲 +1)双轴同时变化(3b 裁;现行敌方攻击全部按普通处理,断言条件当下即成立)。 diff --git a/game/core/battle_fx_host.gd b/game/core/battle_fx_host.gd index 10df992d..22eaa6b9 100644 --- a/game/core/battle_fx_host.gd +++ b/game/core/battle_fx_host.gd @@ -28,7 +28,7 @@ var damage: DamageController var emit_cb: Callable # 宿主 _emit_row——行协议面单点(5 参 head 束形:head/ri/display/value/extra) var facts_cb: Callable # 宿主 _fx_host_fact——round_index/battle_index/seals_count/nest_count/hp_ratio 五事实宿主侧单点 var grant_cb: Callable = Callable() # 揭示兑付回调=RewardGrant 授予面(bind_grant 注入;构造序解法——本件对授予面零强引用) -var engine: EffectEngine # 效果引擎(原编排器 _fx——宿主 _init 经此转注 damage/action_economy 两 set_fx;reward_economy 经 luck_bias_for 查询口取回偏运合计) +var engine: EffectEngine # 效果引擎(原编排器 _fx——宿主 _init 经此建效果端口件转注两业务类 bind_port;reward_economy 经 luck_bias_for 查询口取回偏运合计) var counters_ctx: EffectContext # 四桶引用束(round/battle 两桶本件自有;adventure/level 桶 begin_battle mount_buckets 挂引用) var ac := {"base": -1, "round": 0, "battle": 0} # ac 束(base=主角 AC 基线快照 enc.hero_armor_class,-1=表值兜底未解析;round=ac_bonus 本轮累积器〔封印桩,round_start 清零〕;battle=本战累积器〔石肤,cap 封顶,begin_battle 清零〕) var _fx_bonus_delta := {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} # 本战 bonus_delta 累积(战末宿主 bonus_delta_drain 读后清零) @@ -144,7 +144,7 @@ func apply_ac_bonus(h: Dictionary) -> void: # 揭示效果入队(fx_reveal 掷格兑付延后到授予段末——防轮 facts 迭代中追加重入;见血开窍/ -# 斩首学者〔经伤害控制器 fx_calls["reveal"] 回调〕/地质学家/跳格采掘共用) +# 斩首学者〔经伤害控制器端口 reveal_effect 路由〕/地质学家/跳格采掘共用) func enqueue_reveal() -> void: reveal.queue.append({"round": int(facts_cb.call("round_index"))}) diff --git a/game/presentation/main.gd b/game/presentation/main.gd index fa3d5bd2..41c9713c 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -14,21 +14,11 @@ const AUTOLOAD_PATH_RNG := "/root/RngService" const AUTOLOAD_PATH_SAVE := "/root/SaveManager" const AUTOLOAD_PATH_META := "/root/MetaProgression" -const UiAssembler := preload("res://presentation/ui_assembler.gd") const Narrator := preload("res://presentation/battle_narrator.gd") -const TuneSimController := preload("res://presentation/tune_sim_controller.gd") -const DebugSessionScript := preload("res://presentation/debug_session.gd") -const TreasureInteractController := preload("res://presentation/treasure_interact_controller.gd") # 秘宝两步交互 -const CurioInteractController := preload("res://presentation/curio_interact_controller.gd") # 三选一与替换子交互 -const CampShopPresenter := preload("res://presentation/camp_shop_presenter.gd") # 营地/商店投影 -const BoardResolversScript := preload("res://presentation/board_resolvers.gd") # 棋盘解析回调族 const MainUiBuild := preload("res://presentation/main_ui_build.gd") # 装配分步承接件 const MainAdventureFlow := preload("res://presentation/main_adventure_flow.gd") # 冒险流程承接件 const MainOverlayFlow := preload("res://presentation/main_overlay_flow.gd") # 覆盖层流程承接件 const MainDebugFlow := preload("res://presentation/main_debug_flow.gd") # 调试流程承接件 -const MenuScreenScene := preload("res://presentation/scenes/menu_screen.tscn") # 三屏壳场景 -const CampScreenScene := preload("res://presentation/scenes/camp_screen.tscn") -const AdventureScreenScene := preload("res://presentation/scenes/adventure_screen.tscn") var db var run_scope = null # 会话状态承载(run_scope.gd;_ready 即建闲时态,永非 null) diff --git a/game/presentation/views/camp_sub_view_base.gd b/game/presentation/views/camp_sub_view_base.gd index 20f2d152..4ec5b807 100644 --- a/game/presentation/views/camp_sub_view_base.gd +++ b/game/presentation/views/camp_sub_view_base.gd @@ -1,5 +1,6 @@ -## 营地子界面模态壳基类(锻造工坊/秘宝匣/天赋树/奖励格图鉴四子界面公共形态): -## 实底全屏暖亮档+石纹暗角+顶栏(铭牌+钥石徽)+内容列+底栏返回钮。 +## 营地子界面功能窗口基类(锻造工坊/秘宝匣/天赋树/奖励格图鉴四子界面公共形态): +## 居中定尺寸窗壳(石板皮窗板+半透明压暗遮罩)+顶栏(铭牌+钥石徽)+内容列+底栏返回钮。 +## 窗几何单点=WINDOW_SIZE(camp.md v2.5 起点 720×640);窗内滚动=内容体超高由子类滚动容器承载(窗不随内容涨)。 ## 开合可见性归模态栈(登记层开合一律走栈);返回钮只发 closed——关层编排归 presenter(组合根单口)。 ## 余额徽随子类 refresh_camp 刷新(钥石读数=投影单口,禁第二账本)。 class_name CampSubViewBase @@ -7,24 +8,19 @@ extends Control signal closed +const WINDOW_SIZE := Vector2(720, 640) # 功能窗口起点定值单点(camp.md v2.5——生涯页经本常量引用,禁第二处字面值) + var _balance_label: Label # 钥石徽数值行(探针直读面——ui_probe 余额文案断言改指新界面) -## 壳构建(子类 _init 调用一次):content 由子类先建好传入(列容器/滚动容器皆可) +## 壳构建(子类 _init 调用一次):content 由子类先建好传入(列容器/滚动容器皆可)。 +## 窗壳骨架=UiKit.modal_shell(dim 全屏压暗+CenterContainer 居中+石板皮窗板;min_size 参数保持 ZERO +## 使窗板尺寸经 custom_minimum_size 精确可钉 720×640——panel_skinned 边距被 PANEL_SLICE 钳到 16 为既有工厂行为) func _build_shell(title: String, icon_path: String, content: Control) -> void: - # 半透明压暗遮罩 0.6(camp.md v2.3 定稿——场景根留底可见,窗口之下垫遮罩;交互聚焦由遮罩 STOP 兜底) - var dim := UiKit._modal_dim(self, Color(UiKit.DIM, 0.6)) - var margin := MarginContainer.new() - margin.set_anchors_preset(Control.PRESET_FULL_RECT) - margin.add_theme_constant_override("margin_left", 16) - margin.add_theme_constant_override("margin_right", 16) - margin.add_theme_constant_override("margin_top", 12) - margin.add_theme_constant_override("margin_bottom", 12) - dim.add_child(margin) - var col := VBoxContainer.new() - col.add_theme_constant_override("separation", 8) - margin.add_child(col) - _build_top_bar(col, title, icon_path) + # 半透明压暗遮罩 0.6(camp.md v2.5——场景根留底可见,窗口之下垫遮罩;交互聚焦由遮罩 STOP 兜底) + var shell := UiKit.modal_shell(self, Color(UiKit.DIM, 0.6), Vector2.ZERO, 8, UiKit.panel_skinned(Vector4(14, 12, 14, 12))) + shell.panel.custom_minimum_size = WINDOW_SIZE + _build_top_bar(shell.box, title, icon_path) content.size_flags_vertical = Control.SIZE_EXPAND_FILL # 内容米板身(柜台行板对比底=PanelData——旧柜台面板形态延续,行板墨色对比表不脱锚) var body := PanelContainer.new() @@ -32,8 +28,8 @@ func _build_shell(title: String, icon_path: String, content: Control) -> void: UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 10, 10, 10))) body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_child(content) - col.add_child(body) - _build_bottom_bar(col) + shell.box.add_child(body) + _build_bottom_bar(shell.box) visible = false # 登记模态初始隐(开合走栈) diff --git a/game/presentation/views/career_overlay.gd b/game/presentation/views/career_overlay.gd index c5487a1c..529370e9 100644 --- a/game/presentation/views/career_overlay.gd +++ b/game/presentation/views/career_overlay.gd @@ -1,6 +1,6 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION -## 生涯页·只读全屏页:跨局终身计数汇总(不统计天赋树与账号成长态)。 -## 入口唯一在开始覆盖层「营地与系统」组、局内不放;页内零跳转零动效(直开直关)。 +## 生涯页·只读功能窗口:跨局终身计数汇总(不统计天赋树与账号成长态)。 +## 入口=营地场景图帐篷旁笔记本;窗内零跳转零动效(直开直关)。 ## 构成件全部经 UiKit 工厂产出——禁新素材/新色值/自建样式。 class_name CareerOverlay extends Control @@ -16,28 +16,37 @@ var _keystone_badge: UiKit.KeystoneBadge # 钥石余额(badge 工厂件— func _init() -> void: - # 全屏页壳(T6/T7 对齐批先例——暖亮底+石纹弱质感+暗角+56px 通栏顶条+内容列) - var page: Dictionary[Variant, Variant] = UiKit.page_shell(self, true) - page.base.color = Color(UiKit.DIM, 0.6) # 功能窗口化(camp.md v2.3 定稿——场景根留底+半透明压暗遮罩 0.6;观感随样张目检校准) - var top: HBoxContainer = page.top_row - top.add_child(UiKit.label("生 涯",UiKit.FS_TITLE, true, UiKit.WARM)) - var top_sp := Control.new() - top_sp.size_flags_horizontal = Control.SIZE_EXPAND_FILL - top.add_child(top_sp) - var close_btn := UiKit.button("返回", 90.0) # 紧凑返回钮(设置/状态面板先例形制) - close_btn.tooltip_text = "关闭生涯页,返回主菜单(本页只读,不发生任何购买或修改)" - close_btn.pressed.connect(_emit_closed) - top.add_child(close_btn) + # 功能窗口壳(camp.md v2.5 生涯页与四子界面同壳同形态——居中定尺寸窗板+半透明压暗遮罩 0.6; + # 窗几何单点=CampSubViewBase.WINDOW_SIZE,禁第二处字面值) + var shell := UiKit.modal_shell(self, Color(UiKit.DIM, 0.6), Vector2.ZERO, 8, UiKit.panel_skinned(Vector4(14, 12, 14, 12))) + shell.panel.custom_minimum_size = CampSubViewBase.WINDOW_SIZE + _build_top_row(shell) - var box: VBoxContainer = page.box + # 内容列滚动面(窗内滚动——窗尺寸不随内容增长;横向禁滚同四子界面形态) + var scroll := ScrollContainer.new() + scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL + scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + shell.box.add_child(scroll) + # 内容板(BG_BRIGHT——原页底色随壳迁移,内容板文字 token 零改——契约 9 口径) + var body := PanelContainer.new() + body.add_theme_stylebox_override("panel", + UiKit.panel_flat(UiKit.BG_BRIGHT, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(12, 12, 12, 12))) + body.size_flags_horizontal = Control.SIZE_EXPAND_FILL + scroll.add_child(body) + var box := VBoxContainer.new() + box.add_theme_constant_override("separation", 8) + body.add_child(box) var hint := UiKit.label("跨局生涯汇总(只读——购买与装配在营地;历史冒险回看随后续批次)", UiKit.FS_HINT, false, UiKit.WARM_SOFT) + hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART # 窗内换行——单行 min 宽会把定尺寸窗板撑宽(720 契约) + hint.size_flags_horizontal = Control.SIZE_EXPAND_FILL box.add_child(hint) # 组一·冒险战绩(终身计数域——存档 v6:总局数开局即计、冒险成功局数三关全通才计) - # 对比度口径:页底=暖亮档深棕(BG_BRIGHT),文字全用亮墨族 token(WARM 9.0:1 / WARM_SOFT 8.0:1 / + # 对比度口径:内容板底=暖亮档深棕(BG_BRIGHT),文字全用亮墨族 token(WARM 9.0:1 / WARM_SOFT 8.0:1 / # CANDLE 7.2:1 / INK_L_GOLD 9.1:1 实测对 BG_BRIGHT 全过 §7.2 ≥4.5:1——深墨族〔INK_LABEL〕 - # 压深棕底仅 ~1.3:1 属违例,禁用) + # 压深棕底仅 ~1.3:1 属违例,禁用;顶行标题墨压石板皮不在此列——INK_TITLE 深墨族先例) _section_head(box, "冒险战绩") _total_label = _row(box, "冒险总局数") _cleared_label = _row(box, "冒险成功局数") @@ -58,6 +67,21 @@ func _init() -> void: visible = false # 显式初始化——不靠树序巧合(缺陷⑤家族纪律) +# 顶行(「生 涯」标题+「返回」钮——文案与连接零改;标题墨随亮底石板皮惯例订正 INK_TITLE——设置/状态面板先例) +func _build_top_row(shell: Dictionary[Variant, Variant]) -> void: + var top := HBoxContainer.new() + top.add_theme_constant_override("separation", 10) + (shell.box as VBoxContainer).add_child(top) + top.add_child(UiKit.label("生 涯",UiKit.FS_TITLE, true, UiKit.INK_TITLE)) + var top_sp := Control.new() + top_sp.size_flags_horizontal = Control.SIZE_EXPAND_FILL + top.add_child(top_sp) + var close_btn := UiKit.button("返回", 90.0) # 紧凑返回钮(设置/状态面板先例形制) + close_btn.tooltip_text = "关闭生涯页,返回主菜单(本页只读,不发生任何购买或修改)" + close_btn.pressed.connect(_emit_closed) + top.add_child(close_btn) + + # ---- 开层(组合根唯一调用口——快照随开随建,只读同源口径同状态面板 R4)---- func open_career(data: Dictionary) -> void: @@ -91,5 +115,7 @@ func _row(box: VBoxContainer, name_text: String) -> Label: name_l.custom_minimum_size = Vector2(150, 0) line.add_child(name_l) var val_l := UiKit.label("—",UiKit.FS_BODY, true, UiKit.CANDLE) + val_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART # 长值窗内换行——定尺寸窗禁单行 min 宽撑宽(同 hint 口径) + val_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL line.add_child(val_l) return val_l diff --git a/game/presentation/views/ui_kit.gd b/game/presentation/views/ui_kit.gd index 7ce53dd1..3c8d24c6 100644 --- a/game/presentation/views/ui_kit.gd +++ b/game/presentation/views/ui_kit.gd @@ -230,7 +230,7 @@ static func motion(dur: float) -> float: # 的字形/无演出回退路径;禁 preload(素材缺位树上直接断言炸脚本,冒烟安全红线) const TEX_HERO_IDLE := "res://assets/art/character/character_hero_idle.png" const TEX_HERO_WALK := "res://assets/art/character/character_hero_walk.png" -const TEX_HERO_PORTRAIT := "res://assets/art/character/character_hero_portrait.png" # B 区站桩立绘·normal 态(48px 档) +const TEX_HERO_PORTRAIT := "res://assets/art/character/character_hero_portrait.png" # B 区站桩立绘·normal 态(素材=原稿分辨率,显示档缩放读 standee_motion PORTRAIT_DISPLAY_H) const TEX_HERO_ATTACK := "res://assets/art/character/character_hero_attack.png" # B 区站桩立绘·attack 态(三态切换) const TEX_HERO_HIT := "res://assets/art/character/character_hero_hit.png" # B 区站桩立绘·hit 态(三态切换) # 敌种立绘(B 区站桩)——文件键 = 数据表主键 enemy_id(圣经 §8.3 命名规范), diff --git a/game/probe/screen_flow_probe.gd b/game/probe/screen_flow_probe.gd index ef87e290..8c538e35 100644 --- a/game/probe/screen_flow_probe.gd +++ b/game/probe/screen_flow_probe.gd @@ -28,6 +28,7 @@ func _ready() -> void: await _step_menu_to_camp() await _step_camp_back_to_menu() await _step_camp_esc_and_hud() # 卡⑤——Esc 四态路由/三出口/设置语境分支/HUD 读数 + await _step_camp_window_shell() # 真窗口化批——五窗统一窗壳形态 V1–V8(插 Esc 段之后、选档段之前) await _step_save_select_open_back() # 迭代 33 卡③ sl10——选档开合+返回路径+ESC 语境不响应 await _step_menu_to_adventure() await _step_level_to_battle_and_back() @@ -295,6 +296,209 @@ func _step_camp_esc_and_hud() -> void: _problems.append("[esc] 非营地语境 Esc 唤起浮窗(契约 9④ 失守)〔ESC批 V1〕") +# ---- ③'' 营地五窗统一窗壳(真窗口化批 V1–V8——真实进档入口+真实信号路由+真实钮/键事件)---- +# 判的是呈现形态换壳(近全屏面板→居中定尺寸窗壳 720×640):窗几何/场景根留底/遮罩 0.6/关闭钮/ +# 互斥单开/ESC 优先序/窗内滚动/生涯同壳——行为面(开合路由)零改,只钉窗壳可观测提交面。 +func _step_camp_window_shell() -> void: + ProbeUiUtil.isolate_save(main, "smoke_screen_flow_winshell", false, false, true) + if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_winshell"): + _problems.append("[winshell] 营地入口缺失(选档层新档钮驱动中断)〔窗口化批 V1〕") + return + await _frames() + var views := { + &"forge": main.ui.modals.camp.forge, + &"chest": main.ui.modals.camp.treasure, + &"dummy": main.ui.modals.camp.tree, + &"board": main.ui.modals.camp.codex, + } + var career: Control = main.ui.modals.menu.career_overlay + var five: Array[Control] = [views[&"forge"], views[&"chest"], views[&"dummy"], views[&"board"], career] + var vp: Vector2 = main.get_viewport().get_visible_rect().size + var opened: Array[StringName] = [] + for k: StringName in [&"forge", &"chest", &"dummy", &"board"]: + # 每窗先经真实关闭钮关前窗(V4 四窗半边——轮转间钉关闭链) + if not opened.is_empty(): + var prev: Control = views[opened.back()] + var prev_back: Button = ProbeUiUtil.find_button(prev, "返回营地") + if prev_back == null: + _problems.append("[winshell] %s 底栏「返回营地」钮缺失〔窗口化批 V4〕" % String(opened.back())) + else: + prev_back.pressed.emit() + await _frames() + if prev.visible or not main.ui.modals.camp.scene.visible: + _problems.append("[winshell] %s 关闭钮按下未隐窗或场景根失守〔窗口化批 V4〕" % String(opened.back())) + main.ui.modals.camp.scene.object_pressed.emit(k) + await _frames() + opened.append(k) + var view: Control = views[k] + var panel: PanelContainer = _window_panel(view) + if panel == null: + _problems.append("[winshell] %s 窗板缺失(CenterContainer 下无 PanelContainer)〔窗口化批 V1〕" % String(k)) + continue + var rect: Rect2 = panel.get_global_rect() + # V1 四窗几何:位置 ≈ (280, 80)、尺寸 ≈ (720, 640)(浮点容差 0.01——禁裸 ==) + if absf(rect.position.x - 280.0) > 0.01 or absf(rect.position.y - 80.0) > 0.01 \ + or absf(rect.size.x - 720.0) > 0.01 or absf(rect.size.y - 640.0) > 0.01: + _problems.append("[winshell] %s 窗板几何偏移(实测 %s 应≈P(280,80) S(720,640))〔窗口化批 V1〕" % [String(k), str(rect)]) + # V2 场景根留底:场景根 visible 且窗板四边距视口边 ≥80px + if not main.ui.modals.camp.scene.visible: + _problems.append("[winshell] %s 开态场景根不可见(栈语义失守)〔窗口化批 V2〕" % String(k)) + elif rect.position.y < 80.0 - 0.01 or vp.y - rect.end.y < 80.0 - 0.01 \ + or rect.position.x < 80.0 - 0.01 or vp.x - rect.end.x < 80.0 - 0.01: + _problems.append("[winshell] %s 窗板四边距视口边 <80px(场景留底不足)〔窗口化批 V2〕" % String(k)) + # V3 遮罩 0.6:视图首个子节点 ColorRect alpha≈0.6 且全屏(容差 0.01) + var dim := view.get_child(0) + if not (dim is ColorRect): + _problems.append("[winshell] %s 首子节点非遮罩件(ColorRect 缺位)〔窗口化批 V3〕" % String(k)) + else: + var cr := dim as ColorRect + if absf(cr.color.a - 0.6) > 0.01 or absf(cr.size.x - vp.x) > 0.01 or absf(cr.size.y - vp.y) > 0.01: + _problems.append("[winshell] %s 遮罩 alpha/尺寸偏离(实测 a=%.3f size=%s)〔窗口化批 V3〕" % [String(k), cr.color.a, str(cr.size)]) + # V5a 互斥单开:五窗集 visible 计数 == 1 + var vis_n := 0 + for w in five: + if w.visible: + vis_n += 1 + if vis_n != 1: + _problems.append("[winshell] %s 开态五窗 visible 计数=%d 应=1(互斥单开失守)〔窗口化批 V5〕" % [String(k), vis_n]) + # 树窗点选遮挡(V5b——树窗开态向锻炉命中区推真实鼠标点:被遮罩挡=零新窗零响应) + main.ui.modals.camp.scene.object_pressed.emit(&"dummy") + await _frames() + var tree_view: Control = views[&"dummy"] + if not tree_view.visible: + _problems.append("[winshell] 树窗未开(V5b 前置中断)〔窗口化批 V5〕") + else: + var hotspot: Rect2 = main.ui.modals.camp.scene.get_hotspot_rect(&"forge") + var c := hotspot.get_center() + _push_click(c) + _push_release(c) + await _frames() + var vis_n2 := 0 + for w in five: + if w.visible: + vis_n2 += 1 + if vis_n2 != 1 or not tree_view.visible: + _problems.append("[winshell] 窗开态点穿场景(visible=%d 树窗=%s——遮罩未挡)〔窗口化批 V5〕" % [vis_n2, str(tree_view.visible)]) + # 清窗经真实钮(真实 UI 中生涯入口只在无窗态可达——遮罩挡场景点选;信号直发的轮转残留 + # 随栈 close 恢复语义逐层清账,V6/V8 从干净单窗态起步) + await _close_all_windows(views) + # V6 ESC 先关窗(生涯窗开态 Esc→关生涯窗、浮窗不唤起) + main.ui.modals.camp.scene.object_pressed.emit(&"notebook") + await _frames() + if not career.visible: + _problems.append("[winshell] 生涯窗未开(V6 前置中断)〔窗口化批 V6〕") + else: + _push_esc() + await _frames() + if career.visible: + _problems.append("[winshell] 生涯窗开态 Esc 未关窗(ESC 优先序失守)〔窗口化批 V6〕") + if main.ui.modals.camp.esc_overlay.visible: + _problems.append("[winshell] 关窗同拍唤起浮窗(应无窗才进浮窗)〔窗口化批 V6〕") + # V7 内容超高窗内滚动(树窗+真实投影喂加长行册) + main.ui.modals.camp.scene.object_pressed.emit(&"dummy") + await _frames() + if not tree_view.visible: + _problems.append("[winshell] 树窗未开(V7 前置中断)〔窗口化批 V7〕") + else: + var data: Dictionary = main._camp_data() + var rows: Array = data.get("tree", []) as Array + for i in 30: + rows.append({"node_id": "probe_row_%d" % i, "display_name": "探针加长行 %d" % i, + "vein": "spine", "kind": "stat", "level": 0, "cap": 1, "price": 0, "can_buy": false}) + data["tree"] = rows + (tree_view as TreeView).refresh_camp(data) + await _frames(3) # queue_free 摘行与 min size 重算帧末延迟 + var panel7: PanelContainer = _window_panel(tree_view) + var rect7: Rect2 = panel7.get_global_rect() + if absf(rect7.position.x - 280.0) > 0.01 or absf(rect7.position.y - 80.0) > 0.01 \ + or absf(rect7.size.x - 720.0) > 0.01 or absf(rect7.size.y - 640.0) > 0.01: + _problems.append("[winshell] 加长行册后窗板涨形(实测 %s——窗不随内容涨失守)〔窗口化批 V7〕" % str(rect7)) + var holder: VBoxContainer = (tree_view as TreeView)._tree_holder + var scroll := holder.get_parent() as ScrollContainer + if not (holder.size.y > scroll.size.y): + _problems.append("[winshell] 内容实高 %f 未超滚动视口 %f(超高实据不足)〔窗口化批 V7〕" % [holder.size.y, scroll.size.y]) + # 清窗经真实钮(同 V6 前置——干净单窗态起步) + await _close_all_windows(views) + # V8 生涯同壳(窗板同几何+返回钮在位+内容列滚动面在) + main.ui.modals.camp.scene.object_pressed.emit(&"notebook") + await _frames() + if not career.visible: + _problems.append("[winshell] 生涯窗未开(V8 前置中断)〔窗口化批 V8〕") + else: + var panel8: PanelContainer = _window_panel(career) + if panel8 == null: + _problems.append("[winshell] 生涯窗板缺失〔窗口化批 V8〕") + else: + var rect8: Rect2 = panel8.get_global_rect() + if absf(rect8.position.x - 280.0) > 0.01 or absf(rect8.position.y - 80.0) > 0.01 \ + or absf(rect8.size.x - 720.0) > 0.01 or absf(rect8.size.y - 640.0) > 0.01: + _problems.append("[winshell] 生涯窗板几何偏移(实测 %s)〔窗口化批 V8〕" % str(rect8)) + if ProbeUiUtil.find_button(career, "返回", "prefix") == null: + _problems.append("[winshell] 生涯「返回」钮缺失〔窗口化批 V8〕") + var scrolls := career.find_children("*", "ScrollContainer", true, false) + var scroll_ok := false + for s in scrolls: + if (s as ScrollContainer).size_flags_vertical == Control.SIZE_EXPAND_FILL: + scroll_ok = true + if not scroll_ok: + _problems.append("[winshell] 生涯内容列滚动面缺失(ScrollContainer 纵向 EXPAND_FILL)〔窗口化批 V8〕") + # 段尾收闭:生涯窗经真实「返回」钮关闭(on_camp_closed 关闭名单不含生涯窗——防残留泄段) + var career_back: Button = ProbeUiUtil.find_button(career, "返回", "prefix") + if career_back != null: + career_back.pressed.emit() + await _frames() + if career.visible: + _problems.append("[winshell] 生涯「返回」钮未隐窗〔窗口化批 V4〕") + main._on_camp_closed() # 复归菜单语境(后续选档段前提不受扰) + await _frames() + + +# 清窗经真实钮(逐窗巡检 visible→钮 pressed——栈 close 恢复账随层清空) +func _close_all_windows(views: Dictionary) -> void: + for k: StringName in [&"forge", &"chest", &"dummy", &"board"]: + var w: Control = views[k] + if not w.visible: + continue + var back: Button = ProbeUiUtil.find_button(w, "返回营地") + if back == null: + _problems.append("[winshell] %s 底栏「返回营地」钮缺失〔窗口化批 V4〕" % String(k)) + continue + back.pressed.emit() + await _frames() + + +# 窗板定位(视图子树固定链:首子节点=遮罩 ColorRect → CenterContainer → 窗板 PanelContainer) +func _window_panel(view: Control) -> PanelContainer: + if view.get_child_count() == 0: + return null + var dim := view.get_child(0) + if dim.get_child_count() == 0: + return null + var center := dim.get_child(0) + if center.get_child_count() == 0 or not (center.get_child(0) is PanelContainer): + return null + return center.get_child(0) as PanelContainer + + +# 真实鼠标合成(press+release 成对——BaseButton pressed 信号默认 release 触发,ui_input_probe :718 先例) +func _push_click(pos: Vector2) -> void: + var ev := InputEventMouseButton.new() + ev.position = pos + ev.global_position = pos + ev.button_index = MOUSE_BUTTON_LEFT + ev.pressed = true + main.get_viewport().push_input(ev, true) + + +func _push_release(pos: Vector2) -> void: + var ev := InputEventMouseButton.new() + ev.position = pos + ev.global_position = pos + ev.button_index = MOUSE_BUTTON_LEFT + ev.pressed = false + main.get_viewport().push_input(ev, true) + + func _push_esc(echo := false) -> void: var ev := InputEventKey.new() ev.keycode = KEY_ESCAPE diff --git a/game/tests/golden/palette_baseline/art_snapshot.png b/game/tests/golden/palette_baseline/art_snapshot.png new file mode 100644 index 0000000000000000000000000000000000000000..8e5fb8be7ed3faa186908f5f960b4f832e4bd863 GIT binary patch literal 2572 zcmV+n3iI`eP)&(JVdZS2uNari2~&%k_XA{teqR_kIvk=bI#ste|xQOe{1b^ z2!K%X$6rLxV7U%$Un^lj=xrm?? zRG6o_;X7iZ3{d}+7>__v8Bhj~cfhrwES@_H--il={jd^HLp;$XV2&w(mM#2PahN7Q zYZp&!SD3uGqjuX)!pQSzJHo#BU|6!3QgGt<`Yl}bB%%p82_%Zx_MWT@rJybd)yjkA z3Z=bm@LU*as4rkggfSRU=V1gq`7vz$YNy2k=6UuLc`z**)JF?i?CpV}kV@za(Pu(f<<1k`z9i#Tv%fcetT9BLo`=pH72^di^yKw=R z|3GleGWch~6nLy4C)0zK!OYo+WV!SKxQ^&^1k)=O_F!Q4U3~r_8F%gfnX*KLM?VmU zqbbb^fF!(_Y-Hzga}%Gcm)S((qI8{7cD@Hw>qj`TbvlfwJqbU;uI;iU90Q7k0Rtj( zr<|W`mGOcl;oF+15d2EOl36t*P=*QHLr@7C>}VC^z=IPRb&v4NMI_pJTz?z?v<%)p z4LgeL*>I|09Mi8w^6$kZ(jzTmqekis5E&0s+XOW{vQe}|BB||-G~5V3-ik4qf^krz zABNyq+`mDNUSJ~#)v}`*$r8y3YYf~B+uzWqYzUfRg6H9cWm7*Q2%!lgW1i;0-NH=B zm+X_I?ibzkbi%Ua$P$wzNqJiG8br8;?Q69ICJaOBgPVpL)chZTH3uBz?lp+|@Lj2O zw4nw@Wp3?hhW`6Xdq5cjMM)R%&3>D+|k;SHjYa6+G89e9cd-qqrCfbrmN zz=lDoIl_sSZg@P~R!b~FLiP;;P2eH;bH%Iojv#gz5f5$+jL$|--{L_FkwXO<-2@C> zks`H#&7 zk6_oq>5(bF6jre|9^jixF{vEkdTl?FbTJ-l7fjK!foL5&))sal5SzwVpT?|-u)|0+ z@ZYaVX|zZY=_#&%?`gwxH{8!@>8B^Ba~ogHyT zmhW2y)FE?Hmh6opyc3!0VE04y=vr|(SNstTWhl-S<d8yH_9)E07k;0TLQNSdLH} zDV=d<9N`Iq&$B$SUx(?lV;dNtyh^`%vZQkp$}X2@U5pSf(Fs1l=bu1b7P-liZuAjv z5aXuD#GffSBqIo=0Z(Q@q^5EAa%5`@?inF?N!VpjgWUNSl-&sS%2Z;UoKE5bx30vP z5^>@10g+QUf=EW!t|u@&oDhFmzr-No<;I%t&>4+TaK!6@|>Q@XBJ}KBQV>f;c?4& zd<&A-i1S;Ukd7izDzfy#JU)cP-(fA}hf7dii=YU$M&KZ~EQZsENCj%H0pe_3ilmR~ zyVbs>AHP|;k3$@>sXOKp$WND1Av7)2($C@O9h z_g=u zw*R#d{#Q+)F%y-geBoE9>_y3}i$smUx?yA+x#&eX=(rkwV;K>tYF~{7QILY)iGn zqWYfFgfKsPM}Vl9&h4+@8;^7MQdoMBMD-3X=)#Mu#m~*(!i-XB28`!qZs-AhIW~bO z5Q=mA-%!0t%v#PMeVt}I$}dmjYwyF-ns)}czVSb|W#Lr(Q(;(;RmB*iX@T3Wkeuzr@%nTz|FxwjxRvpmGkk%q8lf_K1x0`U`FH z3N>_&UAy4hDu0%dI>S?{{{;Lx`NC>{SnVm{%1vDM6yIITo96PJ*DUGN+Ac%6~o2VVf{cXZ$O|2BAEw7+vYav~x12(Tt4m!5T?S<{7)&xD z8HCATQBsu1pOQ(}xuUp1Sq`5M0ch##*Of=G7-6s=Q?}>JtORTgevrT5_@yc@l4vH# zUkwsN$mjPvc$S^dVs+zFztz#z3Sub3Yy*~|J*m#XoL_!!&VQ|HWb5kSSR8+i jB#Km@BQ*np*PQ+bS}{J=%uygs00000NkvXXu0mjfN~e>V literal 0 HcmV?d00001 diff --git a/reports/工程改动报告-工程清理批-2026-11-02.md b/reports/工程改动报告-工程清理批-2026-11-02.md new file mode 100644 index 00000000..91905693 --- /dev/null +++ b/reports/工程改动报告-工程清理批-2026-11-02.md @@ -0,0 +1,75 @@ +# 工程改动报告——工程清理批(2026-11-02) + +接收方:待办清单 7 条目(本批完成整条删除——palette_check 基线建档/CAMP_HEADER_BAND 评估/ui_kit 立绘注释/迭代 43 四色墨断言/断言提取器 check_quiet/run_all_gates 02 传参〔已完成漏删〕/main.gd 死 preload)。 + +## 一、批定位 + +工程改动批(用户发令直改,不写卡)。worktree `eng-cleanup`,基点 7e9e57fd,基点即 master 尖端。七件:六件本批落地+一件核查确认已完成(run_all_gates 02 传参)。 + +## 二、逐件落地 + +### 件 1 palette_check 基线建档+双门机检化 + +- 基线目录 `game/tests/golden/palette_baseline/`(图像白名单在册目录)两张条带 PNG:`ui_tokens.png` 92 色(ui_kit.gd 全量 Color8 常量唯一值快照)+ `art_snapshot.png` 1274 色(现役素材 128 张 PNG 唯一色并集快照);并集 1346 色=双门②「新增色 vs 既有色板」的既有色板机检承载物。 +- 新件 `tools/gen_palette_baseline.py`:基线建档与刷新器(token 提取剔除注释行;素材目录零 PNG 拒绝生成空快照)。 +- `tools/palette_check.py` 零代码改动——头注补基线构成与维护纪律、退出码描述校正(单张超 32 色是 WARN 非 FAIL)。 +- 闸机接线:`tools/run_all_gates.sh` 新增 **05b 色板闸**(code 域;--quick 与影响面选闸 SKIP;素材齐盘预检之后段执行)——扫 `game/assets/art` 全目录对基线判新增色近色红。 +- 口径边界(呈报):门①语义检查属设计面人工核对(§4.2 色相语义判域),不入本器执法面;48 token 计数上限不接闸——ui_kit 现役唯一色值 92 与「48 上限」的成员名单口径差归 art-bible §4.1 owner 裁定,本批不自裁。 +- 维护纪律:素材批收尾跑 `python tools/gen_palette_baseline.py` 重生成全套并随批提交——新色过门后须入基线,否则下批扫描把已过门的新色再报新增;两张 PNG 缺一即漏判。 + +### 件 2 extract_checks.py 补 check_quiet 静默通道 + +- `CALL_RE` → `check(?:_quiet)?\(`、`DEF_RE` 同扩(func check / func check_quiet 定义行双拦)、`collect_call` 按实际匹配长度偏移;check_quiet 通道契约 cond 在前,条目 quiet 列记 `channel:check_quiet`。单趟正则无重叠匹配,结构上不双计。 +- 自证三重(当轮实跑):①双计等式——旧两正则匹配数之和 2155 == 新 CALL_RE 2155(全 tests 面);②新旧版条目对拍 1719 → 2133,差 414 条 check_quiet 入账(grep 调用行实数 416,差 2=定义行与注释行不入账,方向自洽);③`--extract` 生成临时清单后 `--diff` 往返对账 PASS 零差异(标签/cond/计数三对齐)。 +- 验证集清单已随验证体系整改阶段 2 退役,`--diff` 无存档参照——对账自证以临时清单往返承载,语义等同。 +- 随批换代 `design/engineering/test-conventions.md` §5.2 工具面局限备案句(2026-10-27 立的「断言消息不进扫描面——验收尾缀 grep 对该通道失效」已消除,留存会漂移);文档设计面仅此一句现状描述更新,申报。 + +### 件 3 意图徽标四色墨机检断言 + +- `tools/render_sample_sheet_v3.py` `run_screen_checks` 增 **S1b**:意图徽标四色墨(攻击/增益/减益/蓄力)对 PANEL_DATA 垫牌(#E9D9B8,与绘制矩形 fill 同源)≥4.5:1 色值对表断言,四墨值即脚本意图色表四值,零新增色。 +- 当轮真实读数:攻击 5.62/增益 5.10/减益 4.97/蓄力 5.21——与独立审计复算区间 4.97–5.62 逐位一致。 +- 样张机检 31/31 全绿。V10 改前基线对拍图属 artwork(不入 git)环境件,worktree 用 `--save-baseline` 重建(本批零素材零绘制变更,基线等值)。 + +### 件 4 main.gd 拆件遗留死 preload 清理 + +- 逐条 grep 复核(登记纪律):死码 **10 条**——登记 8 条,复核多出 2 条(三场景 preload 中 AdventureScreenScene 登记时另有消费面、UiAssembler 登记时在役;现全零)。UiAssembler 唯一非定义出现=组合根接线段注释提及类名(非常量使用)。 +- 7 具名(TuneSimController/DebugSessionScript/TreasureInteractController/CurioInteractController/CampShopPresenter/BoardResolversScript 等)消费面已迁 `main_ui_build.gd:9-13`(拆件自持 preload)——main.gd 旧常量=拆件残留重复声明;三场景消费面=system_test.gd `load()` 与 main.tscn,删常量不触。 +- 删除 10 条 const preload,保留 Narrator/MainUiBuild/MainAdventureFlow/MainOverlayFlow/MainDebugFlow 五条在役。 + +### 件 5 过时注释换代 + +- `game/presentation/views/ui_kit.gd` TEX_HERO_PORTRAIT 行注释「48px 档」→「素材=原稿分辨率,显示档缩放读 standee_motion PORTRAIT_DISPLAY_H」(迭代 48 显示档批现行口径)。 +- `tools/path_check.py` 头注删「· 立绘定档批 2026-10-07」日期批注记(变更叙事黑名单类);47 行键集指名核实现行仍在役(standee_asset_check 现行 import STANDEE_DISPLAY_H),不改。 + +### 件 6 ui_kit CAMP_HEADER_BAND 令牌退役评估 + +现役消费面 4 处逐一核过:`level_map_screen.gd:107/144`(关卡屏头带与地图条)、`shop_overlay_view.gd:49`(商店头带)、`ui_kit.gd` KeystoneBadge 工厂(钥石徽描边——消费方=营地覆盖层+状态面板钥石余额行)。 + +**结论**:「退役删除」不成立——4 处在用且全是活面板绘制面;营地域柜台头带语义岗已随迭代 47 四柜台覆盖层退役,令牌现役值 #6E4526 只剩材质棕底色语义(木色头带/徽章描边)。**处置=改名中性化**(脱 CAMP_ 前缀,名随现役语义,如 WOOD_BAND 一族)归 ui_kit 令牌批顺带;改名零值变化零渲染变化,届时逐点核后动。维持原登记口径「退役归 ui_kit 令牌批顺带处置、非独立强删」。 + +### 件 7 run_all_gates 02 关联档传参(核查确认已完成) + +`d32ff342`(工程小件批·件 1)已落两案中的「解析器补逗号切分」案:`run_all.gd` 选跑解析补 `split(",")`,闸脚本逗号串传参与之配套,孤立验证四套件全绿。本批零改动,条目属完成漏删。 + +## 三、待办清单 + +删 7 条(整条删除,状态机出口):palette_check 基线建档/CAMP_HEADER_BAND 评估/ui_kit 立绘注释/迭代 43 移交小件(①能力清单补行已随 2026-10-31 写卡批落位、②四色墨断言本批落)/断言提取器改版/run_all_gates 02 传参(d32ff342 已完成)/main.gd 死 preload。流水线闸复验 PASS(33 条目/49 总表行)。 + +## 四、窄验读数(worktree 内当轮实跑) + +| 验证 | 读数 | +|---|---| +| palette_check 带基线扫 art 全目录 | 128 张 0 红 0 新增色 | +| presentation 单元套件(main.gd/ui_kit.gd 编译面) | 17 suites 全绿 | +| 冒烟 run_smoke | 15 项 0 失败,三闸全过 | +| 样张 render_sample_sheet_v3 | 31/31 全绿(含新 S1b) | +| path_check | 0 处问题 | +| extract_checks | 双计等式过/1719→2133/往返对账 PASS | +| pipeline_gate | PASS(33 条目/49 总表行/交叉核 3 链) | + +全量回归归合回后本地 CI(post-merge 钩子自动跑,含新 05b 色板闸)。 + +## 五、边界与移交 + +- 48 token 计数上限的机检承载与门①语义检查不属本批(口径归 art-bible owner),呈报待裁。 +- 05b 色板闸的基线维护义务(素材批收尾刷新)已写进 palette_check.py 头注与 gen_palette_baseline.py 头注,闸机注释同步点名。 diff --git a/tools/capability_audit.py b/tools/capability_audit.py index 9130eeae..6a29592a 100644 --- a/tools/capability_audit.py +++ b/tools/capability_audit.py @@ -45,7 +45,6 @@ STATUS_VALUES = ("已覆盖", "部分覆盖", "未覆盖") # 验收标准节豁免:无可写验收面的在册件(逐件附理由;新豁免须经用户裁) ACCEPT_EXEMPT = { - "锻造工坊.md": "工程化系统,验收面由天赋树与元进度两篇承接,本批迁移实现事实后复核", "生涯.md": "P6 后置未实装,无现成可观测面;实装批补验收标准节", } diff --git a/tools/extract_checks.py b/tools/extract_checks.py index 5183eeb6..c1344161 100644 --- a/tools/extract_checks.py +++ b/tools/extract_checks.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- r"""extract_checks.py — 断言静态提取器(验证集清单生成与出口对账) -机器提取宿主脚本的全部静态 check( 调用点(每个调用点记录:所在文件、所在函数、 +机器提取宿主脚本的全部静态断言调用点(每个调用点记录:所在文件、所在函数、 行号、标签字符串字面量原文、cond 表达式原文),提供两种模式: 用法: @@ -19,10 +19,11 @@ r"""extract_checks.py — 断言静态提取器(验证集清单生成与出口 断言,对账时以「新增」出现(非迁移面),出口判定以显式模式为准。 提取口径: - - 静态 check( 调用点,两种通道都认:smoke/系统宿主的 check(cond, label)(cond 在前) - 与单元/集成宿主的 Kit.check("label", cond)(点前缀=标签在前);`_check(` 等同名 - 前缀调用非标准通道,不取。 - - 调用点可跨行:从 `check(` 起括号配平归集完整实参文本(() [] {} 三种括号都计深度); + - 静态断言调用点,三通道都认:smoke/系统宿主的 check(cond, label)(cond 在前)、 + 单元/集成宿主的 Kit.check("label", cond)(点前缀=标签在前)、静默收集通道 + Kit.check_quiet(cond, label)(cond 在前,条目 quiet 列记 channel:check_quiet); + `_check(` 等同名前缀调用非标准通道,不取。 + - 调用点可跨行:从调用名起括号配平归集完整实参文本(() [] {} 三种括号都计深度); 字符串字面量内的括号/逗号/井号、行尾注释、转义序列均不参与结构判定(码位掩码单趟预扫)。 - cond 摘要=cond 原文跨行折叠为单行(连续空白并一空格);标签原文=标签实参里的字符串 字面量逐字保真(含引号与转义序列原样,禁改写);标签实参若带格式化/拼接后缀 @@ -64,8 +65,8 @@ def default_sources(): files.extend(sorted(p.rglob("*.gd"))) return sorted(files) -CALL_RE = re.compile(r"check\(") -DEF_RE = re.compile(r"^\s*(static\s+)?func\s+check\s*\(") +CALL_RE = re.compile(r"check(?:_quiet)?\(") +DEF_RE = re.compile(r"^\s*(static\s+)?func\s+check(?:_quiet)?\s*\(") FUNC_RE = re.compile(r"^(static\s+)?func\s+([A-Za-z_][A-Za-z0-9_]*)") LITERAL_RE = re.compile(r'"((?:[^"\\]|\\.)*)"') OPENERS = "([{" @@ -188,9 +189,9 @@ def split_args(body): return args -def collect_call(joined, start): - """从 check( 的 start 起括号配平归集调用。返回 (body, end_pos);不配平返回 None。""" - i = start + len("check(") +def collect_call(joined, start, call_len): + """从调用名起点起括号配平归集调用。返回 (body, end_pos);不配平返回 None。""" + i = start + call_len depth = 1 in_str = False parts = [] @@ -228,7 +229,7 @@ def collect_call(joined, start): def extract(src_path: Path): - """提取单文件全部静态 check( 调用点。返回(条目列表, 函数表),条目保持源码顺序。""" + """提取单文件全部静态断言调用点(check/check_quiet)。返回(条目列表, 函数表),条目保持源码顺序。""" src = src_path.read_text(encoding="utf-8") lines = src.split("\n") funcs = func_table(lines) @@ -247,28 +248,32 @@ def extract(src_path: Path): entries = [] for m in CALL_RE.finditer(joined): if not mask[m.start()]: - continue # 字符串字面量内 / 注释里的 check( 字样 + continue # 字符串字面量内 / 注释里的调用名字样 ln = line_of(m.start()) if DEF_RE.match(lines[ln - 1]): - continue # func check 定义行 + continue # func check / func check_quiet 定义行 prev = joined[m.start() - 1] if m.start() > 0 else "" if prev and (prev.isalnum() or prev == "_"): continue # `_check(` 等同名前缀——非标准通道 - label_first = prev == "." # Kit.check("label", cond)——点前缀=标签在前 - body, end_pos = collect_call(joined, m.start()) + is_quiet = m.group(0).startswith("check_quiet") + if is_quiet: + label_first = False # Kit.check_quiet(cond, label)——cond 在前 + else: + label_first = prev == "." # Kit.check("label", cond)——点前缀=标签在前 + body, end_pos = collect_call(joined, m.start(), len(m.group(0))) if body is None: - raise SystemExit("[extract_checks] FAIL:%s 行 %d 起的 check( 括号不配平到文件尾——先查解析缺口" + raise SystemExit("[extract_checks] FAIL:%s 行 %d 起的断言调用括号不配平到文件尾——先查解析缺口" % (src_path.name, ln)) args = split_args(body) if len(args) not in (2, 3): raise SystemExit( - "[extract_checks] FAIL:%s 行 %d 的 check( 拆出 %d 个实参(预期 2/3)——解析缺口,先查再跑" + "[extract_checks] FAIL:%s 行 %d 的断言调用拆出 %d 个实参(预期 2/3)——解析缺口,先查再跑" % (src_path.name, ln, len(args))) if label_first: label_expr, cond = args[0], args[1] else: cond, label_expr = args[0], args[1] - quiet = args[2] if len(args) == 3 else "" + quiet = "channel:check_quiet" if is_quiet else (args[2] if len(args) == 3 else "") lm = LITERAL_RE.search(label_expr) if lm is None: continue # 标签实参无字面量(wrapper 直传变量——既有套件先例 reward_overlay_test _req) @@ -336,7 +341,7 @@ def render_list(entries, funcs_by_src, src_names, carry=None) -> str: out.append("# 验证集清单(断言快照·冻结)") out.append("") out.append("> 生成日期:%s · 基点 commit:`%s` · 源:%s" % (date.today().isoformat(), base_commit(), "、".join(src_names))) - out.append("> 提取口径:静态 `check(` 调用点,括号配平归集(可跨行);缺省源=三宿主遍历") + out.append("> 提取口径:静态 `check(`/`check_quiet(` 调用点,括号配平归集(可跨行);缺省源=三宿主遍历") out.append("> (unit/**+integration/**+system_test.gd)。本基点实测 %d 个静态调用点;行级" % len(entries)) out.append("> `grep -c \"check(\"` 是另一口径(含 `func check(` 定义行与注释行),两口径各自浮动,") out.append("> 禁互相换算(tests/README「断言计数双口径」)。运行时计数大于静态计数=循环展开条目。") diff --git a/tools/gen_palette_baseline.py b/tools/gen_palette_baseline.py new file mode 100644 index 00000000..78d58086 --- /dev/null +++ b/tools/gen_palette_baseline.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""色板基线生成器——palette_check 双门②基线的建档与刷新件。 + +产出两张 1×N 条带 PNG 落 game/tests/golden/palette_baseline/(图像白名单在册目录): + ui_tokens.png = ui_kit.gd 全量 Color8 常量唯一色值(token 板现役值快照) + art_snapshot.png = 现役素材(game/assets/art 递归)全量唯一色快照(像素板+各屏环境子板) + +palette_check --baseline 取目录下全部 PNG 的唯一色并集为「既有色板」;基线缺一张 +即漏判——基线刷新必须重生成全套。维护纪律:素材批收尾跑一次本器刷新并随批提交 +(新色过门②后须入基线,否则下批扫描把已过门的新色再报新增)。 + +用法: + python tools/gen_palette_baseline.py [--repo <仓库根>] +退出码: 0 成功 / 2 前置缺位(ui_kit.gd 或素材目录缺 / 素材零 PNG) +""" +import argparse +import os +import re +import sys + +from PIL import Image + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from palette_check import palette_of # 排除全透明的唯一色提取——与检查器同口径 + + +def strip_comments(text): + """剔 GDScript 行注释与字符串字面量,防注释里举的色值被误收进 token 快照。""" + out = [] + for line in text.split("\n"): + i = line.find("#") + if i != -1: + line = line[:i] + out.append(line) + return "\n".join(out) + + +def token_cols(ui_kit_path): + """ui_kit.gd 全量 Color8 唯一色值集合。""" + text = strip_comments(open(ui_kit_path, encoding="utf-8").read()) + vals = re.findall(r"Color8\((\d+),\s*(\d+),\s*(\d+)\)", text) + return sorted(set((int(r), int(g), int(b)) for r, g, b in vals)) + + +def scan_dirs(roots): + """递归扫 PNG,唯一色并集(含像素位置首见序不保——并集无序)。""" + cols = set() + count = 0 + for root in roots: + for dirpath, _, names in os.walk(root): + for fn in sorted(names): + if fn.lower().endswith(".png"): + c, _n = palette_of(os.path.join(dirpath, fn)) + cols |= c + count += 1 + return cols, count + + +def write_strip(cols, out_path): + """1×N 条带 PNG(一色一像素列)。""" + cols = sorted(cols) + im = Image.new("RGB", (len(cols), 1)) + for i, c in enumerate(cols): + im.putpixel((i, 0), c) + im.save(out_path) + return len(cols) + + +def main(): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + ap = argparse.ArgumentParser(description="色板基线生成器(token∪素材快照)") + ap.add_argument("--repo", default=".", help="仓库根(含 game/ 与 tools/)") + args = ap.parse_args() + + ui_kit = os.path.join(args.repo, "game", "presentation", "views", "ui_kit.gd") + artdir = os.path.join(args.repo, "game", "assets", "art") + outdir = os.path.join(args.repo, "game", "tests", "golden", "palette_baseline") + if not os.path.isfile(ui_kit): + print("[gen_palette_baseline] FAIL:ui_kit.gd 缺:%s" % ui_kit) + return 2 + if not os.path.isdir(artdir): + print("[gen_palette_baseline] FAIL:素材目录缺:%s" % artdir) + return 2 + os.makedirs(outdir, exist_ok=True) + + tcols = token_cols(ui_kit) + n_t = write_strip(tcols, os.path.join(outdir, "ui_tokens.png")) + + acols, npng = scan_dirs([artdir]) + if npng == 0: + print("[gen_palette_baseline] FAIL:素材目录零 PNG(素材未同步)——拒绝生成空快照") + return 2 + n_a = write_strip(acols, os.path.join(outdir, "art_snapshot.png")) + + union = set(tcols) | set(acols) + print("[gen_palette_baseline] 已生成:ui_tokens.png %d 色 / art_snapshot.png %d 色" + "(素材 %d 张 PNG);基线并集 %d 色 → %s" + % (n_t, n_a, npng, len(union), outdir)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/palette_check.py b/tools/palette_check.py index b1cc3d87..9a0575ee 100644 --- a/tools/palette_check.py +++ b/tools/palette_check.py @@ -17,12 +17,21 @@ 另:必须排除全透明像素 (0,0,0,0)(同standee_asset_check 的坑)。 +基线(双门②「新增色 vs 既有色板」的既有色板机检承载物): + 目录 = game/tests/golden/palette_baseline/,两张条带 PNG(1×N,一色一像素列): + ui_tokens.png = ui_kit.gd 全量 Color8 token 快照(token 板现役值) + art_snapshot.png = 现役素材全量唯一色快照(像素板+各屏环境子板) + 两张并集=「既有色板」。缺一张即漏判——基线刷新必须重生成全套。 + 门①语义检查属设计面人工核对(§4.2 色相语义判域),不在本器执法面。 + 用法: - # 主用法:扫目录,新增色相对 --baseline 判定 + # 主用法(闸机 05b 色板闸承载):扫目录,新增色相对 --baseline 判定 python tools/palette_check.py <目录> --baseline <基线目录> # 无基线:只出读数(单张色数/ 近色对计数),不判红 python tools/palette_check.py <目录> -退出码: 0 通过 / 1 有红灯(仅新增色违规或超单张上限时) + # 基线建档与刷新:重扫现役素材∪token 重生成基线条带(素材批收尾动作,随批提交) + python tools/gen_palette_baseline.py +退出码: 0 通过 / 1 有红灯(仅新增色近色不过门②时) """ import os import sys diff --git a/tools/path_check.py b/tools/path_check.py index 60f979b5..eba52dbf 100644 --- a/tools/path_check.py +++ b/tools/path_check.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""B3 路径核对器(圣经 §8.3② 挂点实现 · 立绘定档批 2026-10-07)。 +"""B3 路径核对器(圣经 §8.3② 挂点实现)。 职责:扫描 asset-path-map.md 声明的全部路径 + 代码侧素材常量, 报「悬空(声明存在但盘上无)/ 退役未清(仍引用已退役命名)」。 diff --git a/tools/render_sample_sheet_v3.py b/tools/render_sample_sheet_v3.py index f540deb8..744f007e 100644 --- a/tools/render_sample_sheet_v3.py +++ b/tools/render_sample_sheet_v3.py @@ -1209,6 +1209,18 @@ def run_screen_checks(scr, zones, board_origin, frames_strip, frames_meta, s6_im weakest = min(ratios, key=lambda r: r[1]) check("S1", "整屏①全部文字对直接背景 ≥4.5:1(WCAG AA)", all(r[1] >= 4.5 for r in ratios), "最弱 %s=%.2f;全部 %s" % (weakest[0], weakest[1], ratios)) + # S1b 意图徽标四色墨对 PANEL_DATA 垫牌 ≥4.5:1(色值对表——镜像实装同值;垫牌=ui_kit PANEL_DATA #E9D9B8, + # 与 draw_standee 徽标垫矩形 fill 同源;四墨=意图色表 damage/buff/debuff/charge 四值) + panel_data = (233, 217, 184) + intent_inks = [("攻击墨", (150, 44, 36)), ("增益墨", (30, 90, 150)), + ("减益墨", (122, 66, 148)), ("蓄力墨", (116, 80, 11))] + iratios = [(n, round((max(wcag_lum(a), wcag_lum(panel_data)) + 0.05) + / (min(wcag_lum(a), wcag_lum(panel_data)) + 0.05), 2)) + for n, a in intent_inks] + iweak = min(iratios, key=lambda r: r[1]) + check("S1b", "意图徽标四色墨对 PANEL_DATA 垫牌 ≥4.5:1(WCAG AA·色值对表)", + all(r[1] >= 4.5 for r in iratios), + "最弱 %s=%.2f;全部 %s" % (iweak[0], iweak[1], iratios)) # S2 整屏②明度三档带 + 世界底层两子带(§2;舞台=实测区域读数,格底=实测板面;WCAG 0–1 域) stage_l = _region_wcag(scr, (300, 190, 600, 220)) bx, by = board_origin diff --git a/tools/run_all_gates.sh b/tools/run_all_gates.sh index 4c38787b..341b4efe 100644 --- a/tools/run_all_gates.sh +++ b/tools/run_all_gates.sh @@ -413,6 +413,14 @@ if [ $QUICK -eq 1 ] || [ $NONBEHAVIOR -eq 1 ]; then else gate "05 渲染闸" "render.log" bash "$REPO/tools/render_gate.sh" "$GAME" fi +# ── 5b. 色板闸(§4.1 新色双门②:素材新增色 vs 基线〔token∪素材快照〕近色判红; +# 门①语义检查属设计面人工核对不入机检;基线刷新=tools/gen_palette_baseline.py,素材批收尾动作)── +if [ $QUICK -eq 1 ] || [ $NONBEHAVIOR -eq 1 ]; then + NAMES+=("05b 色板闸"); EXITS+=("SKIP"); LOGS+=("") + echo ""; echo "[gates] 05b 色板闸 → SKIP(--quick 或 影响面选闸:素材色板面不随测量面改动开闸)" +else + gate "05b 色板闸" "palette.log" "$PY" "$REPO/tools/palette_check.py" "$GAME/assets/art" --baseline "$REPO/game/tests/golden/palette_baseline" +fi fi # ══ 文档域(6–8e)══ if in_domain docs; then diff --git a/待办清单.md b/待办清单.md index a7ac2e85..268cd45b 100644 --- a/待办清单.md +++ b/待办清单.md @@ -2,22 +2,6 @@ 本清单是流水线的唯一登记处:以后才做的事在此登记一条,随流水线段推进改状态,做完整条删除(条目记进度,不是各段的开工前置);各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据含可选优先级,正文=实况/缺口/改动面/状态四属性)与状态机以 [doc-conventions §12.1](design/engineering/doc-conventions.md) 为权威。 -#### palette_check 基线建档与双门机检化(2026-11-02 登记,来源=批次移交) - -**实况**:证据=迭代 47 实现报告 §7.2(已按吸收即死退役,git 提交 3443890d 可考)——卡④ V10 素材级读数:15 张素材 palette_check 只读模式 0 红灯 0 新增色;但 48 令牌 baseline 目录在库缺位(机检无对照基线),§4.1 两道门(语义检查+RGB 三通道差)的双门机检化缺承载。归工程改动批:建 baseline 目录+闸件双门判据落地。 - -**改动面**:工具代码 - -**状态**:待执行 - -#### ui_kit CAMP_HEADER_BAND 令牌退役评估(2026-11-02 登记,来源=批次移交) - -**实况**:证据=迭代 47 审计记档 2 订正在案(git 提交 3443890d 可考)——营地域柜台头带消费面已随迭代 47 四柜台覆盖层退役注销(ui_contrast 令牌表四行随形态注销),现役消费面仅剩 keystone_badge 工厂与关卡屏/商店两处非营地域用途。处置口径(实现批备案):退役归 ui_kit 令牌批顺带处置、非独立强删;届时逐点核消费面后删令牌或改名。 - -**改动面**:游戏代码 - -**状态**:待执行 - #### hobgoblin 内容三件套补齐(2026-11-01 登记,来源=审计发现) **实况**:敌人表 hobgoblin 行(寻址键 (1, battle, 3))数值完整在役、战斗正常运行,但内容三件套未交付——立绘、敌种语义模板、敌人系统.md 敌种表行均缺,呈现面因语义模板未成文无定义可依。登记凭据=[敌人系统](design/gdd/systems/战斗循环/敌人系统.md) v1.1「hobgoblin 缺口登记」节(2026-10-13 迭代 24 反哺④)。 @@ -38,22 +22,6 @@ **状态**:待文档设计 -#### ui_kit.gd 立绘注释陈旧顺带换代(2026-11-02 登记,来源=批次移交) - -**实况**:证据=迭代 48 实现报告§六与验收审计观察项(已退役,git 提交 4c94eee3 可考)——`ui_kit.gd` TEX_HERO_PORTRAIT 行注释仍写「48px 档」(现行=原稿分辨率+显示档缩放),`path_check.py` 一处注释表述也已改指外;零行为,随下次触碰该两件的批次顺带换代(顺带件,不独立出批)。 - -**改动面**:游戏代码 - -**状态**:待执行 - -#### 迭代 43 批后移交小件——能力清单补行+四色墨断言(2026-10-30 登记,来源=批次移交) - -**实况**:证据=迭代 43 反哺点清单 9/13(已按吸收即死退役,git 历史可考)。逐条:①**能力清单补行**——game/tests/README.md 能力清单无战斗一屏样张批 V5–V9 代表用例行(卡片成文早于清单 2026-10-29 改版定稿),归写卡段随下一写卡会话按 test-conventions §8.3 补行(能力=站桩呈现定值与素材绑定;代表用例=`game/tests/unit/presentation/standee_present_values_test.gd:_init`,真实入口=`UiKit.load_tex()`/`StandeeBody.name_line_probe()`),并同步覆盖域表「战斗表现.md」行。②**意图徽标四色墨补机检断言**——样张脚本 S1 文字对比为绘图用色值对表,意图徽标四色墨(对 PANEL_DATA 垫牌)未入 CHECKS;独立审计复算 4.97–5.62 全 ≥4.5 事实成立仅缺机检断言,`tools/render_sample_sheet_v3.py` CHECKS 补一条对表断言(values 全在册,零新增色)。**2026-10-31 写卡会话落位**:①能力清单补行已随「站桩名字行提档与地台色合并批」卡片成文同批入表(能力行+覆盖域表战斗表现.md 行);②四色墨断言仍待执行。 - -**改动面**:游戏代码+工具代码 - -**状态**:待执行 - #### 架构审查优化讨论——全库架构审计读数的逐条裁决与整改分批(2026-10-29 登记,来源=审计发现) **实况**:2026-10-29 全库架构静态审计完成,读数见 reports/审计-全库架构审查-2026-10-29.md(七节:总评/健康面/P0/P1/P2/待裁点/取证口径;零测试执行零引擎运行,全部结论带 文件:行号)。健康面已逐条核干净:Core 零 UI 八项判据全零命中、core 内 RNG 单点 100% 成立、字符串式信号连接全库 0 处、core 层零超长函数、19 表 ↔ 19 schema 一一对应、无循环 preload。问题集中在三条「写了没闸守」的纪律被实际破坏:①数值单一事实源被 18 处硬编码击穿,最危险两处是 `presentation/main.gd:787` 与 `:790` 把 `rap_refresh_cost=8` 与 `refresh_cap_per_round=2` 写死进提示文案(紧邻 783 行是正确读表的),表改文案不改、玩家直接受错误数字;另有 4 组代码自标「机制结构常量/产出约束」的字面量(`node_map_generator.gd:12` `MAX_WIDTH`、`action_economy.gd:14` `CHARGE_LAYER_CAP`、`board_data.gd:9` `SPAWN_AREA_CELLS`、`node_map_generator.gd:83-94` 域字面量)是否入表待裁。②`game/sim/` 与 `game/core/` 四组平行实现——扫雷推理引擎双份(`board_generator.gd` `measure_inferability` 族 vs `auto_player.gd` `_deduce` 族,输入集口径不同:前者含 safe_set oracle、后者 no-peek)、伤害掷点期望重算(`damage_probe.gd:196-201`)、档位排序与驱动循环双份(`level_autoplay.gd` vs `treasure_item_table.gd:44` 与 `battle_driver.gd:17`)、FakeDB 伪造表喂生成器——直接威胁「模拟读数 = 游戏真实数值」这一数值验证前提。③RNG 单点纪律在 core 外失守(sim 4 处/probe 1 处/tests 3 处自建 `RandomNumberGenerator`,均显式定种、确定性尚存),而 `static_scan.sh` 的 S1/S2 扫描面只含 `DIR_CORE` + `DIR_PRES`。守卫面另有三缺口:S8 是 9 文件显式清单(新增结算文件忘登记=静默不扫,缺位自检只防删改不防增)、S9 类型闸与 S13 质量闸扫描面止步于 core+data+presentation(sim/probe/tests 三层除 S11/S12 外零闸)、S13 三处豁免常态化到最大户文件(`battle_orchestrator`/`main.gd`/`tune_sim_controller`);另有四个 core 文件零测试点名(`attune_loadout`/`battle_fx_host`/`meta_state_store`/`slot_store`)。P2 级:`core/settlement/` 五段类直读写编排器私有成员 20+ 处(`oc._damage.*`/`oc._enemy_system.*`,GDScript 无编译期检查、改名静默断裂);架构文档三处漂移(`architecture.md:110` 登记的 `core/hero_ledger.gd` 盘上不存在且 `hero_ledger_test.gd:171` 显式断言它必须不存在、`:135` 行数过期、EC8 热重载门后 `ConfigDb.reload()` 零调用方且走 `load()` 拿不到磁盘新值);class_name 级双向依赖(`core/config_db` → `data/schemas/board_gen_table` → `core/board_data.SPAWN_AREA_CELLS`)与 `NODE_TYPES` 词表双份;`MetaProgression` 是唯一用 `get_node("/root/...")` 取依赖、唯一跨 Autoload 直调的单例。 @@ -87,29 +55,18 @@ **状态**:文档设计已闭合 -#### 断言提取器改版——覆盖 Kit.check_quiet 静默通道(2026-10-29 登记,来源=会话提及) - -**实况**:`tools/extract_checks.py:67` 的 `CALL_RE = check\(` 只认即时通道,静默收集通道 `Kit.check_quiet(cond, desc)` 静态站点实测 407 个在提取口径外(证据=`grep -rn "check_quiet(" game/tests --include="*.gd"` 实数;test-conventions §5.2 已备案该工具局限)。能力清单以 `文件:函数` 为锚,不受影响;但要把断言面全账纳入机器对账(重建账本或全量断言面审计),须先改版提取器:识别 `check_quiet(` 并按反参序(cond 在前)提取标签与 cond。改版后须以全量套件跑一遍 `--diff` 自证提取面覆盖、且 `Kit.check(` 与 `check_quiet(` 不双计。 - -**改动面**:工具代码 - -**状态**:待执行 - #### 能力清单覆盖缺口补齐——六类无断言面或设计未定项(2026-10-29 登记,来源=会话提及) -**实况**:证据=`game/tests/README.md` 覆盖域表 25 篇对照行(2026-10-29 立)。以下部分覆盖/未覆盖项作为缺口指针,逐项:①增益状态——只验契约词表与布尔受理,叠层/衰减/清除结算链无断言;②设置与本地化——减动效开关本体仅系统测试与冒烟覆盖、无单元/集成断言,多语言切换面未建;③音频——MVP 无音频,P1 后置无断言面;④战斗表现——死亡灰化与减动效演出档只有探针与人工目检;⑤难度曲线与Boss——难度曲线整链与 Boss 出场门控无专项断言;⑥锻造工坊——价格梯仅长剑路线逐级验,GDD 无验收标准节(`capability_audit` 验收标准豁免在册)。教学引导与生涯两篇的缺口归各自在册条目(「教学形态未定」「「生涯」界面——冒险中数据回看的唯一承接处」),不在本条。补齐时同批刷新覆盖域表与能力行,新增断言按 test-conventions §8.4 绑真实入口。 +**实况**:实际情况=2026-11-02 设计讨论闭合——④除名(用户裁:死亡灰化/碎裂属纯表现锦上添花,探针+人工目检已是合规终态,呈现与 battle.md 承诺行零触碰);范围收窄:④除名后余五类,其中②多语言半边(P1 功能缺口,归 owner 文档待设计问题清单)、③音频(P1 后置)、⑤数值落带半边(归 M4 数值批)三块延期不在本条,可写卡面=①增益状态结算链、②减动效开关、⑤机制门控、⑥锻造工坊结构四处。关联文档: +- `game/tests/README.md` 覆盖域表 25 篇对照行(2026-10-29 立,缺口证据) +- `design/gdd/systems/跨局循环/锻造工坊.md` v0.11(2026-11-02,验收标准节九条判据落文——⑥写卡输入合同) +- `design/gdd/systems/战斗循环/增益状态.md` v1.1(2026-10-26,①结算链规则定稿) +- `design/gdd/systems/冒险循环/难度曲线与Boss.md` v1.2(⑤机制门控规则定稿;数值带 provisional 归 M4) +- `design/gdd/systems/表现与支撑/设置与本地化.md` v1.1(②减动效开关与呈现承诺现行口径) -**改动面**:游戏代码 +**改动面**:游戏代码+工具代码 -**状态**:待执行 - -#### run_all_gates 02 关联档多套件词传参修复(2026-10-31 登记,来源=批次移交) - -**实况**:证据=iter45 实现报告 §5 遗留观察(已按吸收即死退役,git 历史可考)——`tools/run_all_gates.sh` 02 关联档以逗号连接多套件词传单个 `--only=a,b` 参数,而 `game/tests/run_all.gd` 选跑解析器不切逗号(多词整串做子串匹配→零命中 quit(2));iter45 施工时以多 `--only=` 旗标形态绕过。合回后 CI 跑全量档不经此路径不受影响,只挡开发期关联档窄跑。修法两案待裁:闸脚本改传多旗标,或解析器补逗号切分。 - -**改动面**:工具代码 - -**状态**:待执行 +**状态**:文档设计已闭合+待写卡 #### 战斗域结构深拆批——效果发射端口+掷格/受理分件(2026-10-31 重立,来源=批次移交) @@ -126,14 +83,6 @@ **状态**:文档设计已闭合+已写卡+已编迭代 49 -#### main.gd 拆件遗留死 preload 清理(2026-10-31 登记,来源=批次移交) - -**实况**:证据=iter44 反哺点 11(已按吸收即死退役,git 历史可考;实现报告实现决定记录同载)——迭代 44 卡① 组合根注册面批拆件后,main.gd 遗留零使用 preload 常量 8 条(UiAssembler/TuneSimController/DebugSessionScript/TreasureInteractController/CurioInteractController/CampShopPresenter/BoardResolversScript/三场景 preload 中 main 零使用者三件);卡① 施工时仅清理与迁移直接相关的 ModalStack/RowPacer 两条,其余属存量债不扩范围,归工程改动直改批顺路清理。清理前须逐条 grep 复核零使用(布局批与 iter45 合回后消费面或有变化)。 - -**改动面**:游戏代码 - -**状态**:待执行 - #### 数值标定窗口总挂账——原九条数值类条目合并(2026-10-28 登记,来源=会话提及) **实况**:2026-10-28 用户令把九条数值类挂账合并为一条:削弱档位的最终数值/迭代 10 奖励重构联动欠账/调参批数值点评登记/三方向各自的解锁清单与定价/GP 经济换算失衡/天赋树实装批登记欠账余两件/商店独立刷新机制两参数/基线通关率越带加难杠杆扫描/成长系数节奏——原九条同步删除,内容全量迁入汇总文档,git 历史可考(本提交)。共同背景:M4 数值批卡已随 2026-10-16 删卡令退役;2026-10-18 用户裁=各条留待办清单、未设计未启动、不另行归属,未来数值标定窗口重开时重走设计段再写卡——本条沿用该裁定,重启条件=用户发令开窗口。合并动机=九条共享同一重启条件与同一执行形态(窗口内一次 MC 批量统一覆盖各条对比需求:全冒险期望+奖励期望),分散登记徒增维护面。逐条挂账明细、裁定史与相关文档见汇总文档。 From 5f5e7a0029565d10cf9e1097d9053c4595c80814 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 2 Nov 2026 07:30:34 +0800 Subject: [PATCH 4/4] =?UTF-8?q?iter49-=E6=94=B6=E5=B0=BE=EF=BC=9Aiter49?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=8A=A5=E5=91=8A+=E5=8F=8D=E5=93=BA?= =?UTF-8?q?=E7=82=B9=E6=B8=85=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- reports/iter49-反哺点-2026-11-02.md | 32 ++++++ reports/iter49-实现报告-2026-11-02.md | 101 ++++++++++++++++++ reports/iter49-验收审计-2026-11-02.md | 146 ++++++++++++++++++++++++++ 3 files changed, 279 insertions(+) create mode 100644 reports/iter49-反哺点-2026-11-02.md create mode 100644 reports/iter49-实现报告-2026-11-02.md create mode 100644 reports/iter49-验收审计-2026-11-02.md diff --git a/reports/iter49-反哺点-2026-11-02.md b/reports/iter49-反哺点-2026-11-02.md new file mode 100644 index 00000000..2f637e86 --- /dev/null +++ b/reports/iter49-反哺点-2026-11-02.md @@ -0,0 +1,32 @@ +# 迭代 49 反哺点清单(2026-11-02) + +> 实现批产出物:要写进 `design/` 与根流程清单的点逐条列落点(哪个文件哪一节、改成什么、依据是什么),由用户裁决后经文档批(doc-design 技能)写入并清条。实现批禁写 `design/` 与根规则文件。 + +## A. 卡①效果端口与掷格分件 + +| # | 落点 | 改成什么 | 依据 | +|---|---|---|---| +| A1 | `design/engineering/architecture/architecture.md`「战斗编排域拆件定稿」节轴戊两条+功能层 B4 行/BattleFxHost 行/damage_controller 行/action_economy 行/reward_economy 行 | 「实现批建件」待建注记换代为已建件现状:EffectPort(`game/core/effect_port.gd`,六 Callable 路由、bind_port 组装期转注、闭面五键 biz_fact、未绑零扰动缺省)与 RewardRoller(`game/core/reward_roller.gd`,roll 批束四键 mapping/power_roll/clear_identity/clear_power、选行算子 `_weighted_pick_row` 返表行、fatal 经构造 Callable 链);B4 公开面 weighted_pick 退役(返行选行算子迁掷格件)、roll_board 装配序(守卫→复位→委托→收纳→ready)、类变量实数(economy 11、damage 12、action 10、fx_host 15、orchestrator 15) | 本批提交 848723b5 实况 | +| A2 | `design/gdd/术语表.md` | 「效果端口件」「掷格件」两机制名是否登记由文档批裁决(首现定义在 architecture 轴戊条款) | 卡① §6 同步面声明 | +| A3 | `tools/static_scan.sh` S8 清单头注 | 「共 10 文件」计数句与实列(14 文件)失符——入批前存量漂移,随批订正 | 本批件核发现(实现报告 §七) | + +## B. 卡②模态句柄分域 + +| # | 落点 | 改成什么 | 依据 | +|---|---|---|---| +| B1 | `design/engineering/architecture/architecture.md` ui_registry 行(v1.30 行号漂移,以内容锚「三域句柄容器」定位) | 换代为「三域句柄容器+模态编排件直驻」:screens/battle/modals+flow(ModalFlow 自 UiModals 升格 UiRegistry 直驻)+register_layer 迁驻注册面;类变量实数 UiRegistry 3→4、UiModals 14→3(run/menu/camp 三子容器纯字段) | 本批提交 bc5dd4ec 实况 | +| B2 | 同上 UiCampModals 行 | 「ui_modals 平铺面收窄」现状化为平铺面清零(分域纪律册级化——12 平铺句柄全数迁 run/menu 两子容器) | 同上 | +| B3 | 同上 ModalFlow 行(「实例随 UiModals 容器构造即建」句) | 换代为「随 UiRegistry 构造建立」(构造时点等价——注册面构造点先于装配) | 同上 | + +## C. 卡③营地五界面真窗口化 + +| # | 落点 | 改成什么 | 依据 | +|---|---|---|---| +| C1 | `design/gdd/ux/camp.md`「叠层与开合」节 | 补实装注记:五窗统一居中定尺寸窗壳起点 720×640 已落码(窗几何单点=CampSubViewBase.WINDOW_SIZE、窗板石板皮、遮罩复用模态遮罩件现值 0.6);样张目检校准意见回填终值(观感校准=普通设计决策) | 本批提交 1f0a8bd3 实况+camp.md v2.5 定稿值 | +| C2 | `design/gdd/08-Alpha版本标准.md`「三·界面」营地行 | 在役陈述补窗壳形态注记(营地五功能页=居中定尺寸功能窗口、场景根留底、窗内滚动) | 卡③ §6 同步面 | +| C3 | `体验验证清单.md` | 登记目检项三条:①五窗观感与场景可见度样张判定(720×640 起点/遮罩 0.6 观感校准)②窗内滚动手感(天赋树长列表/生涯页)③(迭代 48 批后无新增卡①卡②目检项——行为零变化) | 实现报告 §六(真人检测只登记不阻塞) | +| C4 | `design/gdd/systems/跨局循环/生涯.md` :72 | 核查结论记档:现行「营地场景图+五功能窗口形态」陈述与本批一致,零冲突零义务 | 卡③ §6 零义务条(仅记录) | + +## D. 退役提醒(吸收即死) + +本清单各条经文档批写入后逐条清除;本文件与实现报告、三张需求卡片按「证据只留最新」在批后收尾(batch-closure)退役。 diff --git a/reports/iter49-实现报告-2026-11-02.md b/reports/iter49-实现报告-2026-11-02.md new file mode 100644 index 00000000..8e1144c1 --- /dev/null +++ b/reports/iter49-实现报告-2026-11-02.md @@ -0,0 +1,101 @@ +# 迭代 49 实现报告(2026-11-02) + +> 批性质=代码实现批(三卡:效果端口与掷格分件+模态句柄分域+营地五界面真窗口化)。工作区 `g-plan2-iter49`(分支 `iter49`),定义面(roadmap/待办清单/三卡 v1.1)已在 HEAD 预入库、零搬运提交。卡序=核心域先行(端口分件→句柄分域→窗口化),整迭代单工作区串行。 + +## 一、各卡状态 + +| 卡 | 终态提交 | 状态 | 里程碑步闸 | +|---|---|---|---| +| ①效果端口与掷格分件 | 848723b5 | 完成(T0–T4 全落) | `--code` 全绿(红件修复后复验) | +| ②模态句柄分域 | 53c1536c | 完成(T0–T2 全落) | `--code` 全绿(S10 头注修正后复验) | +| ③营地五界面真窗口化 | 1f0a8bd3+2d325d3e(件核) | 完成(T0–T3 全落) | `--code` 全绿(7 闸全绿) | + +## 二、S13 读数对照(改前 → 改后,卡① §14 预算全数达成) + +| 文件 | 类变量 | 公开方法 | 判定 | +|---|---|---|---| +| battle_orchestrator.gd | 15 → 15 | 14 → 14 | 持平(硬预期)✓ | +| battle_fx_host.gd | 14 → 15 | 12 → 16 | 贴顶 ≤15 ✓(+facts_inject 一成员+四法) | +| damage_controller.gd | 15 → 12 | 9 → 9 | 只降 ✓(set_fx 退役/bind_port 增补相抵) | +| action_economy.gd | 11 → 10(plain 口径;raw 15 含 4 属性访问器) | 15 → 15 | 只降 ✓ | +| reward_economy.gd | 15 → 11 | 12 → 9 | 只降 ✓(掷格五成员十二函数三常量迁出+roller 增补) | +| ui_modals.gd(卡②) | 14 → 3 | 1 → 0 | 只降 ✓(转纯字段容器) | +| ui_registry.gd(卡②) | 3 → 4 | 2 → 3 | 上限内不产新键 ✓ | +| 新件三查 | effect_port 6/6、reward_roller 2/6、ui_run_modals 0/6、ui_menu_modals 0/6 | — | 全默认阈值内 ✓;gdstyle 基线外新违规 0(roll 局部变量 12→10 内联收敛后) | + +## 三、实际改动—验收对照表 + +### 卡①(断言尾缀〔效果端口与掷格分件批 V1/V2/V3〕,载体 `game/tests/unit/battle_settlement/effect_port_test.gd`) + +| 需求点 | 验收断言(文件:行) | 真实入口 | 结果 | +|---|---|---|---| +| R1 端口单实例与真实发射路由 | effect_port_test.gd:66(端口同一性——内部字段直查受控例外:端口同一性无公开查询面,对照表行 1 注明);:69-79(attack_commit 经真实回合命中可观测:蓄力层预置 3→攻击行在流→层账 3→0 全处理→delta 读数非零) | `BattleOrchestrator.begin_battle()` 装配链+真实停手回合(settle_batch 同步五段) | 绿(15/15) | +| R2 业务类零引擎直连 | effect_port_test.gd:89-95(两业务文件全文零 `EffectEngine`/`EffectContext` token,扫描面非空前置在案) | 测试直读源码(源码契约锚惯例) | 绿 | +| R3 伤害域事实面闭集 | effect_port_test.gd:107-137(阳性:hp_ratio 载荷 when→承伤减量恰 1;阴性:battle_index 不答→零减量;合成行 effect 段逐字抄现役铁壁 shield 行、订阅面换代独占) | `DamageController.flush_submit()` board_flat_hero 真实单据流 | 绿 | +| R4 端口接线时序与入站路由等价 | 既有套件照跑:refresh_feats_test/effect_words_t1_test/adventure_feats_test(原判据零改) | 真实玩法路径 | 绿 | +| R5 掷格件建件与摇号等价 | 既有 board_gen_reward_test 照跑(全 API);round_flow EV 锚+事件流逐位回放 134 检查 | `begin_battle()` install 链 roll_board 恰一次 | 绿 | +| R6 economy 收纳与键账只降不升 | T3 读数对账(15→11/12→9,见 §二)+既有受理面套件照跑 | 既有受理 API | 绿 | +| R7 装板域与编排域改址 | 既有 variant_placement_test 照跑(白盒指址换代 18 行 22 处、判据零删改) | 装板摇号链 | 绿 | +| R8 行为零变化总证 | round_flow_skill_pool_test 134 检查全绿(EV 锚+逐位回放 ×2 关) | 全战斗回放 | 绿 | +| R9 闸清单随动 | static_scan.sh S7_B1_SET/S8_FILES 各增两新件、B4 符号清单 weighted_pick 行删除;run_all.gd manifest 登记;代码域闸全绿 | 静态扫描域闸 | 绿 | + +### 卡②(断言尾缀〔句柄分域批 V1〕,载体 `game/probe/ui_struct_probe.gd` _modal_registry_contract 段尾 :606-625) + +| 需求点 | 验收断言(文件:行) | 真实入口 | 结果 | +|---|---|---|---| +| R1 局内冒险域六件抽容器 | ui_struct_probe.gd:610-613(run 六件经 `modals.run.*` 逐件非 null) | 真实实例化主场景 `_ready` 装配→registry 合同段 | 绿 | +| R2 菜单与跨语境域六件抽容器 | ui_struct_probe.gd:614-616(menu 六件逐件非 null) | 同上 | 绿 | +| R3 平铺面清零+编排口升格+登记口迁驻 | ui_struct_probe.gd:617-625(V1b 12 件 get() 逐件 null+V1c `ui.flow` 在座且 modals 无 flow);S13 静态判据 14→3/3→4(gdstyle 闸) | 同上 | 绿 | +| R4 全仓寻址换代行为零变化 | 既有断言群改址照跑:smoke 15 检查/system_test 69 检查/探针族全绿/presentation 17 套件(原判据零改,theme_projection :719 期望串两侧一致换代已申报) | 真实开机路径→真实输入/信号路径 | 绿 | +| R5 登记册合同序不破 | ui_struct_probe ①②③④段(装配生命批 V1)改址照跑绿 | 同上 | 绿 | +| R6 诊断转储接线断言字面换代 | theme_projection_test.gd:719 照跑绿(V6 段) | run_all presentation 套件 | 绿 | + +全仓旧形零残留:`grep -rnE "modals\.flow|modals\.(12 平铺名)" --include=*.gd game` 零命中(唯一命中=ui_struct_probe.gd:624 V1c 断言消息字面,非寻址代码)。 + +### 卡③(断言尾缀〔窗口化批 V1–V8〕,载体 `game/probe/screen_flow_probe.gd` _step_camp_window_shell :298-455) + +| 需求点 | 验收断言(文件:行) | 真实入口 | 结果 | +|---|---|---|---| +| 四窗统一窗壳 720×640 起点居中 | screen_flow_probe.gd:342(V1 四窗窗板 global_rect ≈(280,80,720,640) 容差 0.01) | 选档层进档→场景点选 `object_pressed` 真实信号路由 | 绿 | +| 窗明显小于屏+场景根留底 | :345-349(V2 camp.scene.visible+四边距 ≥80px) | 同上 | 绿 | +| 遮罩 0.6 回归 | :352-357(V3 遮罩 ColorRect alpha≈0.6+全屏) | 同上 | 绿 | +| 各窗关闭钮回归 | :324-331 与 :432-439(V4 四窗「返回营地」/生涯「返回」真实 pressed→窗隐+场景根留底) | 窗内真实 Button 信号 | 绿 | +| 五窗互斥单开回归 | :362-381(V5a 逐窗五窗计数==1+V5b 树窗开态推真实鼠标点零点穿) | 同上+push_input | 绿 | +| ESC 优先序回归 | :388-399(V6 生涯窗开态 Esc 先关窗、浮窗不唤起) | push_input 真实 Esc 键 | 绿 | +| 内容超高窗内滚动 | :405-428(V7 加长行册喂入后窗板 rect 仍等值+内容实高>滚动视口) | `object_pressed(&"dummy")`+`refresh_camp` 真实投喂 | 绿 | +| 生涯页同壳同形态 | :433-451(V8 生涯窗板同几何+「返回」在位+纵向 EXPAND_FILL 滚动面);theme_projection_test 全件照跑绿 | 笔记本点选路由;单元侧 `open_career()` | 绿 | + +shot_probe 窄验:exit 0;shot_career.png 通用判据读数 std=70.2(阈 ≥8)、唯一色数 157(阈 ≥40)、尺寸 1280×800——生涯窗壳形态下双达标,读数记档盯 CI。 + +## 四、实现决定记录(超白名单事项) + +1. **int(null) 构造拒(卡①)**:卡内断言「int(null)=0/bool(null)=false」在本引擎(4.7.2)不成立——`int()` 拒 null 构造(SCRIPT ERROR: Nonexistent 'int' constructor)。三调用点(open_ticket dig_streak 载荷键/rider_tier_bonus/rider_tier_crit 读)改 `or 0`/`or false` 空值合并:合法值域(非负 int/bool)上与「缺省 0/false」逐位等价。撤销方式=还原为直包 int()/bool()(会红)。代价:无(缺省语义逐位同,V1-V3 与全量闸为主证)。 +2. **V8 字符串 Callable 硬零闸(卡①)**:effect_port 组建初版用 `Callable(self, "…")` 六枚——source_guard_test V8(产品码字符串 Callable 零命中,迭代 27 换装闸)红。改方法直引用形态(`"emit_cb": fx_emit_biz`),与 bundle emit_cb/facts_cb 既有接线同型。撤销方式=无(直引用即既有形态)。 +3. **roll() 局部变量预算(卡①)**:迁移后 roll() 12 局部(max 10,gdstyle 基线外新键)——清图选行与区间行查表两处临时内联,randi_range 求值位置不动、取号序与取号次数零变化(round_flow 逐位回放主证)。撤销方式=还原临时变量(会撞闸)。代价:行查表达式三连读(同一 dict 两次下标,无行为差)。 +4. **定尺寸窗禁单行撑宽的标签适配(卡③)**:career hint 与行值标签实测单行 min 宽(hint ≈766px/「各关完成次数」值 742px)把窗板撑到 798——补 `AUTOWRAP_WORD_SMART`+横向 EXPAND_FILL(短值视觉零变,长值窗内换行)。属窗几何契约(720×640 不随内容涨)的伴随适配,非内容重排。撤销方式=去 autowrap(V8 立红)。 +5. **V6/V8 测序前置清窗(卡③测试侧)**:模态栈语义=open 隐上方层+close 恢复——信号直发轮转给 board 层留遮挡账,钮关树窗把 board 复原出双窗态;真实 UI 中生涯入口只在无窗态可达(遮罩挡场景点选,V5b 钉死)。V6/V8 前置改经真实钮逐窗清账(_close_all_windows),不测不可达态。撤销方式=无(测试时序修正,非判据改)。 +6. 其余全部按卡执行(端口注入 Callable 束/bind_port 时点/闭面五键/选行算子返表行/窗板石板皮/WINDOW_SIZE 单点等)——零超卡裁断。 + +## 五、T0 复核结论(卡① F1-F14) + +11 条全符;三处纯引用漂移(不改设计前提,未停卡):F5 掷格面行号成簇漂移+phase/is_mapped 引用互换(成员清单与计数全证实);F11 卡内枚举漏列 feats.tres:633 dig_stop 订阅行 when fact=ap_left——ap_left 由 dig_stop 载荷携带(:178),payload 同名优先解析,事实回调不触,统一闭面零观测差结论经本会话逐行复核成立;F13 gdstyle 基线 5 键实际 :8-12。 + +## 六、实机目检总清单(真人检测只登记不阻塞) + +1. 营地五窗观感与场景可见度样张判定(窗 720×640 起点/遮罩 0.6 观感校准意见)。 +2. 窗内滚动手感(天赋树长列表/生涯页)。 +3. 模态开合走查由既有探针族覆盖(卡②登记句;零新增目检项)。 + +登记面=体验验证清单(归文档批随反哺入册),不阻塞合并。 + +## 七、遗留读数义务与观测面局限 + +- shot_career 通用判据读数(std=70.2/色数 157)随 CI 渲染闸复验盯守。 +- 卡③合同表零改动(红线 1)判据为静态:LAYER_CONTRACT 数据块 diff 零行变更由独立验收审计核对(观测面局限申报,非撤销类取证)。 +- static_scan.sh S8 清单注释「共 10 文件」为入批前存量漂移(实列 13→今 14),未属本批申报面,未顺手改。 + +## 八、产出物与审计 + +- 反哺点清单:`reports/iter49-反哺点-2026-11-02.md`(architecture/camp.md/08-Alpha/术语表/体验验证清单)。 +- 独立验收审计:`reports/iter49-验收审计-2026-11-02.md`(未参与实现的子代理执行;未过不得合并)。 +- 能力清单核对:三卡能力行名与实际落地测试名一致(effect_port_test 三函数实名/ui_struct_probe._modal_registry_contract 实名/screen_flow_probe._step_camp_window_shell 实名)——零回表。 diff --git a/reports/iter49-验收审计-2026-11-02.md b/reports/iter49-验收审计-2026-11-02.md new file mode 100644 index 00000000..84e4039a --- /dev/null +++ b/reports/iter49-验收审计-2026-11-02.md @@ -0,0 +1,146 @@ +# 迭代 49 独立验收审计(2026-11-02) + +> **接收方**:iter49 合并门(合回前独立验收审计——test-conventions §8.7;未过不得合并)。 +> **审计员**:未参与实现的独立子代理(迭代 loop 内启动)。只读审计+最窄抽查跑批,除本报告外零改任何文件。 +> **审计输入**:三卡 v1.1「验证闸(改动—验收对照表)」节、`reports/iter49-实现报告-2026-11-02.md` 实际对照表、分支 diff(`git diff master...iter49`,HEAD=52d87309,48 文件 +1553/−878)、测试文件实况、闸回执 `game/tests/.gates/receipt.json`。 + +## 结论先行 + +**通过=可合并。** 缺漏 0;例外 1(卡① 断言消息批名尾缀形态,依据在案,详见 §一卡① 末行与 §七)。 + +--- + +## 一、逐行核对(三卡对照表 ↔ 实现报告 ↔ 测试实况) + +### 卡① 效果端口与掷格分件批(载体 `game/tests/unit/battle_settlement/effect_port_test.gd`,129 行实读) + +| 对照表行 | 测试实况 | 真实入口核验 | 状态 | +|---|---|---|---| +| R1 端口单实例与真实发射路由 | `_smoke_effect_port_wiring`(:55-83)在库;端口同一性断言 :66;attack_commit 命中链 :68-83(蓄力层预置→攻击行在流→层账 3→0→同种子对照跑 delta≥1) | `BattleOrchestrator.new(_db, rng)`+`begin_battle`+`settle_batch([{"action":"stop"}])` 真实装配链与停手回合——**非裸 EffectEngine 直调**;文件内 EffectEngine 直调仅两处夹具预置/读数(:68/:74,注释注明与 refresh_feats_test V8 同法的调试注入,发射路径全经端口) | 通过 | +| R2 业务类零引擎直连 | `_test_business_classes_engine_free`(:88-94)源码锚直读两业务文件,扫描面非空前置在案(:92) | 测试直读源码(源码契约锚惯例) | 通过 | +| R3 伤害域事实面闭集 | `_test_biz_fact_face_closed`(:99-129)阳性 hp_ratio 载荷 when→承伤减量恰 1(:122);阴性 battle_index→零减量(:129);合成行 effect 段逐字抄现役铁壁行、rebuild 独占订阅面(:104-116) | `DamageController.open_ticket`+`flush_submit` board_flat_hero 真实单据流 | 通过 | +| R4 接线时序与路由等价 | 既有 refresh_feats_test/effect_words_t1_test/adventure_feats_test 照跑,原判据零改(diff 核清) | 真实玩法路径(CI gate 02 全绿) | 通过 | +| R5 掷格件建件与摇号等价 | board_gen_reward_test 照跑(全受理 API) | `begin_battle` install 链 roll_board 恰一次(CI gate 02) | 通过 | +| R6 economy 收纳与键账只降不升 | S13 读数对照在报告 §二(15→11/12→9)+受理面套件照跑 | 既有受理 API(CI gate 09 静态+gate 02 行为) | 通过 | +| R7 装板域与编排域改址 | variant_placement_test 指址换代 18 行 22 处、判据文本与阈值逐字保留(diff 逐行核) | 装板摇号链 | 通过 | +| R8 行为零变化总证 | round_flow_skill_pool_test 134 检查全绿(EV 锚+逐位回放 ×2 关) | 全战斗回放(CI gate 02) | 通过 | +| R9 闸清单随动 | static_scan.sh S7_B1_SET 与 S8_FILES 各增 effect_port.gd+reward_roller.gd、B4 符号清单删 weighted_pick 行;run_all.gd:199 manifest 登记 effect_port_test(expect 0)——diff 逐行核清 | 静态扫描域闸(CI gate 09 green) | 通过 | +| 断言尾缀 | 全 15 条断言消息带 V1/V2/V3 编号前缀;批名全称〔效果端口与掷格分件批 Vn〕未入消息字面——全称在文件头注(:1)与 run_all.gd manifest 注释在案 | — | 例外及依据:卡① §11 代拍板白名单第 8 条明文「断言消息文案——非契约面」,尾缀括注落消息文案内属卡内授权裁量;grep 定位面成立(按 V 号+文件名定位断言、按批名定位批次)。与卡②卡③全称尾缀形态不齐,随本报告知悉合并门;如需严格统一归一行字面修复批,不阻本批合并 | + +### 卡② 模态句柄分域批(载体 `game/probe/ui_struct_probe.gd` `_modal_registry_contract` 段尾) + +| 对照表行 | 测试实况 | 真实入口核验 | 状态 | +|---|---|---|---| +| R1 局内冒险域六件 | V1a(:608-612)run 六件经 `modals.run.*` 逐件非 null | main.tscn 真实实例化 `_ready` 装配→:33 无条件调 `_modal_registry_contract(main)` | 通过 | +| R2 菜单与跨语境域六件 | V1a(:613-615)menu 六件逐件非 null | 同上 | 通过 | +| R3 平铺清零+flow 升格+登记口迁驻 | V1b(:616-619)12 件 `get()` 逐件 null;V1c(:620-624)`ui.flow` 在座且 `modals.get("flow")` 为 null;S13 静态判据(CI gate 09) | 同上 | 通过 | +| R4 全仓寻址换代行为零变化 | 既有断言群改址照跑:smoke(CI 00)/system_test(CI 01)/探针族(CI 03)全绿;本审计自跑 ui_struct_probe+screen_flow_probe 双 PASS | **真实开机路径**(main.tscn 实例化+真实输入/信号) | 通过 | +| R5 登记册合同序不破 | ①②③④段〔装配生命批 V1〕(:561-605)改址照跑——实况寻址已为 `modals.run.*`/`modals.menu.*` 二段式,判据零改 | 同上 | 通过 | +| R6 诊断接线字面换代 | theme_projection_test:719 期望串换代与申报逐字一致(diff 核) | run_all presentation 套件(CI 02) | 通过 | +| 旧形零残留(完成定义 #3) | 全仓 `grep -rnE "modals\.flow|modals\.(12 平铺名)" --include=*.gd game` 唯一命中=ui_struct_probe.gd:624 断言消息字面(非寻址代码)——与报告 §三 陈述一致 | — | 通过 | + +尾缀〔句柄分域批 V1〕grep 到(:612/:615/:619/:622/:624)。 + +### 卡③ 营地五界面真窗口化批(载体 `game/probe/screen_flow_probe.gd` `_step_camp_window_shell`) + +| 对照表行 | 测试实况 | 真实入口核验 | 状态 | +|---|---|---|---| +| 四窗统一窗壳 720×640 起点居中 | V1(:336-342)窗板 `get_global_rect()` ≈(280,80,720,640) 容差 0.01 | `ProbeUiUtil.open_camp_via_savesel` 真实选档钮进档+`object_pressed.emit(k)` 真实信号路由(:303-332) | 通过 | +| 窗明显小于屏+场景根留底 | V2(:344-348)`camp.scene.visible`+窗板四边距视口边 ≥80px | 同上 | 通过 | +| 遮罩 0.6 回归 | V3(:350-356)首子节点 ColorRect alpha≈0.6+全屏尺寸容差判等(禁裸 == 落实) | 同上 | 通过 | +| 各窗关闭钮回归 | V4(:320-329 轮转间+:445-451 生涯+:457-467 清窗)真实 Button `pressed.emit()`→窗隐且场景根留底 | 窗内真实钮信号 | 通过 | +| 五窗互斥单开回归 | V5a(:357-363)五窗集 visible 计数==1;V5b(:364-381)`get_hotspot_rect(&"forge")` 公开读面+`push_input` 真实鼠标点→零点穿零新窗 | 同上+真实输入事件 | 通过 | +| ESC 优先序回归 | V6(:385-396)`push_input` 真实 Esc 键→生涯窗隐、浮窗不唤起 | 真实键盘事件 | 通过 | +| 内容超高窗内滚动 | V7(:397-419)真实投影口 `refresh_camp` 喂 30 行合成行→窗板 rect 仍等值+内容实高>滚动视口 | `object_pressed(&"dummy")`+真实数据投喂 | 通过 | +| 生涯页同壳同形态 | V8(:422-444)窗板同几何+「返回」钮在位+纵向 EXPAND_FILL 滚动面;theme_projection_test 全件照跑(CI 02) | 笔记本 `object_pressed(&"notebook")` 路由;单元侧 `open_career()` | 通过 | +| 契约 1/8/9 落码 | `WINDOW_SIZE := Vector2(720, 640)` 单点常量(camp_sub_view_base.gd:11,career 经类常量引用——grep 全库禁二处字面值成立);窗板 `panel_skinned(Vector4(14,12,14,12))` 石板皮;生涯内容板 BG_BRIGHT+顶行墨 INK_TITLE 订正(camp_sub_view_base.gd/career_overlay.gd diff 逐行核) | — | 通过 | +| 合同表零改动(红线 1) | `git diff master...iter49 -- game/presentation/ui_registry.gd`:**LAYER_CONTRACT 数据块零行变更**(diff 仅头注、flow 字段、register_layer 迁驻、node() 十二键、apply_layer_contract 三调用点) | — | 通过 | + +尾缀〔窗口化批 V1–V8〕grep 到(V1 :305/336/342;V2 :345/348;V3 :352/356;V4 :324/329/451/464;V5 :363/369/381;V6 :389/394/396;V7 :401/415/419;V8 :426/430/435/437/444)。 + +--- + +## 二、抽查跑批读数(本审计自跑,工作目录 D:\code\g-plan2-iter49\game,不采信报告「已全绿」) + +**a) effect_port_test**:`…console.exe --headless --path . --script res://tests/unit/battle_settlement/effect_port_test.gd` + +``` +[PASS] V1 端口单实例(两业务类持同一 EffectPort) +[PASS] V1 delta 读数非零(蓄力层加算进入主角攻击基值) +[PASS] V2 全文零引擎直连 token(EffectEngine/EffectContext):res://core/damage_controller.gd +[PASS] V2 全文零引擎直连 token(EffectEngine/EffectContext):res://core/action_economy.gd +[PASS] V3 阳性对照:hp_ratio 载荷 when 通过——承伤减量恰 1(10→9) +[PASS] V3 阴性对照:battle_index 不答——零减量(10→10,事实面未扩) +[effect_port_test] all green (15 checks) +EXIT=0 +``` + +15 检查全 PASS、退出 0——与期望一致。 + +**b) screen_flow_probe**:`…console.exe --headless --path . res://probe/screen_flow_probe.tscn` + +``` +[screen_flow_probe] PASS — 六步主流程+回退路径全绿(屏级互斥:任意时刻 ≤1 屏可见) +EXIT=0 +SCRIPT ERROR 计数=0(全日志 grep) +``` + +winshell 段执行证明:该段在 `_ready` 固定序列 :31 无条件 await(Esc 段之后、选档段之前);探针机制=problems 空才打 PASS、非空打 FAIL 并逐行打印——winshell 段任一断言失败或前置早退都会以〔窗口化批 Vn〕标记行现形。PASS+退出 0+零 SCRIPT ERROR ⇒ V1–V8 全部真实执行且零红。 + +**c) ui_struct_probe**:`…console.exe --headless --path . res://probe/ui_struct_probe.tscn` + +``` +[ui_struct_probe] PASS — 全屏结构断言零红 +EXIT=0 +SCRIPT ERROR 计数=0 +``` + +`_modal_registry_contract` 于 :33 无条件调用;同机制证 V1a/V1b/V1c 三判据全执行零红。 + +**CI 回执**(`game/tests/.gates/receipt.json`):commit_short=1f0a8bd3、timestamp=2026-11-02T07:23:41+08:00、domain=code、scope=full、quick_mode=false、`all_green=true`——00 冒烟/01 系统测试/02 单元与集成/03 场景探针/05 渲染闸/09 静态扫描/10 熵增闸 七闸全 exit 0 green。 + +--- + +## 三、测试削弱核对(申报 ↔ diff 逐行) + +| 文件 | 卡申报 | diff 实况 | 状态 | +|---|---|---|---| +| variant_placement_test.gd | 卡① 只申报白盒指址换代(判据零删改零放宽) | 18 行增删全为 `eco.*`→`eco.roller.*`/`woc._reward_economy.*`→`…roller.*`,判据文本与阈值逐字同(含 wr10② Callable 同一性比较、wr10③ 权重面),实测 18 行 22 处与报告登记一致(卡载「约 17 行 21 处」为写卡估数、T0 实数登记条款覆盖) | 通过 | +| theme_projection_test.gd | 卡② 申报 :719 期望串+寻址字面 | 唯一改动=:719 期望串 `ui.modals.debug_overlay.`→`ui.modals.menu.debug_overlay.`,判据语义不变 | 通过 | +| system_test.gd/smoke_boot.gd/pacing_queue_test.gd | 卡② 申报寻址字面换代 | 全部为 `modals.X`→`modals.run./menu.X` 二段式,check 文本逐字同 | 通过 | +| 探针 12 件(fx_shot/layout/probe_ui_util/screen_flow/shot/t17_defer/ui_click/ui_input/ui_probe/ui_struct/ui_tune) | 卡② 申报寻址换代+ui_struct 新段;卡③ 申报 screen_flow 纯增段 | 全探针 diff 删除行中断言类仅 5 行,逐行有 1:1 新增对应(判据文本同、仅址字面换);新增段为纯增面 | 通过 | +| 卡③ 其余测试面 | 申报零删改零放宽 | 未发现任何删断言/放宽阈值/缩小扫描面/换下层直调 | 通过 | + +**未申报削弱:零。** 全 diff 删除行另核:reward_economy 侧退役的 4 assert+1 fatal 守卫全数逐字承接于 reward_roller.gd(:119/:150/:184/:194/:200,weighted_pick 改名选行算子为卡① 白名单 #5 授权),零守卫丢失。 + +--- + +## 四、内部字段直查受控例外与单点核对 + +- effect_port_test.gd:62-66 端口同一性直查 `orch_c.fx_host().damage._port`/`orch_c.action_economy._port`——行旁注释载理由(端口同一性无公开查询面);卡① 对照表行 1 与实现报告卡① R1 行均注明同一理由。**例外成立**。 +- 单向接线:effect_port.gd 零 BattleFxHost 对象引用(仅头注纪律声明两处,非代码引用);battle_fx_host.gd :54-63 端口组建后 bind_port 转注两业务类、端口不落 host 成员——与卡契约 4 一致。 +- damage_controller.gd/action_economy.gd 全文零 `EffectEngine`/`EffectContext` token(grep 0 命中;V2 断言当轮 PASS 双证)。 +- 卡③ 合同表零改动:见 §一卡③ 末行——LAYER_CONTRACT 数据块 diff 零行变更。 + +--- + +## 五、实现决定记录复核(报告 §四 五项,均有等价论证与撤销方式) + +| 决定 | 复核 | 状态 | +|---|---|---| +| 1 int(null) 构造拒→`or 0`/`or false` | 卡内 `int(…)/bool(…)` 包裹写法在本引擎(4.7.2)不可行;合法值域(非负 int/bool)上与缺省 0/false 逐位等价;V1-V3+全量闸承载 | 通过(已备案) | +| 2 字符串 Callable→方法直引用 | source_guard_test V8 闸约束(产品码字符串 Callable 零命中);与既有 bundle 接线同型 | 通过 | +| 3 roll() 局部变量内联收敛 | gdstyle 预算约束;取号序与次数零变化由 round_flow 逐位回放主证 | 通过 | +| 4 career 标签 autowrap+横向 EXPAND_FILL | 窗几何契约(720×640 不随内容涨)的伴随适配;V8 钉窗板 rect 兜底 | 通过 | +| 5 V6/V8 前置经真实钮清窗 | 测试时序修正(真实 UI 不可达态不测);V6/V8 断言本体与判据零改 | 通过 | + +其余按卡执行(报告 §四 第 6 项「零超卡裁断」与本审计 diff 核对结论一致)。 + +--- + +## 六、总结论 + +**通过=可合并。** + +- 缺漏:**0**。 +- 例外:**1**——卡① 断言消息未带批名全称尾缀〔效果端口与掷格分件批 Vn〕(V1/V2/V3 编号前缀 15/15 全带;批名全称在文件头注与 run_all.gd manifest 在案)。依据=卡① §11 白名单第 8 条「断言消息文案——非契约面」;验收实质(测试存在/真实入口/当轮执行全绿/判据绑定面/零削弱)全部成立,不阻合并。合并门如需三卡尾缀形态严格统一,归一行字面修复批另行走批,不属本批缺漏。