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 8e5fb8be..00000000 Binary files a/game/tests/golden/palette_baseline/art_snapshot.png and /dev/null differ diff --git a/game/tests/golden/palette_baseline/ui_tokens.png b/game/tests/golden/palette_baseline/ui_tokens.png deleted file mode 100644 index adbd7fcd..00000000 Binary files a/game/tests/golden/palette_baseline/ui_tokens.png and /dev/null differ 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 批量统一覆盖各条对比需求:全冒险期望+奖励期望),分散登记徒增维护面。逐条挂账明细、裁定史与相关文档见汇总文档。