diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 3136eaaa..138fe7e3 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -36,10 +36,10 @@ const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", " var db: ConfigDb var rng: RngServiceCore -# E1 封装收口:协作对象 board_generator/enemy_system/status_effects/reward_economy 私有化——跨层读取一律经 presentation_snapshot()/diagnostic_snapshot() 成文出口。 +# E1 封装收口:协作对象 enemy_system/status_effects/reward_economy 私有化——跨层读取一律经 presentation_snapshot()/diagnostic_snapshot() 成文出口。 # minesweeper/action_economy 保持公有 = 架构文档 API Boundaries 的 HUD框架消费面(意图提交 settle_batch + compute_move_path # 只读;ap/rap/相位读取)——非本文档登记的契约面,勿增新用途。 -var _board_generator: BoardGenerator +# 装板域(BoardGenerator+装板六函数族)已拆 BoardInstaller——生成器实例随件自建,宿主零成员。 var minesweeper: MinesweeperRules var action_economy: ActionEconomy var _enemy_system: UnitContainer @@ -230,13 +230,12 @@ var _hero_atk := 0 # 主角攻击基值快照(encounte func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: db = db_ref rng = rng_ref - _board_generator = BoardGenerator.new() action_economy = ActionEconomy.new(db) minesweeper = MinesweeperRules.new(db, action_economy) _enemy_system = UnitContainer.new(db) _status_effects = StatusContainer.new(db) _reward_economy = RewardEconomy.new(db, rng) - _board_generator.fatal_handler = _fatal; _reward_economy.fatal_handler = _fatal # 协作件违约→本类 _fatal 封印(单点接线) + _reward_economy.fatal_handler = _fatal # 协作件违约→本类 _fatal 封印(单点接线) _damage = DamageController.new(db, rng, _enemy_system, _status_effects) _damage.emit_row_cb = _emit_row # 行协议归属本类(9 类行协议 + 来源断言不外泄) _fx_host = BattleFxHost.new({"db": db, "rng": rng, "minesweeper": minesweeper, "action_economy": action_economy, @@ -268,7 +267,7 @@ func begin_battle(enc: Dictionary) -> bool: return false if not _begin_battle_accept(enc): return false - if not _install_new_board(): + if not _install_and_reset(): return false _reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险) _begin_battle_fx_state(enc) @@ -411,34 +410,14 @@ func _begin_battle_preview_economy(enc: Dictionary) -> void: action_economy.grant_reserve_ap(voucher_rap) -func _install_new_board(during_round := false) -> bool: - var stream: RandomNumberGenerator = rng.open_stream("board_gen") - var board: BoardData = _board_generator.generate(int(encounter.level_index), int(encounter.node_depth), stream, db) - if board == null: +# ---- 装板受理薄壳(BoardInstaller 拆件后的宿主承接面——begin_battle/中途刷新/轮内刷新三调用点唯一入口)---- +# 件内装板(生成→全局变异决议→装板受理→资源摇号→变异布置)成功后执行宿主态复位:五处计数清零+ +# 板序推进+board_changed 发射+秘宝叠加层退役——时序逐字;复位职责留宿主(成员单点),不随件。 +func _install_and_reset(during_round := false) -> bool: + var installer := BoardInstaller.new({"db": db, "rng": rng, "minesweeper": minesweeper, + "reward_economy": _reward_economy, "encounter": encounter, "fatal_cb": _fatal}) + if not installer.install(during_round): return false - # 全局作用域变异决议(装板序内前置于 roll_board:图级偏斜旋钮须在资源摇号前定型——详见 _resolve_global_variant) - _resolve_global_variant(board) - var res: Dictionary[Variant, Variant] = minesweeper.install_board(board, during_round) - if not res.accepted: - _fatal("排雷玩法 install 拒绝(不变式违例): %s" % str(res)) - return false - # 地图战斗资源 义务⑥:roll_board 恰一次(install 后、挖掘前);资格格 = 全部非封锁格(安全格 + 危险格,雷安同池) - var r0 := {} - for c in board.compute_r0(): - r0[c] = true - var eligible: Array[Dictionary] = [] - for c in board.safe_cells(): - if not r0.has(c): - eligible.append({"cell": c}) - for c in board.mines.keys(): - if not r0.has(c): - eligible.append({"cell": c}) - if not _reward_economy.roll_board(eligible): - _fatal("地图战斗资源 roll_board 被拒") - return false - # 地形变异 布置段(棋盘设置顺序第 4 步:生成棋盘 → 打开状态初始化 → - # 战斗资源格设置 → 变异格设置)——布置在初始展开与资源格之后,变异天然不落已开区 - _place_variants(board) _seal_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8) _seal_miss_seen.clear() _perfect_hit_true = 0 # 整图完美计数随 install 重置 @@ -452,103 +431,6 @@ func _install_new_board(during_round := false) -> bool: return true -# ---- 全局作用域变异 决议(装板序内 roll_board 之前)---- -# boss/elite 图命中全局型:写整图 board_variants(恰一行)+ 注入资源面偏斜旋钮(图级投影在 -# RewardEconomy.roll_board 消费);不消费 variant_place 流(全局取用确定性——零取号合法态)。 -# 全局池空(含 L1 Boss 免疫/unlock 门控未达):字段留空、旋钮归零——_place_variants 原路走 -# region 布置 = 兜底回落合法态(防配置缺行静默裸奔——provisional)。 -func _resolve_global_variant(board: BoardData) -> void: - board.board_variants.clear() - _reward_economy.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)) - - -# ---- 地形变异 布置段(棋盘设置顺序第 4 步)---- -# 候选池门控(unlock_level ≤ 关卡序号 + board_types 与本地形交集,∅ 通配)经 config_db.variant_pool; -# 池空 = 合法态,静默零布置(框架 P0 口径不变)。零特判:本段只写格子变异数据快照,不改扫雷规则本体。 -# 候选格域 = 安全格 − 初始展开区(伤害型 placement=closed_only 的结构保障——布置发生在初始展开之后, -# 已开格天然排除);危险格不吃变异(挖雷一层惩罚 + 变异伤害双层叠罚污染逐行归因,首版排除——provisional)。 -# 每图至多一种类型(候选池内均匀选一);数量 = round(候选池格数×percent) + -# (关卡序号−1)×per_level_bonus——基数为候选池而非总格数(percent 语义 = 占可挖格比例, -# 关卡递增由 per_level_bonus 绝对格数承载);连通片生长(8 邻),单片 ≤ pack_max。 -# 替换制:整图 board_variants 非空 = 本图已带全局型,跳过 region 布置(详见 _resolve_global_variant)。 -func _place_variants(board: BoardData) -> void: - if not board.board_variants.is_empty(): - return - var pool: Array[Variant] = db.combat.variation.variant_pool(int(encounter.level_index), String(board.board_type)) - if pool.is_empty(): - return - var r0 := {} - for c in board.compute_r0(): - r0[c] = true - var candidates: Array[Vector2i] = [] - for c in board.safe_cells(): - if not r0.has(c): - candidates.append(c) - if candidates.is_empty(): - return - var stream: RandomNumberGenerator = rng.open_stream("variant_place") # 一板恰一 scope(换图/刷新图各取号一次) - var vrow: Dictionary[Variant, Variant] = pool[stream.randi_range(0, pool.size() - 1)] - var total: int = int(round(float(candidates.size()) * float(db.combat.variation.percent_of_cells) / 100.0)) \ - + (int(encounter.level_index) - 1) * int(db.combat.variation.per_level_bonus) - total = mini(total, candidates.size()) - _place_variants_apply(board, candidates, stream, vrow, total) - - -# 变异片生长施加面(布置域=候选格,数量=total,vrow 单型贯穿;取号序=stream 顺序消费) -func _place_variants_apply(board: BoardData, candidates: Array[Vector2i], stream: RandomNumberGenerator, vrow: Dictionary, total: int) -> void: - var placed := {} - while placed.size() < total: - var pack_size: int = mini(total - placed.size(), stream.randi_range(2, int(db.combat.variation.pack_max))) - var free: Array[Vector2i] = [] - for c in candidates: - # 新片种子/生长格禁与其他已布片 8 邻贴接——否则两片粘连成一个超 pack_max 的连通分量 - # (R3 随机流下实测暴露:独立生长的相邻片合流击穿单片上限断言) - if placed.has(c) or _adjacent_to_packs(board, placed, c): - continue - free.append(c) - if free.is_empty(): - break # 生长域耗尽——该片就地收束(数量不足总目标为合法态,候选域碎片化时触发) - var seed_c: Vector2i = free[stream.randi_range(0, free.size() - 1)] - placed[seed_c] = true - var pack: Array[Variant] = [seed_c] - var pack_set := {seed_c: true} - while pack.size() < pack_size: - var fringe := _variants_fringe(board, pack, candidates, placed, pack_set) - if fringe.is_empty(): - break # 生长域耗尽——该片就地收束(数量不足总目标为合法态,候选域碎片化时触发) - var nxt: Vector2i = fringe[stream.randi_range(0, fringe.size() - 1)] - placed[nxt] = true - pack_set[nxt] = true - pack.append(nxt) - for c in placed.keys(): - board.variants[c] = [String(vrow.variant_id)] - - -# 片生长待选格收集(本片内各生长格的 8 邻候选并集——未布置、未在途、不与他片贴接) -func _variants_fringe(board: BoardData, pack: Array, candidates: Array[Vector2i], placed: Dictionary, pack_set: Dictionary) -> Array[Vector2i]: - var fringe: Array[Vector2i] = [] - for c in pack: - for n in board.neighbors8(c): - if candidates.has(n) and not placed.has(n) and not fringe.has(n) \ - and not _adjacent_to_packs(board, placed, n, pack_set): - fringe.append(n) - return fringe - - -# 候选格是否与其他已布片(own_pack 之外的 placed 格)8 邻贴接——片间隔离保证连通分量 = 单片 ≤ pack_max -func _adjacent_to_packs(board: BoardData, placed: Dictionary, c: Vector2i, own_pack: Dictionary = {}) -> bool: - for m in board.neighbors8(c): - if placed.has(m) and not own_pack.has(m): - return true - return false - - # ---- 事件缓冲(轮 facts 收集;结算触发 = Waiting 相位编排点,不依赖到达序)---- func _on_cell_revealed(evt: Dictionary) -> void: @@ -798,7 +680,7 @@ func _exit_distribution() -> void: if mode == "new_board": _reward_economy.retire_mapping() minesweeper.retire_board() - if not _install_new_board(): + if not _install_and_reset(): _fatal("换图编排失败") return # 工兵晨令续队直调拆除(内容数据驱动扩展批 T4——round_start 订阅行承载,见 begin_battle 注) @@ -832,7 +714,7 @@ func player_request_refresh() -> bool: _refresh_used_this_round += 1 _reward_economy.retire_mapping() # 旧图批次退役(地图战斗资源 Ready→retired;E6 新批重置前提) minesweeper.retire_board(true) # 旧板退役(轮内通道) - return _install_new_board(true) # 当场换图——失败即编排违例 fail-fast + return _install_and_reset(true) # 当场换图——失败即编排违例 fail-fast func _battle_over(result: String) -> void: diff --git a/game/core/board_installer.gd b/game/core/board_installer.gd new file mode 100644 index 00000000..96f7d921 --- /dev/null +++ b/game/core/board_installer.gd @@ -0,0 +1,167 @@ +## 棋盘安装与变异布置域件(B1 域·BoardInstaller):生成受理、全局作用域变异决议、装板受理、 +## 资源摇号资格域组装(roll_board 恰一次——install 后、挖掘前)、region 变异决议与连通片布置。 +## 棋盘设置顺序 = 生成棋盘 → 打开状态初始化 → 战斗资源格设置 → 变异格设置(布置在初始展开与 +## 资源格之后,变异天然不落已开区)。宿主侧装板后状态复位(封印/完美计数清零、板序推进、 +## board_changed 发射、秘宝叠加层退役)留宿主薄壳——本件零复位职责。零 UI 零 await; +## 随机只经注入 RngServiceCore 的 board_gen/variant_place 两封闭流(流名与开流时点不变, +## 生成器只消费传入流——同种子同输入逐位复现)。 +class_name BoardInstaller +extends RefCounted + +var db: ConfigDb +var rng: RngServiceCore +var minesweeper: MinesweeperRules +var reward_economy: RewardEconomy +var encounter: Dictionary[String, Variant] # 宿主 encounter 同对象引用(assign 原地变异——跨战活引用) +var fatal_cb: Callable # 协作件违约→宿主 _fatal 封印(单点接线——BoardGenerator 同款) +var generator: BoardGenerator # 件内自建自接线(原宿主 _board_generator 成员随件迁移) + + +func _init(bundle: Dictionary) -> void: + for k in ["db", "rng", "minesweeper", "reward_economy", "encounter", "fatal_cb"]: + assert(bundle.has(k), "[装板域件] 引用束缺键: %s" % k) + db = bundle.db + rng = bundle.rng + minesweeper = bundle.minesweeper + reward_economy = bundle.reward_economy + encounter = bundle.encounter + fatal_cb = bundle.fatal_cb + generator = BoardGenerator.new() + generator.fatal_handler = fatal_cb # 协作件违约→宿主 _fatal 封印(单点接线) + + +func _fatal(msg: String) -> void: + if fatal_cb.is_valid(): + fatal_cb.call(msg) # 组装层注入 orchestrator._fatal(封印传导,不绕第二中止通道) + else: + push_error(msg) + assert(false, msg) + + +# 装板单入口(生成 → 全局变异决议 → 装板受理 → 资源摇号 → 变异布置); +# 宿主态复位归宿主薄壳(_install_and_reset)——本件返回即装板完成、复位未行。 +func install(during_round := false) -> bool: + var stream: RandomNumberGenerator = rng.open_stream("board_gen") + var board: BoardData = generator.generate(int(encounter.level_index), int(encounter.node_depth), stream, db) + if board == null: + return false + # 全局作用域变异决议(装板序内前置于 roll_board:图级偏斜旋钮须在资源摇号前定型——详见 _resolve_global_variant) + _resolve_global_variant(board) + var res: Dictionary[Variant, Variant] = minesweeper.install_board(board, during_round) + if not res.accepted: + _fatal("排雷玩法 install 拒绝(不变式违例): %s" % str(res)) + return false + # 地图战斗资源 义务⑥:roll_board 恰一次(install 后、挖掘前);资格格 = 全部非封锁格(安全格 + 危险格,雷安同池) + var r0 := {} + for c in board.compute_r0(): + r0[c] = true + var eligible: Array[Dictionary] = [] + for c in board.safe_cells(): + if not r0.has(c): + eligible.append({"cell": c}) + for c in board.mines.keys(): + if not r0.has(c): + eligible.append({"cell": c}) + if not reward_economy.roll_board(eligible): + _fatal("地图战斗资源 roll_board 被拒") + return false + # 地形变异 布置段(棋盘设置顺序第 4 步)——布置在初始展开与资源格之后,变异天然不落已开区 + _place_variants(board) + return true + + +# ---- 全局作用域变异 决议(装板序内 roll_board 之前)---- +# boss/elite 图命中全局型:写整图 board_variants(恰一行)+ 注入资源面偏斜旋钮(图级投影在 +# RewardEconomy.roll_board 消费);不消费 variant_place 流(全局取用确定性——零取号合法态)。 +# 全局池空(含 L1 Boss 免疫/unlock 门控未达):字段留空、旋钮归零——_place_variants 原路走 +# region 布置 = 兜底回落合法态(防配置缺行静默裸奔——provisional)。 +func _resolve_global_variant(board: BoardData) -> void: + board.board_variants.clear() + reward_economy.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)) + + +# ---- 地形变异 布置段(棋盘设置顺序第 4 步)---- +# 候选池门控(unlock_level ≤ 关卡序号 + board_types 与本地形交集,∅ 通配)经 config_db.variant_pool; +# 池空 = 合法态,静默零布置(框架 P0 口径不变)。零特判:本段只写格子变异数据快照,不改扫雷规则本体。 +# 候选格域 = 安全格 − 初始展开区(伤害型 placement=closed_only 的结构保障——布置发生在初始展开之后, +# 已开格天然排除);危险格不吃变异(挖雷一层惩罚 + 变异伤害双层叠罚污染逐行归因,首版排除——provisional)。 +# 每图至多一种类型(候选池内均匀选一);数量 = round(候选池格数×percent) + +# (关卡序号−1)×per_level_bonus——基数为候选池而非总格数(percent 语义 = 占可挖格比例, +# 关卡递增由 per_level_bonus 绝对格数承载);连通片生长(8 邻),单片 ≤ pack_max。 +# 替换制:整图 board_variants 非空 = 本图已带全局型,跳过 region 布置(详见 _resolve_global_variant)。 +func _place_variants(board: BoardData) -> void: + if not board.board_variants.is_empty(): + return + var pool: Array[Variant] = db.combat.variation.variant_pool(int(encounter.level_index), String(board.board_type)) + if pool.is_empty(): + return + var r0 := {} + for c in board.compute_r0(): + r0[c] = true + var candidates: Array[Vector2i] = [] + for c in board.safe_cells(): + if not r0.has(c): + candidates.append(c) + if candidates.is_empty(): + return + var stream: RandomNumberGenerator = rng.open_stream("variant_place") # 一板恰一 scope(换图/刷新图各取号一次) + var vrow: Dictionary[Variant, Variant] = pool[stream.randi_range(0, pool.size() - 1)] + var total: int = int(round(float(candidates.size()) * float(db.combat.variation.percent_of_cells) / 100.0)) \ + + (int(encounter.level_index) - 1) * int(db.combat.variation.per_level_bonus) + total = mini(total, candidates.size()) + _place_variants_apply(board, candidates, stream, vrow, total) + + +# 变异片生长施加面(布置域=候选格,数量=total,vrow 单型贯穿;取号序=stream 顺序消费) +func _place_variants_apply(board: BoardData, candidates: Array[Vector2i], stream: RandomNumberGenerator, vrow: Dictionary, total: int) -> void: + var placed := {} + while placed.size() < total: + var pack_size: int = mini(total - placed.size(), stream.randi_range(2, int(db.combat.variation.pack_max))) + var free: Array[Vector2i] = [] + for c in candidates: + # 新片种子/生长格禁与其他已布片 8 邻贴接——否则两片粘连成一个超 pack_max 的连通分量 + # (R3 随机流下实测暴露:独立生长的相邻片合流击穿单片上限断言) + if placed.has(c) or _adjacent_to_packs(board, placed, c): + continue + free.append(c) + if free.is_empty(): + break # 生长域耗尽——该片就地收束(数量不足总目标为合法态,候选域碎片化时触发) + var seed_c: Vector2i = free[stream.randi_range(0, free.size() - 1)] + placed[seed_c] = true + var pack: Array[Variant] = [seed_c] + var pack_set := {seed_c: true} + while pack.size() < pack_size: + var fringe := _variants_fringe(board, pack, candidates, placed, pack_set) + if fringe.is_empty(): + break # 生长域耗尽——该片就地收束(数量不足总目标为合法态,候选域碎片化时触发) + var nxt: Vector2i = fringe[stream.randi_range(0, fringe.size() - 1)] + placed[nxt] = true + pack_set[nxt] = true + pack.append(nxt) + for c in placed.keys(): + board.variants[c] = [String(vrow.variant_id)] + + +# 片生长待选格收集(本片内各生长格的 8 邻候选并集——未布置、未在途、不与他片贴接) +func _variants_fringe(board: BoardData, pack: Array, candidates: Array[Vector2i], placed: Dictionary, pack_set: Dictionary) -> Array[Vector2i]: + var fringe: Array[Vector2i] = [] + for c in pack: + for n in board.neighbors8(c): + if candidates.has(n) and not placed.has(n) and not fringe.has(n) \ + and not _adjacent_to_packs(board, placed, n, pack_set): + fringe.append(n) + return fringe + + +# 候选格是否与其他已布片(own_pack 之外的 placed 格)8 邻贴接——片间隔离保证连通分量 = 单片 ≤ pack_max +func _adjacent_to_packs(board: BoardData, placed: Dictionary, c: Vector2i, own_pack: Dictionary = {}) -> bool: + for m in board.neighbors8(c): + if placed.has(m) and not own_pack.has(m): + return true + return false diff --git a/game/core/board_installer.gd.uid b/game/core/board_installer.gd.uid new file mode 100644 index 00000000..7f2e6b17 --- /dev/null +++ b/game/core/board_installer.gd.uid @@ -0,0 +1 @@ +uid://di6u170vqf8ae diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index 9469a2f0..05d110d5 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -41,7 +41,7 @@ var hero_hp: int: var gp: int: # GP 冒险钱包(局内货币——冒险开始 = 0、收束清零;不入存档不入摘要) get: return _wallet.gp set(v): _wallet.gp = v -var _shop: ShopSession = null # 商店件(_init 建;双模式货架/定价/购买/刷新唯一宿主) +var shop: ShopSession = null # 商店件(_init 建;双模式货架/定价/购买/刷新唯一宿主) # 协作对象私有(E1/E3 封装收口):settlement 读取经 current_battle() 成文出口 var _settlement: BattleOrchestrator = null # 当前战斗编排器(关卡结束置 null) var treasure_inventory: TreasureItems = null # 秘宝 关卡域库存(TreasureItems;跨战斗延续, @@ -96,10 +96,10 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore, ctx: Dictionary = {}) -> v _hero_armor_type = String(ctx.get("hero_armor_type", "")) _voucher_rap = maxi(0, int(ctx.get("voucher_rap", 0))) _voucher_time_sand = maxi(0, int(ctx.get("voucher_time_sand", 0))) - _shop = ShopSession.new(db, rng) + shop = ShopSession.new(db, rng) var pool: Dictionary[Variant, Variant] = ctx.get("pool_wiring", {}) as Dictionary if String(ctx.get("shop_mode", "")) == "pool" and not pool.is_empty(): - _shop.set_shop_pool(pool.get("deck", []) as Array, pool.get("consume", Callable()) as Callable, + shop.set_shop_pool(pool.get("deck", []) as Array, pool.get("consume", Callable()) as Callable, pool.get("ticket", Callable()) as Callable, pool.get("cap", Callable()) as Callable) _bind_shop() @@ -302,8 +302,8 @@ func breakpoint_fragment() -> Dictionary[Variant, Variant]: "node_id": current_node_id(), "visited_ids": visited_ids(), "aux_items": treasure_snapshot, - "shop_shelf": _shop.shop_bp_shelf(), - "shop_visit_buys": _shop.shop_bp_visit_buys(), + "shop_shelf": shop.shop_bp_shelf(), + "shop_visit_buys": shop.shop_bp_visit_buys(), } @@ -503,11 +503,11 @@ func _enter_node(node: Dictionary) -> bool: "shop": # 商店房兑付:到达即开店(覆盖层归呈现层),购买出账/ # 离开归玩家意图——零自动推进;离开确认经 continue_after_node 回选路口 - if _shop.pool_mode: - _shop.open_pool() # 进店抽 3(恰一 shop_draw scope;买后全重抽同签) - node_entered.emit(node, {"gp": gp, "shelf": true}) - return true - node_entered.emit(node, {"gp": gp}) # legacy 固定三件(off 对照承载——零 RNG 零池消费) + var payload := {"gp": gp} # legacy 固定三件(off 对照承载——零 RNG 零池消费) + if shop.pool_mode: + shop.open_pool() # 进店抽 3(恰一 shop_draw scope;买后全重抽同签) + payload = {"gp": gp, "shelf": true} + node_entered.emit(node, payload) return true push_error("[关卡节点地图] 未知节点类型: %s" % str(node)) return false @@ -701,7 +701,7 @@ func continue_after_node() -> bool: if t != "reward" and t != "rest" and t != "shop": return false if t == "shop": - _shop.leave() # 离开店面货架态清零(断点快照 shop_shelf 键随离开落空架合法态) + shop.leave() # 离开店面货架态清零(断点快照 shop_shelf 键随离开落空架合法态) return _present_choices() @@ -721,12 +721,12 @@ func close_wallet() -> void: # ---- 商店房兑付(GP+商店房实施批:呈现层只读投影 + 购买出账)---- -# 实装归 ShopSession 件(双模式货架/定价/购买/刷新唯一宿主);本类保留同签名委托与件注入/ -# 白盒入口——外部调用面与断点取数面零变化。 +# 实装归 ShopSession 件(双模式货架/定价/购买/刷新唯一宿主);外部调用面=调用方经宿主公开 +# 成员 shop 直连(调用直连不留委托壳——2026-10-26 Middle Man 裁定)。 # 件注入(建件与宿主引用换代时调用——钱包/主角账三件/图行/装配集为共享引用,出账直接落同一引用) func _bind_shop() -> void: - _shop.bind({ + shop.bind({ "wallet": _wallet, "journal": _journal, "level_row": level_row, @@ -746,83 +746,3 @@ func _emit_wallet_event(event: String, gp_now: int, extra: Dictionary) -> void: # 购血(商店 max_hp 商品——当前生命 +Δ 并封顶新上限,与账本置满/关卡完成回血同式) func _gain_max_hp_flat(flat: int) -> void: _stats.hero_hp = mini(_stats.hero_hp + flat, _stats.effective_max_hp()) - - -func shop_products() -> Array: - return _shop.shop_products() - - -func current_price(prow: Dictionary) -> int: - return _shop.current_price(prow) - - -func purchased_tier(product_id: String) -> int: - return _shop.purchased_tier(product_id) - - -func buy_product(product_id: String) -> bool: - return _shop.buy_product(product_id) - - -func player_request_refresh() -> bool: - return _shop.player_request_refresh() - - -# 散折价单点(测试取数面沿用本类调用名) -func _spend_discounted(price: int) -> int: - return _shop._spend_discounted(price) - - -# 注入(AdventureRunner._start_current_level 唯一调用方——pool 模式专用):令牌库共享引用 + -# 池消费 handler + 专长凭证判定 + cap 检查回调(cap 满 kind 剔出货架候选视图)。 -func set_shop_pool(deck_ref: Array, consume_handler: Callable, ticket_consider: Callable = Callable(), cap_check: Callable = Callable()) -> void: - _shop.set_shop_pool(deck_ref, consume_handler, ticket_consider, cap_check) - - -# 货架抽签白盒入口(测试取数面沿用本类调用名) -func _shop_draw_shelf() -> void: - _shop._draw_shelf() - - -func pool_stock(kind: String) -> int: - return _shop.pool_stock(kind) - - -func pool_price(kind: String) -> int: - return _shop.pool_price(kind) - - -func shop_shelf() -> Array: - return _shop.shop_shelf() - - -func shop_refresh_free_left() -> int: - return _shop.shop_refresh_free_left() - - -func shop_refresh_cost() -> int: - return _shop.shop_refresh_cost() - - -func shop_refresh() -> bool: - return _shop.shop_refresh() - - -func spend_gp(amount: int) -> bool: - return _shop.spend_gp(amount) - - -func buy_pool_item(kind: String) -> bool: - return _shop.buy_pool_item(kind) - - -func is_shop_pool() -> bool: - return _shop.is_shop_pool() - - -func shop_bp_shelf() -> Array: - return _shop.shop_bp_shelf() - - -func shop_bp_visit_buys() -> int: - return _shop.shop_bp_visit_buys() diff --git a/game/core/shop_session.gd b/game/core/shop_session.gd index 531610d6..be90aa47 100644 --- a/game/core/shop_session.gd +++ b/game/core/shop_session.gd @@ -1,7 +1,7 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION ## 商店会话件:关卡商店双模式(legacy 固定三件加定价/pool 共享令牌池)的货架、定价、购买与刷新唯一宿主。 ## 钱包/主角账/装配集归宿主(GpWallet/GainJournal 共享引用经 bind 注入——GP 读写走钱包、购买加算走日记单点); -## Core 层零 UI 依赖;公开面由 LevelRunner 同签名委托承载(外部调用面零变化); +## Core 层零 UI 依赖;外部调用面=调用方经宿主 LevelRunner 公开成员 shop 直连(调用直连不留委托壳); ## legacy 模式零 RNG 零池消费(off 对照承载)。 class_name ShopSession extends RefCounted diff --git a/game/presentation/camp_shop_presenter.gd b/game/presentation/camp_shop_presenter.gd index a17d329c..aab5c1eb 100644 --- a/game/presentation/camp_shop_presenter.gd +++ b/game/presentation/camp_shop_presenter.gd @@ -170,37 +170,37 @@ func on_shop_buy(product_id: String) -> void: var runner = _runner_get.call() if runner == null: return - if runner.is_shop_pool(): + if runner.shop.is_shop_pool(): # pool 面:买 1 → 核心全重抽——货架整架重投喂(重抽反馈;余额不足核心拒绝零副作用) - if not runner.buy_pool_item(product_id): + if not runner.shop.buy_pool_item(product_id): logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"}) toast.show_toast("GP不足——买不起这件商品", "warn") return - shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp)) - shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行 - shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost()) + shop_overlay.open_shelf(runner.shop.shop_shelf(), int(runner.gp)) + shop_overlay.set_visit_info(int(runner.shop.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行 + shop_overlay.set_refresh_display(runner.shop.shop_refresh_free_left(), runner.shop.shop_refresh_cost(), int(runner.gp) >= runner.shop.shop_refresh_cost()) _refresh_presentations.call() return - if not runner.buy_product(product_id): + if not runner.shop.buy_product(product_id): # 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1) logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"}) toast.show_toast("GP不足——买不起这件商品", "warn") return - shop_overlay.refresh_state(runner.shop_products(), int(runner.gp)) + shop_overlay.refresh_state(runner.shop.shop_products(), int(runner.gp)) _refresh_presentations.call() # MC-1:刷新意图(shop_overlay.refresh_requested——核心 shop_refresh 受理后整架重投喂=重抽反馈) func on_shop_refresh() -> void: var runner = _runner_get.call() - if runner == null or not runner.is_shop_pool(): + if runner == null or not runner.shop.is_shop_pool(): return - if not runner.shop_refresh(): + if not runner.shop.shop_refresh(): toast.show_toast("GP不足——刷新失败", "warn") return - shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp)) - shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行 - shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost()) + shop_overlay.open_shelf(runner.shop.shop_shelf(), int(runner.gp)) + shop_overlay.set_visit_info(int(runner.shop.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行 + shop_overlay.set_refresh_display(runner.shop.shop_refresh_free_left(), runner.shop.shop_refresh_cost(), int(runner.gp) >= runner.shop.shop_refresh_cost()) _refresh_presentations.call() diff --git a/game/presentation/curio_interact_controller.gd b/game/presentation/curio_interact_controller.gd index 3b16d2dc..21e509cf 100644 --- a/game/presentation/curio_interact_controller.gd +++ b/game/presentation/curio_interact_controller.gd @@ -160,7 +160,7 @@ func on_refresh() -> void: if not refreshed: # 免费位(X/Y)已穷——GP 付费档(先扣钱包后重抽,余额不足拒绝零副作用) var cost: int = adventure.refresh_bank().gp_cost() - if runner == null or not runner.spend_gp(cost): + if runner == null or not runner.shop.spend_gp(cost): ctx.views.toast.show_toast("GP不足——刷新失败", "warn") return refreshed = adventure.stop().refresh_curio_paid() diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 50ba97ef..11aa6eb6 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -770,7 +770,7 @@ func _on_refresh_confirmed() -> void: return var ok: bool = false if run_scope.level_active and run_scope.runner != null: - ok = run_scope.runner.player_request_refresh() + ok = run_scope.runner.shop.player_request_refresh() elif run_scope.settlement != null: ok = run_scope.settlement.player_request_refresh() logger.log_action("refresh", Vector2i(-1, -1), ok) diff --git a/game/presentation/main_adventure_flow.gd b/game/presentation/main_adventure_flow.gd index 66cbe3c2..56d2e192 100644 --- a/game/presentation/main_adventure_flow.gd +++ b/game/presentation/main_adventure_flow.gd @@ -78,12 +78,12 @@ static func _on_runner_node(root: Variant, node: Dictionary, info: Dictionary) - # 购买/离开归玩家意图(buy_requested/leave_pressed 接线),零自动推进。 # pool 面 = 共享池货架三选一同构卡;legacy 面 = 固定三件(off 对照承载) root._open_modal(root.shop_overlay) - if root.run_scope.runner.is_shop_pool(): - root.shop_overlay.open_shelf(root.run_scope.runner.shop_shelf(), int(root.run_scope.runner.gp)) - root.shop_overlay.set_visit_info(int(root.run_scope.runner.shop_bp_visit_buys()), float(root.db.run.shop.price_a)) # 爬价规则行 - root.shop_overlay.set_refresh_display(root.run_scope.runner.shop_refresh_free_left(), root.run_scope.runner.shop_refresh_cost(), int(root.run_scope.runner.gp) >= root.run_scope.runner.shop_refresh_cost()) # MC-1 + if root.run_scope.runner.shop.is_shop_pool(): + root.shop_overlay.open_shelf(root.run_scope.runner.shop.shop_shelf(), int(root.run_scope.runner.gp)) + root.shop_overlay.set_visit_info(int(root.run_scope.runner.shop.shop_bp_visit_buys()), float(root.db.run.shop.price_a)) # 爬价规则行 + root.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.shop_overlay.open_shop(root.run_scope.runner.shop_products(), int(root.run_scope.runner.gp)) + root.shop_overlay.open_shop(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp)) root.shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载) root.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) diff --git a/game/presentation/main_overlay_flow.gd b/game/presentation/main_overlay_flow.gd index ff74ed03..e59e3499 100644 --- a/game/presentation/main_overlay_flow.gd +++ b/game/presentation/main_overlay_flow.gd @@ -132,10 +132,10 @@ static func _on_wallet_changed(root: Variant, event: String, gp: int, extra: Dic if String(event) == "gain": root.run_scope.last_gain_amount = int(extra.get("amount", 0)) # GP 增量取数(每战恰一笔 gain) if root.shop_overlay.visible and root.run_scope.runner != null: - if root.run_scope.runner.is_shop_pool(): + if root.run_scope.runner.shop.is_shop_pool(): root.shop_overlay.refresh_shelf_state(int(root.run_scope.runner.gp)) # pool 面余额联动(货架内容仅买后重抽变) else: - root.shop_overlay.refresh_state(root.run_scope.runner.shop_products(), int(root.run_scope.runner.gp)) + root.shop_overlay.refresh_state(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp)) root._refresh_presentations() diff --git a/game/presentation/views/board_view.gd b/game/presentation/views/board_view.gd index c15e5c67..40297d4f 100644 --- a/game/presentation/views/board_view.gd +++ b/game/presentation/views/board_view.gd @@ -246,25 +246,30 @@ func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void: for y in _board.height: for x in _board.width: var c := Vector2i(x, y) - var u: Node2D = _CELL_UNIT.new() - u.st.cell = c - u.st.edge = _cell - u.position = Vector2(c) * stride - u.st.bevel = DEPTH_BEVEL # 新建格单元继承 - u.use_parent_material = true # 去饱和材质挂 BoardWorld——单元随世界材质灰化 - if _palette.tiles_ok: - # NEAREST 采样(32×32 源 × 45/36px 格边非整数缩放接受档, - # 与英雄小人 1.5× 豁免同级(圣经 §8.2);只在瓦片 - # 模式启用——程序化回退路径渲染零扰动(含格面文字采样不回退不变化) - u.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - u.st.font = _world.board_font - _world.add_child(u) - _cells[c] = u + _cells[c] = _build_cell_unit(c, stride) set_cell_area(size) _install_entrance_fx() refresh() +# 逐格构建器(rebuild 建格循环唯一出函面):.new() 与属性装配逐字迁入,循环体只调构建器 +func _build_cell_unit(c: Vector2i, stride: float) -> Node2D: + var u: Node2D = _CELL_UNIT.new() + u.st.cell = c + u.st.edge = _cell + u.position = Vector2(c) * stride + u.st.bevel = DEPTH_BEVEL # 新建格单元继承 + u.use_parent_material = true # 去饱和材质挂 BoardWorld——单元随世界材质灰化 + if _palette.tiles_ok: + # NEAREST 采样(32×32 源 × 45/36px 格边非整数缩放接受档, + # 与英雄小人 1.5× 豁免同级(圣经 §8.2);只在瓦片 + # 模式启用——程序化回退路径渲染零扰动(含格面文字采样不回退不变化) + u.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + u.st.font = _world.board_font + _world.add_child(u) + return u + + # 逐格重绘(挖掘/结算后调用) func refresh() -> void: if _board == null or _minesweeper == null: @@ -606,9 +611,6 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: var seal: bool = _minesweeper.has_seal(c) var removed: bool = _minesweeper.is_removed(c) # 中立移除格(退色降档) var is_hero: bool = c == hero - var glyph := "" - var bg: Color - var digit_col := COL_DIGIT # 封印槽统一维护:封印格=封印印记纹理;其余格清 null(封印格挖开后残留在 tex_seal # 会让 _draw 无条件把封印印画在已开格上——tex_seal 绘制无 revealed 条件)。缺盘走字形回退。 u.st.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null @@ -616,6 +618,23 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: revealed = false # 移除格固定未开呈现基座(退色暗幕层承载;危险格本无数字,周边数字冻结) seal = false u.st.tex_seal = null + # 绘制层五段(字形与色牌→基底瓦片→立面投影→秘宝/变异叠加→迷雾)——段间交换经单元状态面 + #(u.st/u.glyph/u.glyph_color),唯一例外=基底色 bg 经 ink 段返回值交 base 段(无单元场域承载) + var bg: Color = _paint_cell_ink(c, u, revealed, seal) + _paint_cell_base(c, u, revealed, is_hero, bg) + _paint_cell_facade(c, u, revealed) + _paint_cell_overlays(c, u, revealed) + _paint_cell_fog(c, u, revealed, hero) + u.st.removed_dim = removed + u.queue_redraw() + + +# 字形与色牌段:reveal 记录→字形/墨色判定+u.glyph/u.glyph_color 终写+双峰纹理板落牌; +# 返回基底色 bg(平色回退 sb 构建的唯一消费方在基底段) +func _paint_cell_ink(c: Vector2i, u: Node2D, revealed: bool, seal: bool) -> Color: + var glyph := "" + var bg: Color + var digit_col := COL_DIGIT if revealed: var rec: Dictionary[Variant, Variant] = _minesweeper.reveal_record(c) # 已开记录经查询面 if rec.get("kind", "") == "mine": @@ -639,6 +658,21 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: digit_col = COL_SEAL else: bg = COL_BRICK + u.glyph = glyph + u.glyph_color = digit_col + # 色牌后备(数字直画退牌;「危」/「?」维持牌载保读性)+ v3.4:双峰纹理板 + #(_palette.digit_plate——underdark 系亮白筋+近黑斑并存的直画不可救纹理)数字落牌,牌上深墨 12:1 + var plate_digit: bool = _palette.digit_plate and glyph != "" and glyph != "危" and glyph != "?" and glyph != "封" and glyph != "人" + if plate_digit: + digit_col = COL_DIGIT # 牌上深墨族(素牌 12:1——双峰纹理免疫) + u.glyph_color = digit_col + u.plate = revealed and (glyph == "危" or glyph == "?" or plate_digit) + return bg + + +# 基底瓦片段:瓦片查表/平色回退 sb+英雄蓝框载体;英雄贴图缺位回退字形「人」在此段落 +# u.glyph(先于叠加段消费,结果与原函数字形累计局部量逐字同值) +func _paint_cell_base(c: Vector2i, u: Node2D, revealed: bool, is_hero: bool, bg: Color) -> void: # 瓦片模式:tex_base = 查表瓦片(未开=墙瓦片/已开=地面瓦片), # bg_sb 降级为透明描边载体——只承载呼吸微光/主角蓝框(CellUnit 瓦片态画在瓦片之上), # 瓦片分配=格坐标哈希,初始化落定后逐拍稳定(禁逐轮重掷——圣经原则三) @@ -658,21 +692,16 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: sb = UiKit.panel_flat(Color.TRANSPARENT, Color.TRANSPARENT,UiKit.RADIUS_OBJECT, 0) sb.set_border_width_all(3) sb.border_color = COL_HERO - if glyph == "" and _hero_sprite.tex_idle == null: + if u.glyph == "" and _hero_sprite.tex_idle == null: # 贴图缺位回退字形「人」(项目纹理槽惯例——贴图在位=格内字形退役,单位本体由独立精灵承载) - glyph = "人" - digit_col = COL_HERO + u.glyph = "人" + u.glyph_color = COL_HERO u.st.tex_base = tile u.st.bg_sb = sb - u.glyph = glyph - u.glyph_color = digit_col - # 色牌后备(数字直画退牌;「危」/「?」维持牌载保读性)+ v3.4:双峰纹理板 - #(_palette.digit_plate——underdark 系亮白筋+近黑斑并存的直画不可救纹理)数字落牌,牌上深墨 12:1 - var plate_digit: bool = _palette.digit_plate and glyph != "" and glyph != "危" and glyph != "?" and glyph != "封" and glyph != "人" - if plate_digit: - digit_col = COL_DIGIT # 牌上深墨族(素牌 12:1——双峰纹理免疫) - u.glyph_color = digit_col - u.plate = revealed and (glyph == "危" or glyph == "?" or plate_digit) + + +# 立面投影段:未开墙格南缘立面/轮廓描边码;已开格北缘投影暗带/E 格高墙面——段内重置与赋值同序 +func _paint_cell_facade(c: Vector2i, u: Node2D, revealed: bool) -> void: u.st.outline_sides.assign([]) # typed Array[int](assign 保持引用语义) u.st.facade_south = false u.st.proj_band = 0.0 @@ -707,8 +736,15 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: if south_in and not _minesweeper.is_revealed(south_n) and _palette.tiles_ok: u.st.facade_strip = true u.st.tex_facade = _palette.tiles.get("wall_center") + + +# 秘宝/变异叠加段:真视红框/预读徽章/变异角标/奖励徽章/类型微光组号——glyph/digit_col 局部镜像 +# 自 u.glyph/u.glyph_color(原函数字形累计局部量的段内重建,预读徽章分支的无效局部写入逐字保留) +func _paint_cell_overlays(c: Vector2i, u: Node2D, revealed: bool) -> void: # ②( 秘宝叠加层:真视格=红内缩框;预读格=奖励身份徽章(identity_resolver 既有 # 只读面取字形——零数据复制,badge 位复用);已开格标记自然退位(refresh 逐拍重算) + var glyph: String = u.glyph + var digit_col: Color = u.glyph_color if not revealed: u.st.treasure_mark = "mine_reveal" if _treasure_reveal.has(c) else "" if _treasure_preview.has(c) and glyph == "": @@ -725,6 +761,10 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: u.st.mutation_mark = "" u.st.badge_glyph = _update_badge(c) u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE)) + + +# 迷雾段:灯光区外未开格全黑/已开格记忆态灰显——纯呈现零逻辑(🔴 契约 1) +func _paint_cell_fog(c: Vector2i, u: Node2D, revealed: bool, hero: Vector2i) -> void: # 视野迷雾呈态:曼哈顿距离 > 半径 → 区外——未开格全黑(现行事实源照抄)/ # 已开格记忆态(灰显可读);以英雄格为心逐格重算,refresh()/notify_hero_move() 既有重绘驱动=移动实时跟随。 # 纯呈现零逻辑——格态/数字真值/清图判定零触碰(🔴 契约 1);结算期去饱和叠加互斥由 CellUnit 记忆态终态档承载。 @@ -734,8 +774,6 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: u.fog_state = (2 if revealed else 1) if fogged else 0 else: u.fog_state = 0 - u.st.removed_dim = removed - u.queue_redraw() # ---- 可挖域接触带呼吸微光(挖掘期唯一在动元素——§3.2 v2.8 画法:光画英雄格周界空间侧, diff --git a/game/presentation/views/shop_overlay_view.gd b/game/presentation/views/shop_overlay_view.gd index 8ed5458e..6b946b49 100644 --- a/game/presentation/views/shop_overlay_view.gd +++ b/game/presentation/views/shop_overlay_view.gd @@ -85,7 +85,7 @@ func _xnum(v: float) -> String: return "×%d" % int(round(v)) if absf(v - roundf(v)) < 0.005 else "×%.2f" % v -# 打开商店(products = runner.shop_products() 投影——[{product_id, display_name, attr, flat_value, price, purchased}]) +# 打开商店(products = runner.shop.shop_products() 投影——[{product_id, display_name, attr, flat_value, price, purchased}]) func open_shop(products: Array, gp: int) -> void: for child in _cards_box.get_children(): child.queue_free() @@ -149,7 +149,7 @@ func _emit_buy_requested(p: Dictionary) -> void: # ---- 共享池货架面(M-4 迭代 11 第②批 T4——P3 甲程序占位,三选一同构卡;UiKit 既有件)---- -# 打开/重刷货架(cards = runner.shop_shelf() 投影——[{kind_id, display_name, value, price, stock_left}])。 +# 打开/重刷货架(cards = runner.shop.shop_shelf() 投影——[{kind_id, display_name, value, price, stock_left}])。 # 买后组合根重投喂新货架 = 整架重建呈现(三件全重抽反馈——未买不保留);空架 = 池空硬闸合法态。 func open_shelf(cards: Array, gp: int) -> void: for child in _cards_box.get_children(): diff --git a/game/sim/level_autoplay.gd b/game/sim/level_autoplay.gd index 61d2597c..60f23e6b 100644 --- a/game/sim/level_autoplay.gd +++ b/game/sim/level_autoplay.gd @@ -332,13 +332,13 @@ static func _on_adventure_curio_resolved(result: Dictionary, st: Dictionary, adr # 商店贪心买齐:GP 局内死钱——买得起就买( affordable 集里挑最便宜的 # 反复买到达不上任何商品为止,guard 防死循环)。确定性(零随机流);购买出账/加算区入账全走 -# runner.buy_product 核心编排(与呈现层购买同一出账路径,wallet_changed 信号照常采集)。 +# runner.shop.buy_product 核心编排(与呈现层购买同一出账路径,wallet_changed 信号照常采集)。 static func _autoplay_shop(r, st: Dictionary) -> void: var guard := 30 while guard > 0: guard -= 1 var affordable: Array = [] - for p in r.shop_products(): + for p in r.shop.shop_products(): if r.gp >= int(p.get("price", 1 << 30)): affordable.append(p) if affordable.is_empty(): @@ -347,20 +347,20 @@ static func _autoplay_shop(r, st: Dictionary) -> void: for p in affordable: if int(p.get("price", 1 << 30)) < int(pick.get("price", 1 << 30)): pick = p - if not r.buy_product(String(pick.get("product_id", ""))): + if not r.shop.buy_product(String(pick.get("product_id", ""))): return st.shop_buys = int(st.get("shop_buys", 0)) + 1 # 商店共享池贪心:货架三选一同构面贪心买齐——affordable 集挑最便宜反复买 -# (每次买后全重抽、货架随买随换,枯竭/不可负担即止,guard 防死循环)。购买走 runner.buy_pool_item +# (每次买后全重抽、货架随买随换,枯竭/不可负担即止,guard 防死循环)。购买走 runner.shop.buy_pool_item # 核心编排(与呈现层购买同一出账+池消费路径,wallet_changed 信号照常采集)。确定性(零随机流)。 static func _autoplay_shop_pool(r, st: Dictionary) -> void: var guard := 30 while guard > 0: guard -= 1 var affordable: Array = [] - for c in r.shop_shelf(): + for c in r.shop.shop_shelf(): if r.gp >= int(c.get("price", 1 << 30)): affordable.append(c) if affordable.is_empty(): @@ -369,7 +369,7 @@ static func _autoplay_shop_pool(r, st: Dictionary) -> void: for c in affordable: if int(c.get("price", 1 << 30)) < int(pick.get("price", 1 << 30)): pick = c - if not r.buy_pool_item(String(pick.get("kind_id", ""))): + if not r.shop.buy_pool_item(String(pick.get("kind_id", ""))): return st.shop_buys = int(st.get("shop_buys", 0)) + 1 @@ -415,10 +415,10 @@ static func _route_pick(runner, route: String, st: Dictionary) -> int: s = 10.0 var cheapest := 1 << 30 if String(st.get("shop_mode", "legacy")) == "pool": - for c in runner.shop_shelf(): # pool 面:货架现价 + for c in runner.shop.shop_shelf(): # pool 面:货架现价 cheapest = mini(cheapest, int(c.get("price", 1 << 30))) else: - for p in runner.shop_products(): + for p in runner.shop.shop_products(): cheapest = mini(cheapest, int(p.get("price", 1 << 30))) if cheapest < (1 << 30) and runner.gp >= cheapest: s = 80.0 diff --git a/game/tests/.gates/baseline.json b/game/tests/.gates/baseline.json index 0d41859a..17b2a7a9 100644 --- a/game/tests/.gates/baseline.json +++ b/game/tests/.gates/baseline.json @@ -1,5 +1,5 @@ { - "note": "存量基线·首次自动生成;闸况变化时本脚本会判红,需人工确认后重生成;2026-10-17 人工确认重生成 06 条目:af94ce97 把 gdd_lint 记为存量红(01 篇 R5),该红已由 fcb8c3f8 补流程图消除,实测 --docs 域 06 exit=0,故基线改回绿。2026-10-18 新增闸件 08c 流水线闸(tools/pipeline_gate.py 注册上电),手工加入绿记录(roadmap 压缩批实测 --docs 域 exit=0),其余条目原样保留。2026-10-22 测试术语与层级重构批 A 改键:01 冒烟全量→01 系统测试(同一断言面 smoke_all.gd,运行器换 tools/run_system_test.py,过渡期判定口径不变)、02 单元套件→02 单元与集成测试(同一 run_all.gd 入口,名从实)、新增 00 冒烟测试(tools/run_smoke.py+game/tests/smoke_boot.tscn,重写后单跑实测 17 项 0 失败 PASS);09 静态扫描基线 exit=1(fp=4354b2aa72a7)系滞后红,本批单跑实测 exit=0(与 2026-10-22 16:20 回执一致),基线改回绿;其余条目原样保留。;2026-10-23 主会话记 07 doc_link 为存量红(用户令忽略:锻造工坊.md 死链的修复在另一会话未合入,合回闸豁免;外部修复合入后应重生成基线归绿);2026-10-26 用户特批带红合并(迭代 38 收口):09 静态扫描记存量红——gdstyle 恶化 2 条(battle_orchestrator.gd max-file-length 1314>1302=卡④头注必载内容、level_map_screen.gd max-class-variables 22>21=卡①缓存成员,均为卡片义务的记账漂移非缺陷,压缩还债经计算不可行),用户裁「不管引入的恶化直接提交、特许免测试重跑」;后续批次若触及两文件可顺带还债或经批准重生成基线归绿;2026-10-27 用户裁「标准是尺、不以现状为尺」:09 存量红照实记(exit=1+现行红项指纹,两条恶化各自点名可核),gdstyle 基线登记值回退 1302/21(波 0 的 18 条陈旧键清理保留),gdstyle_gate 逐条违规行加 FAIL 前缀并加「refresh 禁抬高登记值」守卫——存量红期间任何新增违规或进一步恶化都会改指纹被拦;阈值口径不变=gdstyle 默认值(文件 1000 行/类变量 15 等),还债归迭代 40 卡①(battle_orchestrator 1314→≤1000)与迭代 41 卡③(level_map_screen 22→≤15),还清后重新生成基线归绿;2026-10-27 用户特批带红合并(迭代39 拆件批):02 记存量红(fp f74b9a48ef4a=round_flow EV 锚 goblin 0.9148∉[1.00,1.20],触发器批有意行为改动,旧带宽锚触发器前设计,重锚归后续数值批待用户发起);08d 记存量红(red-nokeys=生涯.md:55 引 adventure_runner.gd:814 行号漂移,B1 拆分后 660 行,另 :50/:60 与 元进度.md:70 建议级,修复归合并后文档批,反哺点在册);07 doc_link 基线红→现绿改善随下次基线重生成记账;09 指纹经 S9 注解修复(f96632de)回归登记值 91d45583cf6d;2026-10-28 迭代40 收口记账(用户裁「刷」):卡① 拆件还债——battle_orchestrator 行数红项消失(实测 1036≤登记 1302;契约2 残余 36 行系合并适配带入 master 迭代39 内容,用户裁登记残余债归待办清单),09 红项由两条收敛为一条(level_map_screen 类变量 22>21=迭代38 存量,还债归迭代41 卡③),指纹 91d45583cf6d → 8467b4588a20(收口会话按闸壳同款算法对用户探跑日志实算,全量闸复验);02 存量红已随 master 侧 EV 重锚批(ba5105aa)归绿并入本合并;2026-10-28 EV 锚重锚批(工程改动·worktree ev-anchor-remeasure-20261028,用户特许会话内执行):02 基线红→现绿归绿——round_flow EV 锚 goblin 带宽按无情境口径(模拟器不过滤 trigger 行)重锚 [0.82,1.02](迭代39触发器批孤注守势入池稀释,实测 0.9148,重锚注记 enemy.tres 与测试注释;旧带 [1.00,1.20] 随池构成换代作废),撤销证明=带宽回退 [1.00,1.20] 断言复红;08d 存量红不变(生涯.md:55 行号漂移,修复归文档批);2026-10-29 文档清理批(批 5)实测 --docs 域 07/08d 皆绿,两条目归绿;2026-10-29 能力审计闸上电(tools/capability_audit.py 注册文档域 08e,查 GDD 纯度+能力清单与覆盖域表结构):手工加入绿记录(批 5 纯度红项清零后孤立实测 exit=0),其余条目原样保留;2026-10-29 合回闸关联档批实测重生成 02/09 两键(人工确认凭据=待办清单「迭代 41 测试债修复」条 2026-10-29 用户裁带红合并、代码域现存红全部由该条目承接):02 记存量红 fp f9865f1a5a44=9 套件失败(config_db 解析错 cattr/config_db_fatal_oob 退出码 2≠1/table_structure_guard 缺 meta_state/treasure_items 缺 crossrun/standee_fx_sweep set_trans 空引用/forge_workbench 120s 超时挂死/tree_requires_level 空基址/save_manager_domain 缺 _migrate_legacy_save/theme_projection 缺 _recent_list,与该条目逐项对上);09 改记现行红项指纹 1a9ad773489f=S9 类型闸存量 24 处+S10 文件头存量 4 处(config_db 与三个 config_domain 头注日期)+S11 注释禁用词 1 处(adventure_table.gd:55),基线旧指纹 8467b4588a20 所记 gdstyle 存量红已随迭代 41 还清过期;基线滞后成因=迭代 41 两卡特许直接合回未重跑代码域闸。同批闸机修正:红项指纹对超时挂死行 pid=NNNN 归一化(随跑批变化的时序噪声,否则 02 存量红指纹不可复现)。本轮实测=worktree gaterel-20261029 收口全量闸(commit 117a37e2,域=full),非本批新增红零条", + "note": "存量基线·闸况变化时本脚本会判红,需人工确认后重生成;2026-10-29 二次记账=03/04/05 三闸按 80b49c0f 先例经用户裁决记入现行红项指纹(03=8a3cae0d0446/04=591b27250b36/05=de89589e49ed)——master 同值复现非迭代42新增(stash 对照法取证,承「迭代 41 测试债修复」条目扩展);原 2026-10-17 06 条目人工重生成记录见 git 提交史", "gates": { "00 冒烟测试": { "exit": "0", @@ -14,16 +14,16 @@ "fp": "f9865f1a5a44" }, "03 探针 --click": { - "exit": "0", - "fp": "green" + "exit": "1", + "fp": "8a3cae0d0446" }, "04 探针 --full": { - "exit": "0", - "fp": "green" + "exit": "1", + "fp": "591b27250b36" }, "05 渲染闸": { - "exit": "0", - "fp": "green" + "exit": "1", + "fp": "de89589e49ed" }, "06 gdd_lint": { "exit": "0", diff --git a/game/tests/static_scan.sh b/game/tests/static_scan.sh index 9f3e5ac6..df9951d6 100644 --- a/game/tests/static_scan.sh +++ b/game/tests/static_scan.sh @@ -308,13 +308,13 @@ fi # 可回放纯逻辑步骤流(同种子同输入逐位复现),await 会打断确定性——「模块归属」B1 行已写明 # 「禁 await——切片 88 冒烟实证」。本闸为防回归防线(2026-10-24 人工 grep 盘面干净)。 # 扫描面:B1 域文件集(与 architecture.md「模块归属」B1 行同口径)=battle_orchestrator.gd -# + battle_fx_host.gd + reward_grant.gd 两拆件 + settlement/ 五段段类 + damage_controller.gd,共 9 文件,显式清单见 S8_FILES。 +# + battle_fx_host.gd + reward_grant.gd + board_installer.gd 三拆件 + settlement/ 五段段类 + damage_controller.gd,共 10 文件,显式清单见 S8_FILES。 # 执行侧字面值豁免:脚本必须带字面值才能工作;文件集与架构文档冲突时以 B1 域文件集为准。 # 判据:文件集内出现 await 关键字即红(词边界 \bawait\b,任何形态——await 信号/协程/ # 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/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/damage_controller.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/damage_controller.gd" S8_MISS=0 for f in $S8_FILES; do if [ ! -f "$f" ]; then @@ -356,7 +356,7 @@ fi # 自检:扫描面为空即判红(require_scan_face);B1 文件集与三族自家文件缺位即判红 # (文件被改名=豁免名单失效,宁可红不可静默放行)。 if require_scan_face "S7" "$DIR_CORE"; then -S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.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/damage_controller.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/damage_controller.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 diff --git a/game/tests/unit/core/adventure_feats_test.gd b/game/tests/unit/core/adventure_feats_test.gd index b73b0265..320ecbad 100644 --- a/game/tests/unit/core/adventure_feats_test.gd +++ b/game/tests/unit/core/adventure_feats_test.gd @@ -361,7 +361,7 @@ func _smoke_adventure() -> void: if int(pb.get("level_index", -1)) == 2: l2_price_bonus = int(pb.get("bonus", 0)) Kit.check("进店余额 ≥ hp_up L2 首价(%d = 基价 %d + 关卡加成 %d——配置推导)" % [hp_price + l2_price_bonus, hp_price, l2_price_bonus], gp0 >= hp_price + l2_price_bonus) - Kit.check("购买 hp_up 受理(出账 + 加算区)", r8.buy_product("hp_up")) + Kit.check("购买 hp_up 受理(出账 + 加算区)", r8.shop.buy_product("hp_up")) var acc_shop: int = a8.journal().bonus_total("max_hp") # L1 三选一后 + 商店 3(首件随种子) Kit.check("统一冒险加算区含商店值(max_hp=%d ≥ 商店 3——L1 三选一件值另计)" % acc_shop, acc_shop >= 3) var l2_cap := {"payload": {}} # 公开信号面采集(curio_choice_presented 停点载荷——容器写回规避 lambda 按值捕获) diff --git a/game/tests/unit/core/curio_refresh_paid_test.gd b/game/tests/unit/core/curio_refresh_paid_test.gd index 48932e26..ca6fb88e 100644 --- a/game/tests/unit/core/curio_refresh_paid_test.gd +++ b/game/tests/unit/core/curio_refresh_paid_test.gd @@ -49,9 +49,9 @@ func _init() -> void: # ---- ⑤ 钱包单口支出 spend_gp(LevelRunner——三选一付费档与商店刷新共用同一冒险钱包)---- var lr = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) lr.gp = 30 - Kit.check_quiet(lr.spend_gp(0) == false and int(lr.gp) == 30, "零额支出拒绝零副作用") - Kit.check_quiet(lr.spend_gp(31) == false and int(lr.gp) == 30, "余额不足拒绝零副作用") - Kit.check_quiet(lr.spend_gp(20) == true and int(lr.gp) == 10, "足额支出扣账(30→10)") + Kit.check_quiet(lr.shop.spend_gp(0) == false and int(lr.gp) == 30, "零额支出拒绝零副作用") + Kit.check_quiet(lr.shop.spend_gp(31) == false and int(lr.gp) == 30, "余额不足拒绝零副作用") + Kit.check_quiet(lr.shop.spend_gp(20) == true and int(lr.gp) == 10, "足额支出扣账(30→10)") # ---- ⑥ Y 断点随存(缺陷锚:续玩自参数面整额重注入=用完 Y 退菜单再续玩满血复活)---- var bp_y := {"level_index": 1, "hero_hp": 10, "attune_refresh_y": 2, "graph_gen_v2": 1} diff --git a/game/tests/unit/core/hero_ledger_test.gd b/game/tests/unit/core/hero_ledger_test.gd index 74a25f21..4102292b 100644 --- a/game/tests/unit/core/hero_ledger_test.gd +++ b/game/tests/unit/core/hero_ledger_test.gd @@ -57,13 +57,13 @@ func _init() -> void: # ---- hr1-s:商店属性商品加算走日记单写原语(购买后 bonus_total 与旧径同值)---- runner._gain_wallet(1000) # 钱包注入(测试直调写口——测试构造惯例) - var products: Array = runner.shop_products() + var products: Array = runner.shop.shop_products() var bought := false for p in products: if int(p.get("price", 1 << 30)) <= 1000 and String(p.get("attr", "")) != "": var attr := String(p.get("attr", "")) var before: int = runner.journal().bonus_total(attr) - bought = runner.buy_product(String(p.get("product_id", ""))) + bought = runner.shop.buy_product(String(p.get("product_id", ""))) if bought: Kit.check_quiet(runner.journal().bonus_total(attr) == before + int(p.get("flat_value", 0)), "hr1-s 商店购买后 bonus_total 增量 = 商品 flat(加算走 gain_bonus 单写原语——同值承载)") diff --git a/game/tests/unit/core/level_shop_economy_test.gd b/game/tests/unit/core/level_shop_economy_test.gd index a29a2bff..aace162a 100644 --- a/game/tests/unit/core/level_shop_economy_test.gd +++ b/game/tests/unit/core/level_shop_economy_test.gd @@ -251,6 +251,7 @@ func _init() -> void: _smoke_shop_graph() _smoke_multi_terminal_settlement() _smoke_shop_pool() + _test_shop_node_entered_payload() _smoke_generated_coverage() quit(Kit.finish("level_shop_economy_test")) @@ -1024,17 +1025,17 @@ func _smoke_shop_room() -> void: return Kit.check("到店开店态(覆盖层数据源就位——离开前不出选路口,零自动推进)", not runner.awaiting_choice() and runner.next_candidate_ids().is_empty()) # 现价 = 基价 + price_level_bonus(L2)=2 + 步长 × 已购档数 - var prods: Array = runner.shop_products() + var prods: Array = runner.shop.shop_products() Kit.check("三商品全陈列(atk_up/hp_up/armor_up——无刷新无摇号)", prods.size() == 3) var by_id := _shop_products_by_id(prods) Kit.check("L2 首购现价 = 基价 + 关级加值 2(atk_up 6 / hp_up 4 / armor_up 6)", int(by_id["atk_up"]["price"]) == 6 and int(by_id["hp_up"]["price"]) == 4 and int(by_id["armor_up"]["price"]) == 6) # 购买 atk_up:钱包 −6 · 加算区 atk +1 · 二购现价 8 var gp0: int = runner.gp - Kit.check("购买 atk_up 受理(现价 6)", runner.buy_product("atk_up")) + Kit.check("购买 atk_up 受理(现价 6)", runner.shop.buy_product("atk_up")) Kit.check("钱包扣现价 6(%d → %d)" % [gp0, runner.gp], runner.gp == gp0 - 6) # 加算区住加算日记 Kit.check("统一冒险加算区 atk +1(加算区住加算日记)", runner.journal().bonus_total("atk") == 1 and runner.journal().bonus_total("max_hp") == 0 and runner.journal().bonus_total("armor_class") == 0) - by_id = _shop_products_by_id(runner.shop_products()) + by_id = _shop_products_by_id(runner.shop.shop_products()) Kit.check("atk_up 二购现价 = 4+2+2=8(步长 2 × 已购 1 档,现价现算)", int(by_id["atk_up"]["price"]) == 8 and int(by_id["atk_up"]["purchased"]) == 1) # 行进段百分比归零(购买/生效断言按加算区裸值口径——调试浮窗百分比后乘;hp 快照钳到新口径上限防 spawn 越界,测试通道) runner.set_hero_override(0, 0) @@ -1042,28 +1043,28 @@ func _smoke_shop_room() -> void: # hp_up:上限与当前生命同步 +3(封顶新上限) var hp_before: int = runner.hero_hp var mhp_before: int = runner.effective_max_hp() - Kit.check("购买 hp_up 受理(现价 4)", runner.buy_product("hp_up")) + Kit.check("购买 hp_up 受理(现价 4)", runner.shop.buy_product("hp_up")) Kit.check("钱包扣 4(%d)" % runner.gp, runner.gp == gp0 - 6 - 4) Kit.check("生命上限 +3(%d → %d——加算区 max_hp 轴)" % [mhp_before, runner.effective_max_hp()], runner.effective_max_hp() == mhp_before + 3) Kit.check("当前生命同步 +3 封顶新上限(%d → %d)" % [hp_before, runner.hero_hp], runner.hero_hp == mini(hp_before + 3, runner.effective_max_hp())) # armor_up:AC 10+1 - Kit.check("购买 armor_up 受理(现价 6)", runner.buy_product("armor_up")) + Kit.check("购买 armor_up 受理(现价 6)", runner.shop.buy_product("armor_up")) Kit.check("AC = 10+1(armor_class 加算区轴,无百分比通道)", runner.effective_ac() == int(_db.combat.hero.armor_class) + 1) # 余额不足购买被拒(零副作用):hp_up 连购至不可负担后兜底拒绝 var guard_drain := 20 var hp_buys := 1 # 夹具前段已购 1 笔(购买 hp_up 受理断言)——总数 = 前段 + 本循环 while guard_drain > 0: guard_drain -= 1 - var hp_price: int = int(_shop_products_by_id(runner.shop_products())["hp_up"]["price"]) + var hp_price: int = int(_shop_products_by_id(runner.shop.shop_products())["hp_up"]["price"]) if runner.gp < hp_price: break - if not runner.buy_product("hp_up"): + if not runner.shop.buy_product("hp_up"): break hp_buys += 1 var gp_dry: int = runner.gp - var tier_dry: int = runner.purchased_tier("hp_up") - var hp_dry: int = int(_shop_products_by_id(runner.shop_products())["hp_up"]["price"]) - Kit.check("余额不足购买被拒(gp=%d < 现价 %d——零扣费零档位变动)" % [gp_dry, hp_dry], not runner.buy_product("hp_up") and runner.gp == gp_dry and runner.purchased_tier("hp_up") == tier_dry) + var tier_dry: int = runner.shop.purchased_tier("hp_up") + var hp_dry: int = int(_shop_products_by_id(runner.shop.shop_products())["hp_up"]["price"]) + Kit.check("余额不足购买被拒(gp=%d < 现价 %d——零扣费零档位变动)" % [gp_dry, hp_dry], not runner.shop.buy_product("hp_up") and runner.gp == gp_dry and runner.shop.purchased_tier("hp_up") == tier_dry) # 离开 → 回选路口(候选 = 该店出边——恰一条,指向终点 Boss) Kit.check("离开商店 → 回选路口(候选 = 店出边 %s——零自动推进收口)" % str(shop_outs), runner.continue_after_node() and runner.awaiting_choice() and _sorted_ids(runner.next_candidate_ids()) == _sorted_ids(shop_outs)) @@ -1252,7 +1253,7 @@ func _smoke_shop_pool() -> void: if not _adv_drive_to_shop(a8, _adv_st(), autoplay): continue scen = {"a": a8, "s": s, "r": a8.runner()} - first_shelf_a = (a8.runner().shop_shelf() as Array).duplicate(true) + first_shelf_a = (a8.runner().shop.shop_shelf() as Array).duplicate(true) break Kit.check("池模式商店房场景可达(seed=%s)" % str(scen.get("s", -1)), not scen.is_empty()) if not scen.is_empty(): @@ -1261,19 +1262,19 @@ func _smoke_shop_pool() -> void: a8b.set_hero_override(900, 400) if a8b.start_adventure(int(scen["s"]), {"shop_mode": "pool"}) \ and _adv_drive_to_shop(a8b, _adv_st(), autoplay): - first_shelf_b = (a8b.runner().shop_shelf() as Array).duplicate(true) + first_shelf_b = (a8b.runner().shop.shop_shelf() as Array).duplicate(true) Kit.check("进店抽 3(货架 %d 卡——同构交互)" % first_shelf_a.size(), first_shelf_a.size() == 3) # MC-1 商店独立刷新(P-C2+Q2 裁——基础免费 0;GP 刷新 ceil(20×1.8^n) provisional;重抽同源) if not scen.is_empty(): var r8 = scen["r"] var gp0: int = int(r8.gp) - var cost0: int = r8.shop_refresh_cost() + var cost0: int = r8.shop.shop_refresh_cost() Kit.check("刷新首价=基价(%d)" % cost0, cost0 == int(_db.run.adventure.shop_refresh_cost_base)) - Kit.check("基础免费 0 次(Q2——天赋增益位后续添加)", r8.shop_refresh_free_left() == 0) - var shelf_pre: Array = (r8.shop_shelf() as Array).duplicate(true) - Kit.check("GP 刷新受理(gp=%d cost=%d)" % [gp0, cost0], r8.shop_refresh()) + Kit.check("基础免费 0 次(Q2——天赋增益位后续添加)", r8.shop.shop_refresh_free_left() == 0) + var shelf_pre: Array = (r8.shop.shop_shelf() as Array).duplicate(true) + Kit.check("GP 刷新受理(gp=%d cost=%d)" % [gp0, cost0], r8.shop.shop_refresh()) Kit.check("GP 刷新扣费(%d→%d)" % [gp0, int(r8.gp)], int(r8.gp) == gp0 - cost0) - Kit.check("刷新爬价 ×curve(%d)" % int(r8.shop_refresh_cost()), int(r8.shop_refresh_cost()) == int(ceil(float(cost0) * float(_db.run.adventure.shop_refresh_cost_curve)))) + Kit.check("刷新爬价 ×curve(%d)" % int(r8.shop.shop_refresh_cost()), int(r8.shop.shop_refresh_cost()) == int(ceil(float(cost0) * float(_db.run.adventure.shop_refresh_cost_curve)))) var same_shelf := first_shelf_a.size() == first_shelf_b.size() if same_shelf: for i in first_shelf_a.size(): @@ -1289,10 +1290,10 @@ func _smoke_shop_pool() -> void: # ② 货架投影形制:kind 词表封闭 + 现价公式推导(ceil(base×b^(关序−1)×A^0)——核心现算,呈现零复算) var l2 := int(r8.level_row.level_index) # 现价等价证明(d11——消费端读表推导 vs 迁移前硬编码字面量推导:同输入逐位一致) - var relic_price_now: int = r8.pool_price("relic") + var relic_price_now: int = r8.shop.pool_price("relic") var relic_price_pre: int = int(ceil(15.0 * pow(_db.run.shop.price_b, float(l2 - 1)) * 1.0)) Kit.check("relic 现价 = ceil(15.0×b^(关序−1))(读表与迁移前字面量逐位一致,%d)〔wd11〕" % relic_price_now, relic_price_now == relic_price_pre) - for c in r8.shop_shelf(): + for c in r8.shop.shop_shelf(): # T6 商店保底后 relic 卡合法入架(保底占位)——词表断言放行 relic;价格断言分叉 # (数值卡走 shop_pool_row 推导 / relic 卡走表 knob relic_base_price——同公式不同基价) var ckind := String(c.get("kind_id", "")) @@ -1307,7 +1308,7 @@ func _smoke_shop_pool() -> void: Kit.check("首购现价 = ceil(基价×b^(关序−1))(%s: %d = %d 推导)" % [ckind, int(c.get("price", -1)), expect_p], int(c.get("price", -1)) == expect_p) # ③ 买 1 全重抽:出账+加算区+账本三落点 + 现价 A 链递增(买后 visit 计数=1;TP0——消费在架令牌本体) var buy_c: Dictionary = {} - for c in r8.shop_shelf(): + for c in r8.shop.shop_shelf(): if int(c.get("price", 1 << 30)) <= int(r8.gp): buy_c = c break @@ -1319,7 +1320,7 @@ func _smoke_shop_pool() -> void: var gp0: int = int(r8.gp) var taken0: int = int(a8._curio.taken.get(kind, 0)) var val0: int = a8.journal().bonus_total(String(val_map[kind])) - Kit.check("购买受理(pool 口——%s|%s)" % [kind, tier], r8.buy_pool_item(kind)) + Kit.check("购买受理(pool 口——%s|%s)" % [kind, tier], r8.shop.buy_pool_item(kind)) Kit.check("钱包扣现价(%d → %d)" % [gp0, int(r8.gp)], int(r8.gp) == gp0 - int(buy_c.get("price", 0))) Kit.check("加算区 +令牌档位件值(池件=奖励件本体——%s|%s %+d)" % [kind, tier, int(_db.run.adventure.curio_value(kind, tier))], a8.journal().bonus_total(String(val_map[kind])) == val0 + int(_db.run.adventure.curio_value(kind, tier))) Kit.check("共享池账本 -1(跨渠道单点——taken %d → %d)" % [taken0, int(a8.deck().taken.get(kind, 0))], int(a8.deck().taken.get(kind, 0)) == taken0 + 1) @@ -1327,7 +1328,7 @@ func _smoke_shop_pool() -> void: # A 链:白盒抬 GP 保证第二购场景确定构造(直改测试基建——非生产行为) r8.gp = int(r8.gp) + 64 var second: Dictionary = {} - for c in r8.shop_shelf(): + for c in r8.shop.shop_shelf(): if int(c.get("price", 1 << 30)) <= int(r8.gp): second = c break @@ -1335,14 +1336,14 @@ func _smoke_shop_pool() -> void: var prow2: Dictionary = _db.run.shop.pool_row(String(second.get("kind_id", ""))) var expect_p2: int = int(ceil(float(int(prow2.get("base_price", 0))) * pow(_db.run.shop.price_b, float(l2 - 1)) * _db.run.shop.price_a)) Kit.check("A 链爬价 = ceil(base×b^(关序−1)×A^1)(进店第 2 购——%d)" % expect_p2, int(second.get("price", -1)) == expect_p2) - Kit.check("同 visit 第二购受理(A 链软闸在价不在限)", r8.buy_pool_item(String(second.get("kind_id", "")))) + Kit.check("同 visit 第二购受理(A 链软闸在价不在限)", r8.shop.buy_pool_item(String(second.get("kind_id", "")))) else: Kit.check("A 链爬价场景未构造(抬 GP 后货架仍无可购第二卡)——场景绑定失败", false) # ④ 切边零入店(白-box 重抽 40 次——GP 袋/规则改写型特性永不在架;重抽消费 shop_draw 签,场景尾不回写主时间线) var cut_seen := false for i in 40: - r8._shop_draw_shelf() - for c in r8.shop_shelf(): + r8.shop._draw_shelf() + for c in r8.shop.shop_shelf(): if String(c.get("kind_id", "")) == "gp" or String(c.get("kind_id", "")) == "treasure_item": cut_seen = true Kit.check("切边件零入店(40 次重抽零 gp/aux——负交易切边+三轴 doctrine)", not cut_seen) @@ -1352,8 +1353,8 @@ func _smoke_shop_pool() -> void: a8._curio.deck.remove_at(di8) var attack_seen := false for i in 40: - r8._shop_draw_shelf() - if (r8.shop_shelf() as Array).any(func(c): return String(c.get("kind_id", "")) == "attack"): + r8.shop._draw_shelf() + if (r8.shop.shop_shelf() as Array).any(func(c): return String(c.get("kind_id", "")) == "attack"): attack_seen = true break Kit.check("库存尽剔牌库(attack 令牌耗尽——池消耗即上限跨渠道共库 reward→shop 向)", not attack_seen) @@ -1391,6 +1392,74 @@ func _smoke_shop_pool() -> void: Kit.check("终局池余量在域 [0,配额](%s)" % str(ps), ps_ok) +# ---- 商店房 node_entered 载荷(尾波 V1——_enter_node pool/legacy 双发射路径归一后的行为保持断言)---- + +func _test_shop_node_entered_payload() -> void: + print("--- 商店房 node_entered 载荷(pool 含 shelf:true+gp 现值/legacy 无 shelf 键)---") + var autoplay = load("res://sim/level_autoplay.gd") + # pool 态:载荷字典经信号闭包当场取样(字典为引用累积器——lambda 按值捕获标量的绕行惯例) + var cap_pool := {"payload": {}, "gp": -1} + var pool_ok := false + for s in range(42, 92): + var pr = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(s)) + pr.set_hero_override(900, 400) + if not pr.start_adventure(s, {"shop_mode": "pool"}): + continue + var rp = pr.runner() + rp.node_entered.connect(func(node, payload): + if String(node.get("type", "")) == "shop": + cap_pool["payload"] = payload + cap_pool["gp"] = int(rp.gp)) + if not _adv_drive_to_shop(pr, _adv_st(), autoplay): + continue + pool_ok = not (cap_pool["payload"] as Dictionary).is_empty() + if pool_ok: + break + Kit.check("pool 态进店信号可达(seed 扫描 42–91)", pool_ok) + Kit.check("pool 态 node_entered 载荷含 shelf:true〔尾波 V1〕", pool_ok and bool((cap_pool["payload"] as Dictionary).get("shelf", false))) + Kit.check("pool 态载荷 gp = 进店钱包现值(%d)〔尾波 V1〕" % int(cap_pool["gp"]), pool_ok and int((cap_pool["payload"] as Dictionary).get("gp", -1)) == int(cap_pool["gp"])) + # legacy 态:载荷无 shelf 键(两 payload 字典逐字保持的另一半——直构 LevelRunner 链) + var cap_leg := {"payload": {}, "gp": -1} + var leg_ok := false + for s in range(42, 92): + var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(s)) + lr.set_hero_override(900, 400) + lr.start_level(2, s) + lr.node_entered.connect(func(node, payload): + if String(node.get("type", "")) == "shop": + cap_leg["payload"] = payload + cap_leg["gp"] = int(lr.gp)) + var st := _battle_st("normal") + var guard := 60 + while String(lr.level_result) == "" and String(lr.current_node().get("type", "")) != "shop" and guard > 0: + guard -= 1 + if lr.awaiting_choice(): + var ids: Array = lr.next_candidate_ids() + ids.sort() + lr.pick_next_node(int(ids[0])) + continue + var t := String(lr.current_node().get("type", "")) + if t == "reward" or t == "rest": + lr.continue_after_node() + continue + var b = lr.current_battle() + if b == null: + break + var fin := {} + b.battle_finished.connect(func(p): fin["p"] = p) + var g2 := 80 + while lr.current_battle() == b and not fin.has("p") and g2 > 0: + g2 -= 1 + autoplay._autoplay_battle(lr, b, st, null) + if String(lr.level_result) == "" and String(lr.current_node().get("type", "")) == "shop": + leg_ok = not (cap_leg["payload"] as Dictionary).is_empty() + if leg_ok: + break + Kit.check("legacy 态进店信号可达(seed 扫描 42–91)", leg_ok) + Kit.check("legacy 态 node_entered 载荷无 shelf 键〔尾波 V1〕", leg_ok and not (cap_leg["payload"] as Dictionary).has("shelf")) + Kit.check("legacy 态载荷 gp = 进店钱包现值(%d)〔尾波 V1〕" % int(cap_leg["gp"]), leg_ok and int((cap_leg["payload"] as Dictionary).get("gp", -1)) == int(cap_leg["gp"])) + + # 入口→Boss 全路径枚举(DFS 迭代栈——图合法性校验保证单入口无环,路径有限) diff --git a/game/tests/unit/core/meta_progression_forge_test.gd b/game/tests/unit/core/meta_progression_forge_test.gd index f1660fa9..0b3360c8 100644 --- a/game/tests/unit/core/meta_progression_forge_test.gd +++ b/game/tests/unit/core/meta_progression_forge_test.gd @@ -732,12 +732,12 @@ func _smoke_tree_rules() -> void: var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777004), {"tree_rules": []}) # 构造注入(拆件批 T3——setter 群退役) Kit.check("散未激活:商品标价原价(无 economy_spend 键)", - int(lr.current_price({"base_price": 100, "price_increment": 0})) == 100 + int(_db.run.shop.price_level_bonus_at(0))) + int(lr.shop.current_price({"base_price": 100, "price_increment": 0})) == 100 + int(_db.run.shop.price_level_bonus_at(0))) lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777005), {"tree_rules": ["economy_spend"]}) # 构造注入(拆件批 T3——setter 群退役) Kit.check("散激活:商品购买价 ×80%(provisional 折率——floor 偏向玩家、下限 1;刷新价不折=另一计价点零触碰)", - int(lr.current_price({"base_price": 100, "price_increment": 0})) == maxi(1, int(floor(float(100 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount)))) - and int(lr.current_price({"base_price": 1, "price_increment": 0})) == maxi(1, int(floor(float(1 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount))))) + int(lr.shop.current_price({"base_price": 100, "price_increment": 0})) == maxi(1, int(floor(float(100 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount)))) + and int(lr.shop.current_price({"base_price": 1, "price_increment": 0})) == maxi(1, int(floor(float(1 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount))))) # 财富之道·聚:战斗掉落乘算·入账前(编排 _roll_gp_drop——同种子配对对拍 1.5×) var orch3 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005)) Kit.check("聚 rig:基线编排启动(无增益侧)", diff --git a/game/tests/unit/core/pool_case_d_test.gd b/game/tests/unit/core/pool_case_d_test.gd index a00b593a..86aabf8d 100644 --- a/game/tests/unit/core/pool_case_d_test.gd +++ b/game/tests/unit/core/pool_case_d_test.gd @@ -42,9 +42,9 @@ func _init() -> void: Kit.check_quiet(not e.stop().skip_empty_curio(), "空配置闸反例:非空停点拒绝跳过(零自动推进契约不破)〔专长批 T10〕") var lr = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) - lr.set_shop_pool([], Callable(), Callable(), Callable()) - lr._shop_draw_shelf() - Kit.check_quiet((lr.shop_bp_shelf() as Array).is_empty(), + lr.shop.set_shop_pool([], Callable(), Callable(), Callable()) + lr.shop._draw_shelf() + Kit.check_quiet((lr.shop.shop_bp_shelf() as Array).is_empty(), "空配置闸:商店空货架=合法态(牌库空=零扰动不崩)〔专长批 T10〕") quit(Kit.finish("pool_case_d")) diff --git a/game/tests/unit/core/run_time_fatal_test.gd b/game/tests/unit/core/run_time_fatal_test.gd index 8f58cf95..b25e1910 100644 --- a/game/tests/unit/core/run_time_fatal_test.gd +++ b/game/tests/unit/core/run_time_fatal_test.gd @@ -14,6 +14,7 @@ const RngScript := preload("res://core/rng_service.gd") const BoardGeneratorScript := preload("res://core/board_generator.gd") const RewardEconomyScript := preload("res://core/reward_economy.gd") const OrchestratorScript := preload("res://core/battle_orchestrator.gd") +const BoardInstallerScript := preload("res://core/board_installer.gd") const TAG_OK := "board_gen" # 在册合法标签(PURPOSE_TAGS 含;wr7 必须用合法标签——未知标签检查排在播种检查之前) @@ -64,11 +65,15 @@ func _default_form(db) -> bool: # ①wr5(p5):两协作件违约经组装点接线传导 → orchestrator.battle_result 落 "fatal_error" +# (棋盘生成半边:装板域拆件后生成器随 BoardInstaller 件内自建——组装点=引用束 fatal_cb 接线, +# 经新组装点构建后触发生成器违约,封印传导断言不变) func _seal_propagation(db) -> bool: var rng_a = RngScript.new() rng_a.set_master_seed(5) var oc_bg = OrchestratorScript.new(db, rng_a) - oc_bg._board_generator._fatal("测试违约①棋盘生成") + var bi_bg = BoardInstallerScript.new({"db": db, "rng": rng_a, "minesweeper": oc_bg.minesweeper, + "reward_economy": oc_bg._reward_economy, "encounter": oc_bg.encounter, "fatal_cb": oc_bg._fatal}) + bi_bg.generator._fatal("测试违约①棋盘生成") if oc_bg.battle_result != "fatal_error": push_error("[run_time_fatal_test/wr5] 棋盘生成违约未传导封印(battle_result=%s)" % str(oc_bg.battle_result)) return false diff --git a/game/tests/unit/core/treasure_effects_test.gd b/game/tests/unit/core/treasure_effects_test.gd index 13db7148..4f232f6a 100644 --- a/game/tests/unit/core/treasure_effects_test.gd +++ b/game/tests/unit/core/treasure_effects_test.gd @@ -226,11 +226,11 @@ func _init() -> void: # ⑩ 刷新执照(商店免费刷新 +2/店——读数叠加) var lv10 = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) - Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free), + Kit.check_quiet(int(lv10.shop.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free), "刷新执照基线:未装配=表值(%d)" % int(db.run.adventure.shop_refresh_free)) lv10 = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new(), {"attunement": ["refresh_license"]}) # 构造注入(拆件批 T3——setter 群退役) - Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free) + 2, + Kit.check_quiet(int(lv10.shop.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free) + 2, "刷新执照正常:免费刷新 +2/店(读数即本店可用——进店重置生命周期同 knob)") # ⑪ 天眼(3×3 真视+安全格直开/危险格不开/封印格跳过) diff --git a/game/tests/unit/core/tree_rules_v11_test.gd b/game/tests/unit/core/tree_rules_v11_test.gd index ad233a8b..8ecd5030 100644 --- a/game/tests/unit/core/tree_rules_v11_test.gd +++ b/game/tests/unit/core/tree_rules_v11_test.gd @@ -3,7 +3,7 @@ # 在册词表组合;本测试直构 rebuild 同构合成行(与 effect_engine.gd rebuild 树规则段逐字段同源) # 调 validate_trigger_row 断零违例。实现侧扩闭集未呈报时,本断言以在册词表为准转红。 # 聚散两键(economy_hoard/economy_spend)不入事件面:消费点=tree_rules 成员判定 -# (battle_orchestrator._roll_gp_drop GP 掉落乘算 / level_runner._spend_discounted 商品折价), +# (battle_orchestrator._roll_gp_drop GP 掉落乘算 / shop_session._spend_discounted 商品折价), # rebuild 树规则段对两键零合成行——此状态在此被显式断言(旧版对两键取空夹具静默跳过=假绿): # rebuild 实装面行数对照——四键逐键与夹具同行数(rebuild 分支被删/漂移即转红), # 两键断零行(将来被加合成行即转红,提示先复核效果闭集再同步夹具与实现)。 diff --git a/reports/iter42-反哺点-2026-10-29.md b/reports/iter42-反哺点-2026-10-29.md new file mode 100644 index 00000000..d9f07bf1 --- /dev/null +++ b/reports/iter42-反哺点-2026-10-29.md @@ -0,0 +1,24 @@ +# 迭代 42 反哺点清单(2026-10-29) + +要写进 `design/` 的点逐条列全:哪个文件哪一节、改成什么、依据是什么。用户在会话中裁决后归文档批落文,落文后本条清掉(不登记待办清单)。 + +## 反哺点 1:architecture.md:109 ShopSession 行——委托承载句失效+公开面格重写(卡② §6) + +- **位置**:`design/engineering/architecture/architecture.md` 功能层表 ShopSession 行(:109)「对外暴露」格。 +- **失效陈述**:「外部调用面由 LevelRunner 同签名委托承载(外部零变化)」——委托壳已随迭代 42 卡② 全量拆除(19 委托+2 死壳归零)。 +- **改写内容全文(建议落文)**:该格末句改为——「外部调用面=调用方经宿主 LevelRunner 公开成员 `shop` 直连(调用直连不留委托壳,2026-10-26 Middle Man 裁定;迭代 42 拆除);直连消费的方法=shop_products/current_price/purchased_tier/buy_product/player_request_refresh/set_shop_pool/pool_price/shop_shelf/shop_refresh_free_left/shop_refresh_cost/shop_refresh/spend_gp/buy_pool_item/is_shop_pool/shop_bp_shelf/shop_bp_visit_buys(+白盒两入口仅测试侧:_draw_shelf/_spend_discounted)」。 +- **set_shop_pool 调用面改述**:pool 注入经 LevelRunner 构造 ctx 供给(level_runner.gd:101–102 内部直调 ShopSession.set_shop_pool);原委托块注释「AdventureRunner._start_current_level 唯一调用方」系陈旧表述(全仓 grep 证实产品码零其他调用方),随壳删除作废;测试面直连 `shop.set_shop_pool`。 +- **依据**:迭代 42 卡② v1.1 §2–§6(拆除形态权威=architecture.md:154「调用直连:不留中间人委托壳」);实测残留扫描零真残留(实现报告 §7)。 + +## 反哺点 2:architecture.md「测试与质量域」S13 豁免登记段——本批 5 键+既有讨论 5 键按实测值落文(卡① D5/§14.5) + +- **位置**:`design/engineering/architecture/architecture.md`「测试与质量域」S13 豁免登记段(先例=battle_orchestrator 第三条形态:键永久基线内、禁再恶化、豁免不等于了结)。 +- **落文内容(批末实测值,2026-10-29 全量扫描)**: + - board_view.gd max-class-variables = 32(D2 裁悬停域不拆——类变量无拆分动作可归绿,残留面全为契约承载) + - board_view.gd max-public-methods = 27(同 D2——27 个逐类核对全为探针契约面与依赖单向纪律承载面) + - level_runner.gd max-public-methods = 33(D3/D6——委托壳拆除后终值,直连形态下无再降空间) + - level_runner.gd max-class-variables = 30(D3——`shop` 公开成员留宿主,非无效成员) + - adventure_runner.gd max-class-variables = 28(D4 裁断点域不拆——断点协议住冒险生命周期 owner 内本身内聚) + - 既有讨论 5 键同段合并落文:main.gd max-class-variables = 49/tune_sim_controller.gd await-in-loop/board_cell_unit.gd max-class-variables = 30/standee_body.gd max-class-variables = 19/battle_orchestrator.gd max-class-variables = 37(随装板域拆件 38→37 只降,替代原登记值 38)。 +- **依据**:卡① §6 D5 裁决(2026-10-29 用户裁)+§14.5;实测值见实现报告 §3 键账对账表。 +- **配套**:S13 基线重锚由用户发令 `python tools/gdstyle_gate.py --refresh --force`(16→10 键,降幅逾 10%)——重锚后归绿 6 键自然消失,豁免 10 键以新登记值留基线。 diff --git a/reports/iter42-实现报告-2026-10-29.md b/reports/iter42-实现报告-2026-10-29.md new file mode 100644 index 00000000..ae720946 --- /dev/null +++ b/reports/iter42-实现报告-2026-10-29.md @@ -0,0 +1,94 @@ +# 迭代 42 实现报告(2026-10-29) + +批次=gdstyle 尾波清偿批(卡① v2.0)+ level_runner 商店委托层拆除(卡② v1.1)并排编队。执行形态=整迭代单工作区 `g-plan2-iter42`(两卡均未声明并行工作区,D6 裁「同迭代并排编队、先后皆可」=串行任一序),卡序 gdstyle 先(编排既定)、level_runner 后;卡① T6 批末对账与卡② T2 收口合并为迭代总收口一次执行。定义面(roadmap/待办清单/两卡)已随编排提交 14a10e5e 入库,本批无定义面搬运提交。§1 表内提交号为重塑前中间提交;分支史已按一卡一笔重塑(卡①=63e9fbf3/卡②=11cd46a5/收尾=b704f1dc 含并 master 四笔文档面),原史留 backup/iter42-20261029-080300。 + +## 1. 各卡状态 + +| 卡 | 任务 | 状态 | 分支提交 | +|---|---|---|---| +| 卡① gdstyle 尾波清偿 | T0 读数复核与对账 | 完成(16 键现值与基线登记值逐键全数对上,零漂移;行位订正 7 处就地落卡) | 509f41f5(随 T1 同笔) | +| 卡① | T1 board_view 拆分 | 完成(键 1–4 归绿) | 509f41f5 | +| 卡① | T2 _enter_node 载荷合并 | 完成(键 9 归绿) | c47a032d | +| 卡① | T3 装板域拆件 BoardInstaller | 完成(键 10 归绿,类变量 38→37) | adff0772 | +| 卡② level_runner 委托拆除 | T0 调用面直连(中间态) | 完成(改址六件产品码+sim+七件测试,壳在位全绿) | f7e898be | +| 卡② | T1 壳拆除与完整性证明 | 完成(键 7 实测 50→33,残留扫描零真残留) | ae3bd5fc | +| 两卡 | T6/T2 迭代总收口 | 本报告+反哺点清单+全量域闸+独立验收审计+合回呈报 | 见 git log | + +## 2. 实际改动—验收对照表(承两卡 §13/§11 需求侧对照表;结果列为本批实测) + +| 行 | 需求点 | 改动(文件:行) | 验收测试(名称+落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 结果 | +|---|---|---|---|---|---|---|---| +| V1 | 键 1/2/3/4——_paint_cell 拆五段+rebuild 构建器 | board_view.gd `_paint_cell` 拆 `_paint_cell_ink/_paint_cell_base/_paint_cell_facade/_paint_cell_overlays/_paint_cell_fog` 五段+`_build_cell_unit` 出函 | 复用 type_glow_test(48 checks 绿)/theme_projection_test._smoke_type_glow(163 checks 绿)/player_copy_sweep_test(22 checks 绿);render_gate 棋盘 golden 三张 | 单元+探针+golden | BoardView.rebuild()→refresh()→type_glow_probe();board_shot_probe 真实场景帧 | 既有断言面+golden 逐像素 | 绿——三套件全绿(theme_projection 的 163 checks 全过;该套件 FAIL=存量红,见 §6-①,与本改动无涉);golden 有 diff 与 stash 对照逐位同值(L2/L3 0.00%、L1 2.74% 双侧同值——见 §6-③) | +| V3 | 键 9——_enter_node 载荷合并 | level_runner.gd shop 分支双 return 并单 return(payload 变量,两字典逐字保持) | 新增 level_shop_economy_test.gd:_test_shop_node_entered_payload〔尾波 V1〕四断言 | 单元 | LevelRunner.start_level(_external)→pick_next_node 进 shop 节点 | node_entered 信号载荷(pool 含 shelf:true 且 gp=进店钱包现值 27;legacy 无 shelf 键且 gp=现值 30) | 绿——套件 192 checks 全绿(exit 0) | +| V4 | 键 10——装板域拆件 | board_installer.gd 新建;battle_orchestrator 六函数迁出、复位块留薄壳 `_install_and_reset`、三调用点改址、`_board_generator` 成员退役 | 复用 variant_placement_test(49 checks)/round_flow_skill_pool_test(129 checks)/seal_polarity_test(70 checks)/run_time_fatal_test(负套件 exit=1,wr5 棋盘生成半边随拆件改指 BoardInstaller 新组装点——见 §5-7)+run_smoke(15 项) | 单元+集成 | BattleOrchestrator.begin_battle()/player_request_refresh() | 同种子变异布置逐位断言面+全战斗步骤流逐行回放断言(board_gen/variant_place 取号序零漂移由同种子断言钉住)+违约封印传导链(wr5 双半边) | 绿——三套件+负套件+冒烟全绿;键 10 归绿(文件长 916≤1000 键消失)、类变量实测 37 | +| R1 | 调用面直连改址(非行为改动) | 产品码 6 件+sim 1 件+测试 7 件全部调用点 `<表达式>.shop.<方法>` | 既有套件回归两轮全绿:T0 中间态轮(壳在位)+T1 壳删后轮(level_shop_economy 192/hero_ledger/adventure_feats/pool_case_d/treasure_effects/curio_refresh_paid/meta_progression_forge 全套+冒烟) | 单元+接口 | 各套件既有真实入口 | 既有断言逐位不变+壳删后编译通过 | 绿——两轮全绿(全量 65 套件两轮失败清单逐一同值=9 存量红,见 §6-②;七件相关套件两轮全绿) | +| R2 | 壳块拆除(非行为改动) | level_runner.gd :723–725 区块涉委托两句+:751–828 十九委托全量删除(含死壳 2 枚) | 残留扫描零命中(19 方法名全仓 grep,输出留 §7)+T1 套件绿 | 静态+单元 | grep 全仓+套件真实入口 | 扫描面>0(19 名单命中 34 行逐一判类归属)+壳删后零引用可编译 | 绿——34 行全数为他类定义与调用/字典键名/宿主内部 shop. 直连点,零真残留 | +| R3 | 宿主成员公开化(非行为改动) | level_runner.gd:44 `_shop`→`shop`+内部引用 28 处随更名 | 随 R1 两轮套件绿 | 单元 | 套件真实入口 | 内部使用点行为不变 | 绿——词级 `_shop` 全仓 .gd 零残留(件核) | +| R4 | 注释同步六处(非行为改动) | shop_session.gd:4/level_runner.gd 区块句/level_autoplay.gd:335/:356/shop_overlay_view.gd:88/152/tree_rules_v11_test.gd:6 | 随批代码域闸绿(静态扫描覆盖文件头纪律面) | 静态 | —(静态判据) | 静态扫描+评审注记(观测面局限申报:注释无运行时提交面) | 绿——六处已同步,闸 09 见全量域闸回执 | + +反向核对:diff 中行为改动仅 V3 一处(载荷合并,行为保持——载荷字典逐字);其余均为非行为改动(结构整改,判据=零漂移证明如上)。孤儿断言:无(新增断言仅 V3 行四条〔尾波 V1〕,已挂行)。 + +## 3. 键账对账(S13——11 键终值对照卡① §7 预期) + +| 键 | 预期(卡① §7) | 批末实测 | 判定 | +|---|---|---|---| +| 1 board_view _paint_cell 长度 | ≤50 归绿 | 键消失(函数已拆五段,新函数无超标键) | 归绿 ✓ | +| 2 board_view _paint_cell 分支 | ≤8 归绿 | 键消失 | 归绿 ✓ | +| 3 board_view _paint_cell 局部变量 | ≤10 归绿 | 键消失 | 归绿 ✓ | +| 4 board_view allocation-in-loop | 消除归绿 | 键消失(.new() 迁入 _build_cell_unit) | 归绿 ✓ | +| 5 board_view 类变量 | 32 维持豁免终局 | 32 持平 | 豁免终局 ✓ | +| 6 board_view 公开方法 | 27 维持豁免终局 | 27 持平 | 豁免终局 ✓ | +| 7 level_runner 公开方法 | 约 33 降值后豁免终局 | 33(50→33,并排卡兑现,与卡面预测精确一致) | 豁免终局 ✓ | +| 8 level_runner 类变量 | 30 维持豁免终局 | 30 持平 | 豁免终局 ✓ | +| 9 _enter_node 返回数 | 6 归绿 | 键消失 | 归绿 ✓ | +| 10 battle_orchestrator 文件长 | 约 910–920 归绿 | 键消失(实测 916 行) | 归绿 ✓ | +| 11 adventure_runner 类变量 | 28 维持豁免终局 | 28 持平(本批零触碰) | 豁免终局 ✓ | + +豁免通道既有键随批只降核对:battle_orchestrator 类变量 38→37(D1 修正二兑现,登记值不抬)。**零新增键**(全扫描现值 10 键=基线 16−归绿 6,EXTRA 清单为空)。基线重锚=用户发令人工 `python tools/gdstyle_gate.py --refresh --force`(降幅 6/16≈37.5% 逾 10% 须 --force,卡① §14.4)。 + +## 4. 逐拆出单元职责语句(拆分边界纪律①,逐件一行) + +- **BoardInstaller**(新件,board_installer.gd):棋盘安装与变异布置域唯一承载——生成受理→全局作用域变异决议→装板受理→资源摇号资格域组装(roll_board 恰一次)→region 变异决议与连通片布置;单一变化原因=棋盘设置顺序与变异布置算法。 +- **_paint_cell_ink**(board_view 私有段):字形与色牌层——reveal 记录→字形/墨色判定+u.glyph/u.glyph_color 终写+双峰纹理板落牌。 +- **_paint_cell_base**:基底瓦片层——瓦片查表/平色回退 sb+英雄蓝框载体与贴图缺位回退字形。 +- **_paint_cell_facade**:立面投影层——未开墙格南缘立面/轮廓描边码;已开格北缘投影暗带/E 格高墙面。 +- **_paint_cell_overlays**:秘宝/变异叠加层——真视红框/预读徽章/变异角标/奖励徽章/类型微光组号。 +- **_paint_cell_fog**:迷雾层——灯光区外未开格全黑/已开格记忆态灰显。 +- **_build_cell_unit**:逐格构建器——CellUnit 实例化与属性装配(allocation 出循环唯一出函面)。 +- **_install_and_reset**(battle_orchestrator 宿主薄壳):装板受理承接+宿主态复位单点(五处计数清零+板序推进+board_changed 发射+秘宝叠加层退役)。 +- **LevelRunner.shop**(公开成员,非新件):商店件取件唯一形态(委托壳拆除后的直连承载,零新增职责)。 + +## 5. 实现决定记录(代拍板白名单外事项备案) + +1. **run_all.gd `--only` 逗号形态工具缺陷**(卡① T1/T3、卡② T0 验证命令形态适配):`game/tests/run_all.gd` 的 `--only` 解析自 6caf56b5 起把整串 `--only=a,b,c` 当单个词(不按逗号拆分),与文件头注「词[,词…]」文档形态及 `tools/run_all_gates.sh:331` 的逗号拼接传参形态均不符——本批改用逐词重复 `--only=` 形态(语义等价)。批末全量闸不受影响(本批触 battle_orchestrator=PARITY 触面表→横切→02 回落全部套件,不经选跑分支);**建议登记工程改动**:run_all.gd 补逗号拆分(或闸脚本改多旗标传参)。 +2. **BoardInstaller 宿主持有形态**:薄壳 `_install_and_reset` 内逐调用构建 installer(非宿主成员)——T3 验收项「类变量实测 37」要求宿主不新增成员;BoardGenerator 唯一成员 fatal_handler、零跨 generate 调用状态,逐调用构建观察等价(装板取号序由同种子断言钉住,实测全绿)。 +3. **ink→base 段间 bg 传值**:卡① §7 键 1 限「段间只经 u.st/glyph/digit_col 传值」;bg(基底色)无单元场域可承载、其唯一消费方在 base 段回退分支——经 ink 段返回值交 base 段(唯一例外,函数体逐字搬运不受影响)。 +4. **两卡里程碑全量闸并入迭代总收口**:卡① T6 与卡② T1 的「里程碑出口 run_all_gates --code」按 iter 技能闸序列「全量域闸每迭代一次」铁律合并为收口单次跑闸(重塑后)——卡面两处闸义务由同一次跑闸兑付。 +5. **worktree 冷检出 `--editor --quit` 首跑 `--path` 误指仓库根**(未建 .godot 类缓存致首跑解析红)——改指 game 切片目录重刷后正常(106 类)。坑速查条目候选,登记建议随工程批。 +6. **信号契约注释陈旧发现**(不属本批改动面):level_runner.gd:14 node_entered 信号注释 `shop={gp}` 与 pool 态实际载荷 `{gp, shelf:true}` 不符——改动前即如此(本批未触载荷),随本报告呈报,归文档批/后续批处置。 +7. **run_time_fatal_test wr5 改指新组装点(L1 就地修复)**:T3 删宿主 `_board_generator` 成员后,该负面套件 :71 的白盒取数 `oc._board_generator._fatal(...)` 断链(脚本错误中断断言函数→零断言退出 2)=**本批新增红**。卡面 §3 现状考证只查了 battle_orchestrator.gd 全文件(四处使用属实)漏查跨文件测试取数点;§16 L1「探针断言调用面改址漏改——就地修复重跑最窄确认」通道处置:wr5 棋盘生成半边改经新组装点(BoardInstaller 引用束 fatal_cb 接线)构建后触发,断言目标(违约→封印传导)不变、断言零增删(§17 四不违);卡 §9 文件结构图就地补行;复跑 exit=1(负套件期望码)绿。**勘误**:本报告先前稿曾将该套件误列存量红——彼次 stash 对照时工作树处于卡①完成态(HEAD 已含 T3)而非 master 原态,对照被污染;更正后 02 存量红=9 套件与基线登记清单逐项一致。 +8. **PARITY 实跑口径(用户裁决①「批准实跑」的执行记录)**:实跑已执行(终闸 PARITY_APPROVED=1 一轮+归因实验一轮,见 §6-5);终闸回执按既有基线口径出 11=NEED-APPROVAL(未实跑态)——不把本轮对拍红指纹(red-nokeys)记入基线:该指纹形态下任何未来对拍失败(无论差异内容)同指纹不可区分,记入即全量放行后续漂移,属超出本轮授权的新裁决事项;对拍漂移的查因/重锚随 §6-5 归测试债条目。 + +## 6. 存量红清单(master 原状同值复现,非本批新增——stash 对照法逐组取证) + +1. **theme_projection_test**:`_recent_list` 断链(test:805 访问 `camp._recent_list`——成员已随迭代 41 卡② 拆入内嵌类 `_SideState`;iter41 特许移交合回未跑遗留)。该套件 163 checks 本体全绿,FAIL=脚本错误扫描命中。 +2. **全量轮 9 套件红**(T0/T1 两轮与 master 原状逐一同值,与基线登记清单及待办清单「迭代 41 测试债修复」条目逐项一致):config_db_test(cattr 解析)/config_db_fatal_oob_test(退出码 2≠1)/table_structure_guard_test(meta_state)/treasure_items_test(crossrun)/standee_fx_sweep_test(set_trans)/forge_workbench_test(120s 挂死)/tree_requires_level_test(row_by_id Nil)/save_manager_domain_test(_migrate_legacy_save)/theme_projection_test(同①)——形态与迭代 41 卡①(核心数据面结构收敛)卡②(呈现层结构收敛)改动特征吻合;已随基线 80b49c0f 记账(用户 2026-10-29 裁带红合并承接)。初稿曾多列 run_time_fatal_test 一项,经 §5-7 勘误更正为本批新增红(已修)。 +3. **probe_gate --click 三红**:ui_input_probe 超时挂死(exit 124)/ui_struct_probe resume 断言/ui_click_probe 16 行 set_trans 空值(battle_end_overlay 等未触文件)——master 原状同值复现(连错误行数都一致)。 +4. **render_gate golden L1_dungeon 2.74% 超容差**:master 原状同值(L2/L3 双侧 0.00%)——golden 锚落后于 master 现状,重锚须用户批准(本批禁自行换锚)。 +5. **gate 11 PARITY 实跑发现(用户批准①后执行)**:回放确定性对拍锚(2026-10-26 冻结)与现行全冒险重放在三变体(base/attune/shop)全部逐位漂移(RUNSUM 40 行全异+JSONL 行数级差异,如 base/normal 锚 9216 行 vs 今 9221 行)——**master 原状(ee4efad5)单独对拍 base 变体同值复现(今值指纹 8a81884efe51/afc8c07c1bf3 与本批分支逐字相同)=漂移系迭代 40/41 特许合回的存量(两批合并时全量闸停跑、对拍从未执行),本批在其上零新增漂移**。取证件:`game/tests/.gates/parity.log`(终闸实跑)+归因实验记录(master 态 checkout 后 base 变体重跑)。归「迭代 41 测试债修复」条目扩展,重锚/查因裁决归该条目承接(锚重制须用户批准——本批禁自行换锚)。 + +## 7. 残留扫描输出留痕(卡② T1/R2 判据) + +19 方法名全仓 grep(game/ 下 .gd)命中 34 行,逐行判接收方类归属**零真残留**:字典键名(adventure_runner:505/546/591/600、breakpoint_resume_test:196、level_shop_economy_test:1378/1383、sim 两文件共 4 行);他类定义与调用(battle_orchestrator.player_request_refresh 定义 :709+settlement 侧 8 处调用、CurioDeck.pool_stock 经 deck() 9 处、GpWallet.spend_gp 定义 :15);宿主内部直连使用点(level_runner.gd:102/305/306/704 shop. 形态)。产品码与测试面零旧委托调用。 + +## 8. 实机目检总清单 + +本批零玩家可观测改动(结构整改+调用面直连)——**无新增目检项**。既有在册提醒(不阻塞):迭代 41 移交的四项实机目检在体验验证清单㊵。 + +## 9. 遗留读数义务与登记建议 + +1. S13 基线重锚:用户发令 `python tools/gdstyle_gate.py --refresh --force`(§3)。 +2. MC 对拍:不补跑(零数值零数据表零 RNG 流词表改动,卡① §15)。 +3. PARITY gate 11(回放确定性对拍):本批触 PARITY_TOUCH_FACES(battle_orchestrator.gd)——闸记 NEED-APPROVAL,实跑或跳过由用户裁决(卡① §15,卡内不定案);批准实跑=`PARITY_APPROVED=1` 重跑闸。 +4. 工程改动登记建议:§5-1(--only 逗号拆分)+§5-5(坑速查补条)。 +5. 独立验收审计:报告落 `reports/iter42-验收审计-2026-10-29.md`(未参与实现的子代理执行,test-conventions §8.7 四事),结论见该件。 diff --git a/reports/iter42-验收审计-2026-10-29.md b/reports/iter42-验收审计-2026-10-29.md new file mode 100644 index 00000000..a3f505a4 --- /dev/null +++ b/reports/iter42-验收审计-2026-10-29.md @@ -0,0 +1,72 @@ +# 迭代 42 独立验收审计报告 + +- 审计人:独立子代理(未参与本批实现,test-conventions §8.7) +- 审计日期:2026-10-29 +- 审计范围:`git diff ee4efad5..b704f1dc`(master→分支尖,3 提交:63e9fbf3 卡①/11cd46a5 卡②/b704f1dc 收尾) +- 审计输入:卡① 对照表 §13 与 §17 零申报、卡② 对照表 §11 与 §4.7 零申报、实现报告 §2/§5/§6/§7、回执 `game/tests/.gates/receipt.json`(commit b704f1dc,domain=code,scope=full)与 `.gates/` 各闸日志、`%TEMP%\iter42_final_gate.log` +- 方法:只读取证+最窄抽查;不采信实现报告「已全绿」自述,判据全部独立复核(grep/逐 hunk diff/新旧件逐行比对/日志实读) +- 义务边界:未改 `game/` 下任何文件;本报告外未写任何文件 + +## 一、两卡对照表逐行核对(V1/V3/V4/R1/R2/R3/R4) + +| 行 | 核对结果 | 依据(独立复核) | +|---|---|---| +| V1 | **通过** | 三套件在册且函数落点属实:theme_projection_test.gd:183 `_smoke_type_glow`、type_glow_test.gd、player_copy_sweep_test.gd:106 `_sweep_board_glyph`;三文件在本 diff 范围**零改动**(diff 仅含 9 个申报测试文件)。拆分实落:board_view.gd `_paint_cell` 五段(ink/base/facade/overlays/fog)+`_build_cell_unit`(rebuild 循环 `.new()` 出函)。实跑:run_all_62 type_glow all green(48 checks)/run_all_63 theme_projection all green(163 checks,run_all 判 FAIL=脚本错误扫描命中 `_recent_list` 存量红,见第五节)/player_copy all green(22 checks)。golden:render.log L2/L3 双 0.00% OK、L1 2.74% FAIL——与报告 §6-④ 同值。 | +| V3 | **通过** | `_test_shop_node_entered_payload`(level_shop_economy_test.gd:1397)在位;〔尾波 V1〕尾缀断言恰 4 条。真实入口未绕过 `_enter_node`:pool 态经 AdventureRunner.start_adventure+`_adv_drive_to_shop`(autoplay 驱动);legacy 态经 `lr.start_level(2, s)`+awaiting_choice/pick_next_node 循环;run_all_54.log 报错回溯栈实证调用链 `_test_shop_node_entered_payload→pick_next_node(level_runner.gd:653)→_enter_node(:493)`,载荷经 node_entered 信号闭包取样,无直接 emit、无 shop 直调。断言守恒:旧 186 处 Kit.check→新 192 处=+6(新函数恰 6 条:2 可达性+4 尾缀);9 处被改写断言行一一有新对应行,零删除。实跑 run_all_54.log「all green (192 checks)」+4 条尾缀 PASS(gp 读数 27/30 与报告 §2 一致)。 | +| V4 | **通过** | variant_placement_test/round_flow_skill_pool_test/seal_polarity_test 三套件**零 diff**(函数落点 :54/:147、:47 全套、seal 20 函数);实跑 49/129/70 checks 全绿(run_all_30/29/28)。wr5 见第三节。零漂移证明链见第四节。 | +| R1 | **通过** | 产品码 6 件+sim 1 件 diff 逐 hunk 核对:全部为 `.shop.<方法>` 取件路径文本变化,方法名与实参逐字(camp_shop_presenter 14 处/main_adventure_flow 6/main_overlay_flow 2/main.gd 1/curio_interact_controller 1/level_autoplay 6+注释 2)。测试 7 件改址见第三节。七件相关套件 02 当轮全绿:level_shop_economy 192/hero_ledger/adventure_feats/pool_case_d/treasure_effects/curio_refresh_paid/meta_progression_forge 全 PASS(exit=0)。 | +| R2 | **通过** | 19 委托壳在 level_runner.gd diff 中全量删除(逐名清点 19/19);`_shop_draw_shelf` 词级 grep 全仓零命中(壳与白盒转发均消失)。独立残留扫描(19 名单 `grep -w` 于 game/ 全部 .gd):命中全部落四类合法形态——shop_session/gp_wallet/curio_deck/battle_orchestrator 定义与内部调用、字典键名(adventure_runner/level_autoplay/sim/测试断言)、跨类同名(settlement.player_request_refresh、deck().pool_stock)、宿主内部 shop. 直连点(level_runner.gd:102/305/306/704)。产品码与测试面零旧委托调用——零真残留。报告 §7 的 34 行分类账可复算(字典键 11+orchestrator 面 10+CurioDeck 面 9+GpWallet 1+宿主直连 4,按其口径合计 34),与本审计独立扫描分类一致。 | +| R3 | **通过** | level_runner.gd:44 更名 `var shop: ShopSession` 公开成员;词级 `_shop` 于 level_runner.gd 与全仓 .gd 零残留(仅保留件 `_bind_shop`:65 注释/:104 调用/:728 定义——词级 grep 不命中,合规);内部引用点(_init 建件与 pool 注入、breakpoint_fragment、_enter_node、continue_after_node、_bind_shop)全部随更名。 | +| R4 | **通过** | 六处注释同步逐处核对属实:shop_session.gd:4(委托承载句→直连表述)、level_runner.gd 区块注释(涉委托与白盒两句删、直连句换入、件注入说明保留)、level_autoplay.gd:335/356、shop_overlay_view.gd:88/152、tree_rules_v11_test.gd:6(`_spend_discounted` 指址改 shop_session)。 | + +反向核对:diff 全量 27 文件逐类过目——行为改动仅 V3 载荷合并一处(两 payload 字典 `{"gp": gp, "shelf": true}`/`{"gp": gp}` 逐字保持);board_view/battle_orchestrator/board_installer 均为结构搬运(第四节)。孤儿断言:无(新增断言仅 V3 六条,已挂行;两卡就地补行均为行号订正与卡① §9 增 run_time_fatal 一行,属 T0「漂移处就地改卡并记录」授权面,与实现报告 §1/§5-7 申报一致)。 + +## 二、判据绑定面抽查 + +- **V3**:02 闸 run_all_54.log 当轮实跑,〔尾波 V1〕四条 PASS 可 grep(:720/:721/:779/:780);回执 02 指纹 f9865f1a5a44 与 baseline.json 02 指纹**逐字一致**=除 9 个登记存量红外零漂移。 +- **V4**:round_flow_skill_pool_test 129 checks 全绿(run_all_29,全战斗步骤流逐行回放);variant_placement_test 49 checks 全绿(run_all_30,同种子变异布置逐位)。 +- **R1**:hero_ledger/treasure_effects/curio_refresh_paid/meta_progression_forge/pool_case_d/adventure_feats 六套件 02 日志 PASS(exit=0)逐一在案。 +- 冒烟/系统:run_smoke 15 项 0 失败、system_test 69 项 0 失败(smoke_boot.log/system_test.log)。 + +## 三、测试削弱检查(对照两卡零申报) + +**卡① §17 零申报 vs run_time_fatal_test.gd diff(7 行)**:preload 增 BoardInstallerScript 1 行+注释 3 行+wr5 棋盘生成半边 1 行改 3 行(组装点构建+`bi_bg.generator._fatal(...)`);wr5 封印断言(`battle_result != "fatal_error"`→push_error→return false)逐字未动;wr5 资源半边、wr6、wr7、`_default_form` 零触碰;断言数量零增删。 + +独立判定:**改址,非削弱**。依据三点: + +1. 旧取数点 `oc._board_generator` 随宿主成员退役物理消失,无可保留形态——卡① §16 L1「调用面改址漏改就地修复」通道的适用情形。 +2. 新构建束与生产薄壳 `_install_and_reset`(battle_orchestrator.gd:417-418)**逐字同束**(同 fatal_cb=oc_bg._fatal 接线),传导链 generator._fatal→fatal_handler→宿主 _fatal→封印与生产同路。 +3. 束缺键有 assert fail-fast(board_installer.gd:21-22),薄壳漏传 fatal_cb=每次 begin_battle 断言崩、全量套件即红——无静默绿通道。 + +观察注记(不判缺):触发对象由「编排器自有生成器实例」变为「同形态组装点构建的实例」,wr5 不再直接钉宿主薄壳自身的束组装字面——该面由上条第 3 点的全量兜底覆盖,且同种子断言面(V4 三套件)钉住真实 begin_battle 装板路径。 + +**卡② §4.7 零申报 vs `game/tests/unit/` diff 逐 hunk**:8 个测试文件改动全部为调用地址文本变化(`.<方法>`→`.shop.<方法>`;白盒 `_shop_draw_shelf`→`shop._draw_shelf`)+tree_rules_v11 头注 1 行指址;抽全 9 处被改写断言行逐一比对,断言条件与期望值逐字零变化;level_shop_economy 断言总量守恒(186→192,+6 全为新函数)。**零未申报的断言删除/阈值放宽/扫描面缩小/换下层直调**。 + +**static_scan.sh**:S7_B1_SET 与 S8_FILES 各增 board_installer.gd 一行(扩面非缩面),与卡① §9 申报一致;缺位自检通过。 + +## 四、V4「非行为改动」判定依据核对 + +- **六函数迁件零漂移**:board_installer.gd 与 `git show ee4efad5:game/core/battle_orchestrator.gd` :414-549 逐段比对——抽查 `_resolve_global_variant` 与 `_place_variants_apply` 逐行同构(差异仅接收方更名 `_reward_economy`→`reward_economy` 与一处注释行合并,代拍板白名单覆盖面);`_place_variants`/`_variants_fringe`/`_adjacent_to_packs`/装板主序(生成→决议→装板→摇号→布置)同核无差。 +- **RNG 零漂移**:board_gen 流开流时点=装板主序首行(旧 `_install_new_board` 首行=新 `install` 首行);variant_place 流开流时点=候选域构造后、vrow 摇号前(与旧同位);消费序逐字同码;board_installer/battle_orchestrator/level_runner/board_view grep `randomize`/自建 RandomNumberGenerator 零命中。 +- **宿主薄壳时序逐字**:`_install_and_reset`(:416-431)=旧 `_install_new_board` 尾部复位块逐字搬运(_seal_consumed/_seal_miss_seen 清零→_perfect_hit_true/_perfect_hit_false/_perfect_forgiven 清零→_board_from_midround_refresh→_board_seq+=1→board_changed.emit→treasure_inventory.retire_overlays),且保持「装板成功后复位」的先后时序。 +- **三调用点改址**:begin_battle :270/_exit_distribution :683/player_request_refresh :717——与旧 :271/:801/:835 同位同参(during_round 传参逐字);begin_battle 五段驱动序与 attune 族未动(新旧文件对照属实)。 +- **`_board_generator` 成员退役**:新 battle_orchestrator 零 `_board_generator` 字样;类变量 38→37(static_scan.log S13「较登记值恶化 0 条」——只降不升兑现)。 +- **实跑钉住**:variant_placement 同种子断言 02 当轮全绿(第二节);呈现面由 golden L2/L3 双 0.00% 钉住。 + +## 五、回执与呈报一致性 + +- 回执与终闸日志一致:commit b704f1dc/domain=code/scope=full;00 冒烟绿、01 系统测试绿、10 熵增闸绿、11 NEED-APPROVAL(PARITY 触面 battle_orchestrator——报告 §9.3 同述,待用户裁决)。 +- **02**:exit=1、指纹 f9865f1a5a44 与 baseline.json 逐字一致。unit.log FAIL 恰 9 套件,错误形态与报告 §6-② 逐项注(cattr/退出码 2≠1/meta_state/crossrun/set_trans/120s 挂死/row_by_id Nil/_migrate_legacy_save/_recent_list)逐一吻合;56 PASS+9 FAIL=65 套件,与报告「全量 65 套件」计数一致。 +- **03/04/05**:回执记「基线绿→现红」,终闸日志同列三行 ✗;报告解释为 master 原状同值复现(实现者 stash 对照法取证)。本审计按审计指示未复现 stash 实验,只核自洽性:报告口径(probe 三红=ui_input 超时 exit 124/ui_struct resume 断言/ui_click 16 行 set_trans 空值;golden L1 2.74%、L2/L3 双 0.00%)与 probe_click.log/probe_full.log/render.log 实况逐项一致,且涉案文件(battle_end_overlay 等)均不在本批 diff 内。自洽成立;「非本批新增」结论采信实现者 stash 取证。 +- **09**:exit=1、指纹 1a9ad773489f 与基线一致;S13 十键全在基线内、基线外新违规 0、恶化 0——与报告 §3「零新增键(10=16−6)」同述。 + +**表述性瑕疵(不判缺,列已备核)**: + +1. 实现报告 §2 R1 行「失败清单逐一同值=10 存量红」未随 §5-7 勘误同步为 9——T0/T1 两轮当时实况确含本批新增 wr5 共 10,勘误后终态口径 9(以 §6-② 与终闸回执为准)。 +2. 实现报告 §1 各任务「分支提交」列为 squash 前中间提交号(509f41f5 等,reflog 可达、非 b704f1dc 祖先);分支终态 3 提交,内容经 cherry-pick 同入。 +3. static_scan.sh S8 ok 消息「扫描面 B1 文件集 9 个 .gd」为陈旧字面量——S8_FILES 实含 10 文件且缺位自检通过,扫面无缩小;建议工程批顺手订正该消息文本。 +4. level_runner.gd:14 node_entered 信号注释陈旧——实现报告 §5-6 已申报,归文档批/后续批,非本批义务。 + +## 总体结论 + +**通过**。两卡对照表七行(V1/V3/V4/R1/R2/R3/R4)逐行核实:验收测试真实存在、当轮真实跑批执行并通过、尾缀可 grep、真实入口未绕过改动点;判据绑定面抽查全中;测试削弱零申报成立(wr5 判改址非削弱,依据如第三节);V4 零漂移证明链(逐字同构+流名流时点同位+薄壳时序逐字+三调用点同参+同种子断言实跑钉住)成立;回执与呈报自洽。在册遗留事项均待用户发令,不构成本批缺漏:S13 基线重锚(--refresh --force)、PARITY 闸 11 实跑或跳过裁决、golden L1 重锚批准、03/04/05 存量红处置。 diff --git a/reports/需求卡片-gdstyle尾波清偿批-2026-10-29.md b/reports/需求卡片-gdstyle尾波清偿批-2026-10-29.md index 9115ade9..291cf7fe 100644 --- a/reports/需求卡片-gdstyle尾波清偿批-2026-10-29.md +++ b/reports/需求卡片-gdstyle尾波清偿批-2026-10-29.md @@ -34,9 +34,9 @@ | 2 | `board_view.gd\|quality/max-branches\|_paint_cell = 24` | 24/8 | 同上 | | 3 | `board_view.gd\|quality/max-local-variables\|_paint_cell = 22` | 22/10 | 同上 | | 4 | `board_view.gd\|quality/allocation-in-loop`(裸键) | 命中 | `board_view.gd:249`——rebuild() 建格循环内 `_CELL_UNIT.new()` | -| 5 | `board_view.gd\|quality/max-class-variables = 32` | 32/15 | `board_view.gd:49-85`(var 成员 31 + `cell_size` 只读属性 1,以闸实测为准) | +| 5 | `board_view.gd\|quality/max-class-variables = 32` | 32/15 | `board_view.gd:49-85`(var 成员 31)+:93(`cell_size` 只读属性;计数以闸实测为准) | | 6 | `board_view.gd\|quality/max-public-methods = 27` | 27/20 | 全文件:11 setter+6 探针出口+4 几何契约出口+3 入站事件+2 生命周期(rebuild/refresh)+1 访问器(board()) | -| 7 | `level_runner.gd\|quality/max-public-methods = 50` | 50/20 | `game/core/level_runner.gd`——其中商店同签名委托 17 个公开方法(751-829 行)+3 个私有转发(`_bind_shop`:728、`_spend_discounted`:772、`_shop_draw_shelf`:783)+`_shop` 成员(:44) | +| 7 | `level_runner.gd\|quality/max-public-methods = 50` | 50/20 | `game/core/level_runner.gd`——其中商店同签名委托 17 个公开方法(751-828 行)+3 个私有转发(`_bind_shop`:728、`_spend_discounted`:772、`_shop_draw_shelf`:783)+`_shop` 成员(:44) | | 8 | `level_runner.gd\|quality/max-class-variables = 30` | 30/15 | `level_runner.gd` 类成员区(含 `hero_hp`/`gp` 两属性) | | 9 | `level_runner.gd\|quality/max-returns\|_enter_node = 7` | 7/6 | `level_runner.gd:486-513`——shop 分支 pool/legacy 双 return(:509、:511) | | 10 | `battle_orchestrator.gd\|quality/max-file-length = 1035` | 1035/1000 | `game/core/battle_orchestrator.gd`——装板与变异布置域六函数合计约 140 行(:414-549) | @@ -46,7 +46,7 @@ - board_view 公开方法 27 个全是契约面:setter 是组合根注入面(依赖单向纪律),探针出口(cells_probe/diggable_probe/hover_probe/fog_params_probe/type_glow_probe/cell_rect_probe)是探针断言面,几何出口与入站事件是演出与输入契约。可降空间极小。 - battle_orchestrator 装板域六函数全仓零外部调用,仅本文件三处调用(:271 begin_battle、:801 _exit_distribution、:835 player_request_refresh)——拆件不触任何外部调用面。 -- 六函数现行均无超标键(基线无其函数级键);`_install_new_board` 体内 442-451 行为宿主态复位块(五处计数清零+板序推进+board_changed 发射+秘宝叠加层退役)——按第 6 节 D1 裁决留宿主薄壳。 +- 六函数现行均无超标键(基线无其函数级键);`_install_new_board` 体内 443-452 行为宿主态复位块(五处计数清零+板序推进+board_changed 发射+秘宝叠加层退役)——按第 6 节 D1 裁决留宿主薄壳。 - `_board_generator` 全文件仅四处使用(:42 声明、:233 创建、:239 fatal 接线、:416 装板消费)——装板族独占,随件迁移后宿主成员退役(38→37)。 - level_runner 商店委托面被呈现层 5 文件(camp_shop_presenter/main_adventure_flow/main_overlay_flow/main_ui_build/shop_overlay_view)、`game/sim/level_autoplay.gd`、测试 4 套件(level_shop_economy/pool_case_d/tree_rules_v11/hero_ledger)引用——shop_overlay_view 三处命中均为注释投影引用(:15/:88/:152 实查),代码调用面在 presenter/组合根拆件/sim 侧。 - `tools/run_all_gates.sh` PARITY_TOUCH_FACES 含 `game/core/battle_orchestrator.gd`——本批必触面。 @@ -122,6 +122,7 @@ | game/core/battle_orchestrator.gd | 战斗结算编排中枢 | 修改(非行为改动——装板域六函数迁出、宿主薄壳承接复位块、`_board_generator` 成员退役、三调用点改址;验收=对照表行 V4) | | game/core/board_installer.gd | 棋盘安装与变异布置域件(新增,architecture v1.24 功能层在册) | 新增(非行为改动;验收=对照表行 V4) | | game/tests/unit/core/level_shop_economy_test.gd | 关卡图与商店宿主套件 | 修改(仅新增断言 _test_shop_node_entered_payload〔尾波 V1〕;委托面调用点改址归并排卡) | +| game/tests/unit/core/run_time_fatal_test.gd | 运行期违约通道负面套件 | 修改(wr5 棋盘生成半边改指 BoardInstaller 新组装点——T0 复核补查项:卡面 §3 漏查的该测试白盒取数点 `_board_generator` 随成员退役断链,wr5 断言目标违约→封印传导不变) | | game/tests/static_scan.sh | 静态扫描域承载 | 修改(S7 B1 域文件集与 S8 await 文件集增行 board_installer——工程随动,architecture 拆件定稿节 S7/S8 随动条在册) | 新增件「为什么不能复用」理由与唯一落点核查见第 10 节;对不上即卡片不成立。 @@ -163,7 +164,7 @@ **T3 battle_orchestrator 装板域拆件——键 10 归绿(1–1.5 工作日)**(前置:architecture v1.24 轴丙在册) -- [ ] 建 `game/core/board_installer.gd`(引用束注入,第 10 节落位预检),装板逻辑六函数迁件;`_install_new_board` 拆件体=生成→决议→装板→摇号→布置(415-441 行段),宿主薄壳 `_install_and_reset` 承载复位块(442-451 行段:五处计数清零+板序推进+board_changed 发射+秘宝叠加层退役),时序逐字 +- [ ] 建 `game/core/board_installer.gd`(引用束注入,第 10 节落位预检),装板逻辑六函数迁件;`_install_new_board` 拆件体=生成→决议→装板→摇号→布置(415-441 行段),宿主薄壳 `_install_and_reset` 承载复位块(443-452 行段:五处计数清零+板序推进+board_changed 发射+秘宝叠加层退役),时序逐字 - [ ] 宿主三调用点(:271/:801/:835)改调薄壳;删宿主 `_board_generator` 成员(38→37,禁无效成员) - [ ] static_scan.sh S7 B1 域文件集与 S8 await 文件集增行 - [ ] 孤立验证:`-- --only=variant_placement,round_flow,seal_polarity` 全绿+`python tools/run_smoke.py` diff --git a/reports/需求卡片-level_runner商店委托层拆除-2026-10-29.md b/reports/需求卡片-level_runner商店委托层拆除-2026-10-29.md index e555a661..80b25c18 100644 --- a/reports/需求卡片-level_runner商店委托层拆除-2026-10-29.md +++ b/reports/需求卡片-level_runner商店委托层拆除-2026-10-29.md @@ -21,14 +21,14 @@ 跳过设计线的证据指认(需求性工程条目,第〇步核验放行 2026-10-29): - 待办清单条目实况:委托壳=迭代 37 卡④ ShopSession 拆分时立的「冻结公开面」契约过渡件;架构登记=`design/engineering/architecture/architecture.md:109`「外部调用面由 LevelRunner 同签名委托承载(外部零变化)」。 -- 拆除形态权威=`architecture.md:152`「调用直连:不留中间人委托壳(纯转传=Middle Man 反模式,2026-10-26 用户裁定)」。 +- 拆除形态权威=`architecture.md:154`「调用直连:不留中间人委托壳(纯转传=Middle Man 反模式,2026-10-26 用户裁定)」。 - 2026-10-29 写卡会话裁决:改址定式四条(公开成员取件/零调用死壳删/测试直连私有面/契约注释随壳消失)+ spend_gp 归属确认非问题(共享冒险钱包的校验扣款原语,宿主 ShopSession 语义自洽、不迁移)。 ## 3. 现状事实(代码级,开工自行复核) 委托壳块: -1. level_runner.gd:722–724 区块注释(「本类保留同签名委托」句)+:725(「白盒入口——外部调用面与断点取数面零变化」)+:751–828 共 19 个同签名转传方法:shop_products:751/current_price:755/purchased_tier:759/buy_product:763/player_request_refresh:767/_spend_discounted:772/set_shop_pool:778/_shop_draw_shelf:783/pool_stock:787/pool_price:791/shop_shelf:795/shop_refresh_free_left:799/shop_refresh_cost:803/shop_refresh:807/spend_gp:811/buy_pool_item:815/is_shop_pool:819/shop_bp_shelf:823/shop_bp_visit_buys:827。 +1. level_runner.gd:723–725 区块注释(:723「商店房兑付」节头保留;:724「本类保留同签名委托」句+:725「白盒入口——外部调用面与断点取数面零变化」两句随壳删除)+:751–828 共 19 个同签名转传方法:shop_products:751/current_price:755/purchased_tier:759/buy_product:763/player_request_refresh:767/_spend_discounted:772/set_shop_pool:778/_shop_draw_shelf:783/pool_stock:787/pool_price:791/shop_shelf:795/shop_refresh_free_left:799/shop_refresh_cost:803/shop_refresh:807/spend_gp:811/buy_pool_item:815/is_shop_pool:819/shop_bp_shelf:823/shop_bp_visit_buys:827。 2. 零外部调用方死壳 2 枚:`_spend_discounted`(全库仅 tree_rules_v11_test.gd:6 注释提及)、`pool_stock`(外部同名调用全为 CurioDeck.pool_stock,如 adventure_feats_test.gd:545——同名不同件)。 外部调用面(产品码与 sim): @@ -48,7 +48,7 @@ 登记与账面: -9. 架构:architecture.md:109(委托承载句+公开面格)/:152(直连原则);`design/` 全目录仅此一处委托登记(grep 考证 2026-10-29)。 +9. 架构:architecture.md:109(委托承载句+公开面格)/:154(直连原则);`design/` 全目录仅此一处委托登记(grep 考证 2026-10-29)。 10. gdstyle 基线:tools/gdstyle_baseline.txt:11–13——level_runner 三键 max-class-variables=30/max-public-methods=50/max-returns|_enter_node=7。 11. 能力清单:game/tests/README.md 旅程段四「GP 钱包/escrow/商店收口/共享池」行真实入口列引用委托名——已随本卡写作更新为直连形态(写卡段义务,实现批禁写该文件)。 12. shop_session.gd:4 头注「公开面由 LevelRunner 同签名委托承载」句随拆除失效(商店件唯一注释级 diff,见契约 3)。 @@ -65,7 +65,7 @@ ## 5. 范围边界 -做:壳块与死壳删除;`_shop`→`shop` 公开化(含内部引用点);产品码/sim/测试调用点改址;注释同步六处(shop_session.gd:4、level_runner.gd:722–724 区块句、level_autoplay.gd:335、shop_overlay_view.gd:88/152、tree_rules_v11_test.gd:6);level_runner 头注涉委托表述随批核查同步。 +做:壳块与死壳删除;`_shop`→`shop` 公开化(含内部引用点);产品码/sim/测试调用点改址;注释同步六处(shop_session.gd:4、level_runner.gd:723–725 区块涉委托两句〔删 :724–725、留 :723 节头〕、level_autoplay.gd:335、shop_overlay_view.gd:88/152、tree_rules_v11_test.gd:6);level_runner 头注涉委托表述随批核查同步。 不做(去向): @@ -136,7 +136,7 @@ game/tests/unit/core/tree_rules_v11_test.gd 头注同步 1 处【非行 - 取用:T0 全调用点直连形态在位。 - 产出:终态 level_runner(壳归零);T2 的闸与审计对象。 -- [ ] 删除 level_runner.gd:722–724 区块注释涉委托句+:725 契约注释(保留「商店房兑付」节头与件注入说明)+:751–828 十九个委托全量(含死壳 `_spend_discounted`:772/`pool_stock`:787) +- [ ] 删除 level_runner.gd:723–725 区块注释中涉委托与白盒两句(:724–725;保留 :723「商店房兑付」节头与 :727 件注入说明)+:751–828 十九个委托全量(含死壳 `_spend_discounted`:772/`pool_stock`:787) - [ ] shop_session.gd:4 头注委托承载句更新(「外部调用面=调用方经宿主公开成员 `shop` 直连」表述) - [ ] 验证:`python tools/run_smoke.py` 绿+全单元套件绿(壳删后绿=T0 改址无遗漏的编译级证明) - [ ] 残留扫描:grep 19 个方法名于 `game/`(排除 shop_session.gd 本体定义、level_runner.gd 内部 `shop.` 使用点、跨类同名方法——BattleOrchestrator.player_request_refresh 与 CurioDeck.pool_stock 按现状事实 5 清单判接收方类归属)——产品码与测试面零命中,输出留实现报告 @@ -160,7 +160,7 @@ game/tests/unit/core/tree_rules_v11_test.gd 头注同步 1 处【非行 | 需求点 | 改动(文件:行/行为) | 验收测试(名称+落点) | 层级 | 真实入口 | 判据绑定面 | 能力行 | |---|---|---|---|---|---|---| | R1 调用面直连改址(非行为改动——调用目标/实参/返回/副作用逐项同前,仅取件路径文本变化) | §9 结构图产品码 6 件+sim 1 件+测试 7 件全部调用点 | 既有套件回归两轮全绿:T0 中间态轮+T1 壳删后轮(level_shop_economy/hero_ledger/adventure_feats/pool_case_d/treasure_effects/curio_refresh_paid/meta_progression_forge 全套+冒烟) | 单元+接口 | 各套件既有真实入口(直构 LevelRunner/AdventureRunner 链) | 既有断言逐位不变(行为零改动正向证据)+壳删后编译通过(改址无遗漏) | 既有:GP 钱包/escrow/商店收口/共享池;既有:奖励三选一候选/刷新付费/呈现 | -| R2 壳块拆除(非行为改动——纯转传方法与契约注释删除,行为承载在 ShopSession 不变) | level_runner.gd:722–724 句+:725+:751–828(含死壳 `_spend_discounted`:772/`pool_stock`:787) | 残留扫描零命中(T1 静态步)+T1 套件绿 | 静态+单元 | grep 全仓(19 方法名清单固定)+套件真实入口 | 扫描面>0 前置(grep 命令对 19 名单非空可执行)+壳删后零引用可编译;命中行按接收方类归属判别(现状事实 5 跨类同名清单) | 既有:GP 钱包/escrow/商店收口/共享池 | +| R2 壳块拆除(非行为改动——纯转传方法与契约注释删除,行为承载在 ShopSession 不变) | level_runner.gd:723–725 区块涉委托两句(:724–725)+:751–828(含死壳 `_spend_discounted`:772/`pool_stock`:787) | 残留扫描零命中(T1 静态步)+T1 套件绿 | 静态+单元 | grep 全仓(19 方法名清单固定)+套件真实入口 | 扫描面>0 前置(grep 命令对 19 名单非空可执行)+壳删后零引用可编译;命中行按接收方类归属判别(现状事实 5 跨类同名清单) | 既有:GP 钱包/escrow/商店收口/共享池 | | R3 宿主成员公开化(非行为改动——私有 `_shop` 更名公开 `shop`,装配与读写路径不变) | level_runner.gd:44+内部引用 :99/:102/:104/:305–306/:506–507/:704/:729 | 随 R1 两轮套件绿 | 单元 | 套件真实入口 | 同 R1(内部使用点行为不变) | 既有:GP 钱包/escrow/商店收口/共享池 | | R4 注释同步六处(非行为改动——注释文本,无运行时可观测面) | shop_session.gd:4/level_runner.gd:722–724/level_autoplay.gd:335/shop_overlay_view.gd:88/152/tree_rules_v11_test.gd:6 | 随批代码域闸绿(静态扫描覆盖文件头纪律面) | 静态 | —(静态判据) | 观测面局限申报:注释无运行时提交面,判据=静态扫描+评审注记 | 既有:GP 钱包/escrow/商店收口/共享池 |