# VERTICAL SLICE - NOT FOR PRODUCTION # Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2) # Date: 2026-09-10 # 战斗结算编排中枢:轮次生命周期唯一 owner · 五段结算(授予→增益/减益→战斗→衰减→判定——2026-09-15 # 格面资源批,道具窗口/道具段随可选类删除下线)· 授予段极性路由(「动作 × 格型」矩阵:开安全格/标中旗 # = 正向,开雷格/标错旗 = 负向;结算队列 + 抵消过滤——标旗后被撤旗以轮末在场旗为基线不判)· # 10 类行协议可回放步骤流(零 await;随机消费三处皆封闭流——开战遭遇骰〔初始化随机〕+ 技能摇号 # behavior_roll〔回合内、开轮时点、步骤流之外——敌人技能池批 2026-09-16〕+ 护甲等级掷点〔回合内、 # 步骤流之内〕;同种子同输入逐位复现)· 高光双落(完美排雷 ×2 / 标中旗 highlight)· # Waiting 相位结算触发(不依赖事件到达序)。 # GP+商店房实施批(2026-09-21):授予段极性路由增 GP 型兑付(effect_id gp_gain/gp_loss → 战斗内缓冲 # escrow 记账,不入钱包不入增益容器);战斗结束分支掷 gp_drop 封闭流(每战斗末恰一 scope)出 # gp_dropped = 掷定 + level_gp_bonus + escrow 随 battle_finished 上抛——战斗域零 GP 状态持久。 # 伤害管线单据化批(2026-09-24 用户三裁决,06 台账 148):伤害兑付全部收口 伤害控制器(DamageController # 单点管线)——本类六调用点(主角先手/敌方伤害技/棋盘克制直伤/群伤/反噬/地形变异)退化为开单点 # (流程决定何时开单给谁开单;单据怎么算归控制器)。结账时机开关在控制器 policy:IMMEDIATE(A=开单 # 即结,逐位等价迁移前行为——回切位)/ SEGMENT(B=攒批到段界 flush:战斗段尾统一兑现)。 # 地图伤害账殿后批(案C 2026-10-06 用户裁——「主角先手」歧义质询触发):授予段不再当场兑付任何伤害—— # 全部伤害形态单据经 _submit 暂存闸入 _deferred_board(保授予段事件序),战斗段「主角先手→逐敌还手」后 # 补提交统一结账;主角死于敌方还手 → 暂存单整体不兑,逐单留「未生效(主角已倒下)」行(forfeit=hero_down)。 # 死亡事件 = 单位容器 unit_died(单位自行宣布)→ 控制器作废该敌未结单据〔死亡即不动手〕/ 主角死终止兑付。 # 切片登记:① 事件总线载体 = 直连信号(实现期 ADR 项,provisional)② Waiting 相位刷新通道 2026-09-15 用户拍板整体 # 拆除(仅 Active 挖掘期受理)③ 效果行协议 = 授予段逐 grant 行 + 增益/减益段续存实例组合行。 # 实现 GDD:design/gdd/systems/战斗循环/战斗结算.md(v0.7 格面资源批——registry 追溯锚) class_name BattleOrchestrator extends RefCounted signal step_row(row: Dictionary) signal battle_finished(payload: Dictionary) # {result, hero_hp, gp_dropped}——hero 在 despawn 前捕获(呈现层销毁后读状态的时序防护);gp_dropped = 战斗掉落合计(GP+商店房实施批 2026-09-21) signal round_started(round_index: int) signal board_changed() signal settlement_snapshot(snap: Dictionary) # 每结算末尾的全量状态快照(playtest_logger 消费——JSONL 数值分析) # 明细流水行型协议(战斗结算 Core Rule;9 类——2026-09-20 伏击单轮上限退役,溢出未生效行 overflow 随之下线; # 2026-09-15 格面资源批由 15 类收拢,清单改动须同步 battle_log_view 的 _format_row 与本文档头注): # 效果行 effect · 伤害行 damage · 治疗行 heal · # 奖励未生效行 reward_ineffective · 标旗未中行 flag_miss · 施放行 cast(敌人技能池批由蓄力行 charge 改型)· 衰减行 decay · # 判定行 verdict · 高光行 highlight # 行来源断言豁免集合(行型层)——判定段 / 衰减段本就无棋盘来源,勿一刀切误炸 const SOURCE_EXEMPT_TYPES := ["decay", "verdict"] # 行来源断言豁免集合(来源类别层)——事实来源是单位 / 清图而非棋盘格; # fx_reveal=专长揭示掷格(落空行无真实格〔迭代 21 存量红修复——a57489e9 落地件三处空来源 # 触发行来源断言,51 行/冒烟,两轮确定性复现〕) const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "fx_reveal"] var db: ConfigDb var rng: RngServiceCore # E1 封装收口(2026-09-13):协作对象 board_generator/enemy_system/status_effects/reward_economy 私有化——跨层读取一律经 presentation_snapshot()/ # diagnostic_snapshot() 成文出口(呈现层链式穿透 settlement._enemy_system.hero_unit() 的根因收口)。 # minesweeper/action_economy 保持公有 = 架构文档 API Boundaries 的 HUD框架消费面(意图提交 settle_batch + compute_move_path # 只读;ap/rap/相位读取)——非本文档登记的契约面,勿增新用途。 var _board_generator: BoardGenerator var minesweeper: MinesweeperRules var action_economy: ActionEconomy var _enemy_system: UnitContainer var _status_effects: StatusContainer var _reward_economy: RewardEconomy var treasure_inventory: TreasureItems # 迭代 7 T3——LevelRunner 注入的关卡域库存引用; # 每战新建本类,库存不归本类所有——注入面 var _damage: DamageController # 伤害控制器(伤害管线单据化批 2026-09-24——单据唯一兑付管线) var encounter: Dictionary[String, Variant] = {} var round_index := 0 var battle_result := "" # "" / "victory" / "defeat" / "fatal_error" var escrow := 0 # 战斗内 GP 缓冲(战斗域中间态——begin_battle 清零;gp_gain/gp_loss 兑付累记;随 gp_dropped 上抛即弃,不入钱包) var _facts_reveals: Array[Dictionary] = [] var _facts_flags: Array[Dictionary] = [] var _facts_path := "" var _settling := false var _defer_board_damage := false # 案C 暂存闸(2026-10-06 用户裁:地图奖励伤害兑付迁战斗段末——怪物还手后) var _deferred_board: Array[Dictionary] = [] # 授予段暂存伤害单据(保授予段事件序;战斗段怪物还手后按序补提交统一结账) var _refresh_used_this_round := 0 # 每轮刷新计数(Active 相位合计,兜底上限 = ActionEconomyTable.refresh_cap_per_round) var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用) var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换来的图 = true(清图奖励守卫——刷新图不发清图奖励) var _flag_consumed := {} # 兑付资格以雷格实例为键(板级;换图重置) # 同调装配运行态(迭代 11 第①批 T5——begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动) var attunement: Array = [] # 装备中同调件 item_id 集 var _attune_charges_ref: Dictionary = {} # 充能账本共享引用(迭代 11 第①批 T6——use_treasure 消费直改) var _attune_safe_seen := 0 # 掘进蓄能 battle 域安全开格计数(进度呈现取数面;积点账本=效果引擎计数器 dig_charge_bank——行 triggers counter_spend 订阅同账本) # ---- 效果引擎扩签运行态(迭代 20 专长实装批 T1——新效果类型施加面;空订阅零扰动)---- var _fx_hero_ac_base := -1 # 主角 AC 基线快照(enc.hero_armor_class;-1=表值兜底未解析) var _fx_ac_round := 0 # ac_bonus 本轮累积器(封印桩形态——round_start 清零) var _fx_ac_battle := 0 # ac_bonus 本战累积器(石肤形态——cap 封顶,begin_battle 清零) var _fx_adventure_counters_ref: Dictionary = {} # adventure 域计数器共享引用(T6——attune_charges 先例:begin_battle 回灌 rebuild、battle 末 counters_export 回写) var _fx_bonus_ref: Dictionary = {} # 冒险加算区共享引用(bonus_delta 战末汇入——血税双向账) var _fx_bonus_delta := {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} # 本战 bonus_delta 累积(战末并入 _fx_bonus_ref 后清零) var _fx_new_resource_types := {} # 本战已见资源类型集(地质学家 fact new_resource_type 判定——battle 域) var _fx_pending_reveals: Array[Dictionary] = [] # 揭示效果待兑队列(见血开窍/斩首学者/地质学家/跳格采掘——授予段末统一兑付,防轮 facts 迭代中追加重入) var _fx_revealing := false # fx 揭示兑付中标记(_on_cell_revealed 缓冲旁路——fx 揭示的奖励由兑付点直接入账,防二次缓冲二次兑付) var _fx_insurance := 0 # 完美保险余量(迭代 20 T4——本战标错豁免次数;消费于 flag_miss 首见判定) # 特性运行态(内容数据驱动扩展批 T3——relic 持有集经 enc.feats 注入:效果行为=数据行 triggers 经 # 效果引擎分发;魔矢每战首次计数 battle 域。空集=零扰动) var _feat_first_dig_used := false # 效果引擎(内容数据驱动扩展批 T1——发布订阅解释器;begin_battle 从持有集/装备集重建订阅。 # T3 起施加面全量切换:发射点 dispatch 返回命中,发射点按组合规则施加——旧 feat_flags 旗注入消亡) var _fx: EffectEngine # 事件发射薄面(18 发射点统一入口——订阅空=零循环零扰动;确定性同步分发零 RNG) func _fx_emit(event: String, payload: Dictionary) -> Array: if _fx == null: return [] return _fx.dispatch(event, payload, Callable(self, "_fx_fact")) # round_start 发射+施加(内容数据驱动扩展批 T3——grant_ap mode:queue=当轮入队,行动经济轮切换 # 即兑现;计数器 round_start 重置点在 dispatch 内生效。调用点两处:begin_battle 首轮 / 轮始段续轮。 # 迭代 20 T1——payload 扩 ap_exhausted_last〔一滴不剩 when 面〕+ac_bonus 本轮累积器清零) func _emit_round_start() -> void: _fx_ac_round = 0 _sync_hero_ac() for h in _fx_emit("round_start", {"round": round_index, "ap_exhausted_last": action_economy.ap_exhausted_last_round()}): if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue": action_economy.queue_ap_delta(int(h.get("value", 0))) # ac_bonus 累积后同步主角 AC(石肤/封印桩消费面——defender_ac 经 set_hero_ac_override 单点; # 基线 = enc 口径,-1(表值兜底态)时解析为表值后叠加,守恒口径与 begin_battle 注入一致) func _sync_hero_ac() -> void: if _fx_hero_ac_base < 0: _fx_hero_ac_base = int(db.hero.armor_class) _damage.set_hero_ac_override(_fx_hero_ac_base + _fx_ac_round + _fx_ac_battle) # ac_bonus 施加(迭代 20 T1——mode=round 本轮累积器〔封印桩,round_start 清零〕/ # mode=battle 本战累积器〔石肤,cap 封顶;缺省 battle〕;cap=0/缺省=无上限) func _apply_ac_bonus(h: Dictionary) -> void: var mode := String(h.get("mode", "battle")) var cap := int(h.get("cap", 0)) if mode == "round": _fx_ac_round += int(h.get("value", 0)) else: _fx_ac_battle = _fx_ac_battle + int(h.get("value", 0)) if cap <= 0 else mini(cap, _fx_ac_battle + int(h.get("value", 0))) _sync_hero_ac() # 揭示效果入队(fx_reveal 掷格兑付延后到授予段末——防轮 facts 迭代中追加重入;见血开窍/ # 斩首学者〔经伤害控制器 fx_reveal_request 回调〕/地质学家/跳格采掘共用) func _fx_enqueue_reveal() -> void: _fx_pending_reveals.append({"round": round_index}) # 揭示效果兑付(授予段末统一——fx_reveal 封闭流从未揭示安全格掷 1 格全揭示; # 信息红线=只揭真格〔雷格不入候选〕;空候选=零兑付留归因行;揭示奖励经 _grant_routed # 正向路由直接入账——_fx_revealing 旁路 _on_cell_revealed 缓冲防二次兑付) func _flush_pending_reveals(ri: int) -> void: if _fx_pending_reveals.is_empty(): return var board: BoardData = minesweeper.current_board() while not _fx_pending_reveals.is_empty(): _fx_pending_reveals.pop_back() var cands: Array = [] for c in board.all_cells(): if not minesweeper.is_revealed(c) and not board.is_mine(c): cands.append(c) if cands.is_empty(): _emit_row("fx_reveal", "fx_reveal", Vector2i(-1, -1), ri, "揭示落空(无未揭示安全格)", 0, {}, "") continue var rg: RandomNumberGenerator = rng.open_stream("fx_reveal") var cell: Vector2i = cands[rg.randi_range(0, cands.size() - 1)] _fx_revealing = true var ok: bool = minesweeper.fx_reveal(cell) _fx_revealing = false if not ok: _emit_row("fx_reveal", "fx_reveal", Vector2i(-1, -1), ri, "揭示落空(格子不可揭)", 0, {}, "") continue var snap = _reward_economy.query_full_snapshot(cell) if snap is Dictionary: _grant_routed(snap, "positive", "fx_reveal", cell, ri, "fx_reveal") _emit_row("fx_reveal", "fx_reveal", cell, ri, "专长揭示", 0, {}, "fx_reveal") # 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数) func _fx_fact(fact: String): match fact: "charge_layers": return action_economy.charge_layers "nest_count": var n := 0 for eff in _status_effects.query_active("enemy"): if String(eff.effect_id) == "weaken": n += 1 return n "round_index": return round_index "battle_index": return int(encounter.get("battle_index", 1)) # 关卡域战斗序(1 基——level_runner enc 携带;开局姿态窗条件面) "flags_count": var hits_n := 0 for fevt in _facts_flags: if bool(fevt.get("hit", false)): hits_n += 1 return hits_n "hp_ratio": var hu: Dictionary = _enemy_system.hero_unit() return int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1)))))) # 百分比 int(0-100——when lte 算子 int 截断比较口径) return null func _attune_has(item_id: String) -> bool: return attunement.has(item_id) # 装配集只读出口(迭代 19 卡④——呈现桥消费面:组合根每战装配链判「装配集含 fog_lantern」→ # board_view.set_fog_lantern。_attune_has 契约化出口,save v6 只读访问器先例;private 面内部调用 # 零改动。只读零副作用——呈现侧判定专用,非结算写入口) func attune_has(item_id: String) -> bool: return _attune_has(item_id) # 装备件表值读取(charge_n 等——TreasureItemTable 行;未装配不读表。magnitude 读数面随 T4 # 秘宝订阅迁移退役——行值经 value_from 在 rebuild 定影,零运行期按件取值) func _attune_row(item_id: String) -> Dictionary: return db.treasure_item_row(item_id) if _attune_has(item_id) else {} # 特性求值(内容数据驱动扩展批 T3——_feat_flags_for 挂点求值退役:效果行为=数据行 triggers 订阅, # 效果引擎 dispatch 分发;旧 23 hook 枚举与旗注入消亡。流派档位质变=FeatsEngine 纯函数求值 # (档位机制本身系引擎基建非内容效果),两运行态注伤害控制器:档一 rider+1 / 档二 rider 可暴击) var _flag_miss_seen := {} # 标旗未中行去重(每安全格实例一次) var _perfect_hit_true := 0 # 板级完美排雷计数(install 重置) var _perfect_hit_false := 0 var _hero_atk := 0 # 主角攻击基值快照(encounter.hero_atk;E4 收口——表值运行期不可变) func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: db = db_ref rng = rng_ref _fx = EffectEngine.new(db_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) _damage = DamageController.new(db, rng, _enemy_system, _status_effects) _damage.emit_row_cb = _emit_row # 行协议归属本类(9 类行协议 + 来源断言不外泄) _damage.set_fx(_fx) # 效果引擎注入(attack_commit/clamp_check/armor_roll/kill/damage_taken 发射面) action_economy.set_fx(_fx) # dig_paid/charge_full 发射面 _reward_economy.set_fx(_fx) # reward_roll 发射面 minesweeper.cell_revealed.connect(_on_cell_revealed) minesweeper.flag_judged.connect(_on_flag_judged) minesweeper.round_end_board.connect(_on_round_end) action_economy.round_end_stopped.connect(_on_round_end) # ---- 战斗开始序列(编排专章①:门 → 增益状态 → 敌人系统 → install+roll → 行动经济)---- func begin_battle(enc: Dictionary) -> bool: if round_index != 0: push_warning("[战斗结算] begin_battle 重复受理") return false encounter.assign(enc) escrow = 0 # 战斗域零 GP 持久(GP+商店房实施批——每场战斗缓冲从零起算) _damage.set_hero_ac_override(int(enc.get("hero_armor_class", -1))) # 主角护甲合流入参(伤害控制器 defender_ac 消费——口径同原 _defender_ac) db.set_reload_gate(true) if not _status_effects.notify_battle_start(): _fatal("增益状态 开域被拒(编排违例)") return false # 遭遇骰(难度曲线与Boss GDD Core Rule 8——2026-09-12 用户拍板):行有 unit_count_max 时按 [unit_count, max] 均匀骰; # 2026-09-14 敌方攻击区间批扩展:行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值 # (每拍抖动否决——数字可读性锚;中点 = 行值)。encounter 封闭流恰一 scope(数量骰先、atk 骰后); # 挖掘决策域之外——P1 合规;敌人系统 零随机纪律保持(收 override 即确定值)。 # 遭遇骰(难度曲线与Boss GDD Core Rule 8——2026-09-12 用户拍板):行有 unit_count_max 时按 [unit_count, max] 均匀骰; # 2026-09-14 敌方攻击区间批扩展:行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值 # (每拍抖动否决——数字可读性锚;中点 = 行值)。encounter 封闭流恰一 scope(数量骰先、atk 骰后); # 挖掘决策域之外——P1 合规;敌人系统 零随机纪律保持(收 override 即确定值)。 # 逐行骰换代(2026-10-07 行集多行批 Q8 用户裁 + P1 补充):混编行集**每行各掷自己的区间骰**—— # 先按行序逐行数量骰、再按行序逐行 atk 骰(单行行集 = 既有一次数量骰 + 一次 atk 骰,判定条件、 # 开流条件、消费序、消费次数逐位一致——零内容期随机流零变化;区间宽度为零的行不掷 = 「定值不骰」 # 既有口径;atk 区间逐行同构 = 行值语义完整,代拍板随批留痕:属「每行一骰」预批准语义内的 # 行字段区间骰逐行化,非新增随机源形态)。 var erowset_pre: Array = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth)) if erowset_pre.is_empty(): _fatal("敌人表 行集解析为空(%d,%d)" % [int(enc.level_index), int(enc.node_depth)]) return false var count_overrides: Array = [] var atk_overrides: Array = [] var needs_count := false var needs_atk := false for r in erowset_pre: if int(r.get("unit_count_max", int(r.get("unit_count", 1)))) > int(r.get("unit_count", 1)): needs_count = true if r.has("atk_min"): needs_atk = true if needs_count or needs_atk: var estream: RandomNumberGenerator = rng.open_stream("encounter") if needs_count: for r in erowset_pre: var lo: int = int(r.get("unit_count", 1)) var hi: int = int(r.get("unit_count_max", lo)) count_overrides.append((lo + estream.randi_range(0, hi - lo)) if hi > lo else lo) if needs_atk: for r in erowset_pre: if r.has("atk_min"): atk_overrides.append(int(r.atk_min) + estream.randi_range(0, int(r.atk_max) - int(r.atk_min))) else: atk_overrides.append(-1) # E4 收口:加成经 encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径) _hero_atk = int(enc.get("hero_atk", int(db.hero.atk))) if not _enemy_system.spawn(int(enc.level_index), int(enc.node_depth), int(enc.hero_hp), count_overrides, _hero_atk, int(enc.get("hero_max_hp", -1)), atk_overrides): _fatal("敌人系统 spawn 被拒") return false if not _install_new_board(): return false _reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险) # 同调装配运行态注入(迭代 11 第①批 T5——enc.attunement 携带;off=空集零扰动。淬毒 N 注入 # 伤害控制器、偏运 N 注入地图战斗资源——均须先于首板 install) attunement = (enc.get("attunement", []) as Array).duplicate() _attune_charges_ref = enc.get("attune_charges", {}) # 共享 dict 引用(level/adventure 同账本——use_treasure 扣减直改) _attune_safe_seen = 0 # 淬毒/偏运注入拆除(内容数据驱动扩展批 T4——两件被动改 triggers 订阅行:淬毒=clamp_check # modify 窗口、偏运=reward_roll bias_rewards;value 行字段 value_from 定影,零 begin_battle 注入) # 特性运行态(内容数据驱动扩展批 T3——enc.feats 携带持有集:效果行为=数据行 triggers 经效果 # 引擎分发;流派档位=FeatsEngine 纯函数求值两运行态注伤害控制器。魔矢每战首次开格计数同步归零) # 迭代 20 T1——扩签运行态就位:AC 基线快照、累积器清零、adventure 计数器/加算区共享引用接收 _fx_hero_ac_base = int(enc.get("hero_armor_class", -1)) _fx_ac_round = 0 _fx_ac_battle = 0 _fx_new_resource_types = {} _fx_pending_reveals = [] _fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} var bonus_ref = enc.get("bonus_ref", null) _fx_bonus_ref = bonus_ref if bonus_ref is Dictionary else {} var counters_ref = enc.get("adventure_counters", null) _fx_adventure_counters_ref = counters_ref if counters_ref is Dictionary else {} _damage.fx_reveal_request = Callable(self, "_fx_enqueue_reveal") _damage.fx_ac_request = Callable(self, "_apply_ac_bonus") var feat_ids: Array = enc.get("feats", []) as Array # 迭代 20 T3 换代(V14 旧实现摘除)——流派档位质变随重设计退役(奇物.md v1「不设流派标签、 # 不设档位机制」):FeatsEngine 求值与 rider_tier 注入拆除,散件单体效果=唯一形态 _damage.rider_tier_bonus = 0 _damage.rider_tier_crit = false _feat_first_dig_used = false # 树 rule 键消费(迭代 12 T10 → 内容数据驱动扩展批 T3 换代:Fork 激活侧效果=效果引擎合成 # 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1),旧直调拆除。其余 rule 键 # =stat 面投影不入战斗(T8 表内容 provisional 口径披露)) var tree_rules: Array = enc.get("tree_rules", []) as Array # 效果引擎订阅重建(内容数据驱动扩展批 T1——每战从持有集/装备集重建,中途获得下一战生效; # 树规则合成订阅同面装载。装载序=feats 表序→treasure 表序→树规则,写死保种子复放。 # 迭代 20 T6——adventure 域计数器从共享引用回灌〔跨战持久〕) _fx.rebuild(feat_ids, attunement, tree_rules) _fx.counters_seed(_fx_adventure_counters_ref) # 迭代 10 奖励重构批——跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现 # 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后 # 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节) var ap_bonus: int = int(enc.get("hero_ap_bonus", 0)) + int(enc.get("voucher_time_sand", 0)) # 迭代 20 T4——时沙同通道(首轮池增量,不改上限) if ap_bonus > 0: action_economy.queue_ap_delta(ap_bonus) # 迭代 20 T4——储备金袋(节点级 RAP 注入)与完美保险(标错豁免次数)接收 var voucher_rap: int = int(enc.get("voucher_rap", 0)) if voucher_rap > 0: action_economy.grant_reserve_ap(voucher_rap) _fx_insurance = int(enc.get("voucher_insurance", 0)) # 工兵晨令直调拆除(内容数据驱动扩展批 T4——round_start grant_ap(mode:queue) 订阅行承载, # when round_index ≤ effective_rounds 行字段 value_from 定影;发射位先于轮切换=当轮兑现) # round_start 发射+施加(内容数据驱动扩展批 T3——发射位先于行动经济轮切换:树规则 grant_ap # mode:queue 当轮入队、notify_round_start 即兑现(先手压制=首轮 AP+1 语义锚,与迁移前 # begin_battle 直调 queue_ap_delta 逐位一致);计数器 round_start 重置点同点生效) round_index = 1 _emit_round_start() if not action_economy.notify_round_start(): _fatal("行动经济 首轮 notify_round_start 被拒") return false _roll_round_actions() # 开轮摇号(敌人技能池批——首轮同样摇;先于 round_started,挖掘期动画位可读) round_started.emit(round_index) return true 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: return false # 全局作用域变异决议(首发批 2026-10-04——装板序内前置于 roll_board:图级偏斜旋钮须在资源 # 摇号前定型;全局门控与板无关〔节点身份+unlock+board_types〕,确定性取用零取号)。 # boss/elite 图命中全局型 → 写 board_variants(_place_variants 见非空即跳 region=替换制); # 全局池空 → 板级字段留空(_place_variants 原路走 region=兜底回落合法态)。 _resolve_global_variant(board) var res: Dictionary = 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 # 地形变异 布置段(2026-09-19 用户拍板棋盘设置顺序第 4 步:生成棋盘 → 打开状态初始化 → # 战斗资源格设置 → 变异格设置)——布置在初始展开与资源格之后,变异天然不落已开区 _place_variants(board) _flag_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8) _flag_miss_seen.clear() _perfect_hit_true = 0 # 板级完美计数随 install 重置 _perfect_hit_false = 0 _board_from_midround_refresh = during_round # 轮内刷新装板 = 刷新图(清图奖励不发);常规换图/开战装板 = false _board_seq += 1 board_changed.emit() if treasure_inventory != null: treasure_inventory.retire_overlays() # 迭代 7 T3——叠加层随图消亡(换图/刷新图装板点;已花道具不返还) return true # ---- 全局作用域变异 决议(首发批 2026-10-04——装板序内 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 = db.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)) # ---- 地形变异 布置段(首批类型定稿批 2026-09-19——棋盘设置顺序第 4 步)---- # 候选池门控(unlock_level ≤ 关卡序号 + board_types 与本地形交集,∅ 通配)经 config_db.variant_pool; # 池空 = 合法态,静默零布置(框架 P0 口径不变)。零特判:本段只写格子变异数据快照,不改扫雷规则本体。 # 候选格域 = 安全格 − 初始展开区(伤害型 placement=closed_only 的结构保障——布置发生在初始展开之后, # 已开格天然排除);雷格不吃变异(挖雷一层惩罚 + 变异伤害双层叠罚污染逐行归因,首版排除——provisional)。 # 每图至多一种类型(候选池内均匀选一,混型布置归因复杂留待后续批);数量 = round(候选池格数×percent) + # (关卡序号−1)×per_level_bonus——基数为候选池而非总格数(2026-09-21 用户拍板基数修正批:初始展开 # 占全板 56–67%,总格数基数使有效密度膨胀 4–6 倍、L1 小板极端全池覆盖;percent 语义 = 占可挖格比例, # 关卡递增由 per_level_bonus 绝对格数承载);连通片生长(8 邻),单片 ≤ pack_max(2×2 / 1×4 / 1×3+1 等形)。 # 替换制(全局作用域变异首发批 2026-10-04):板级 board_variants 非空 = 本图已带全局型,跳过 region # 布置(每图一型的板级推广:普通图=region,Boss/精英图=global);空 = 原路径(含 Boss 图全局池空回落)。 func _place_variants(board: BoardData) -> void: if not board.board_variants.is_empty(): return var pool: Array = db.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 = pool[stream.randi_range(0, pool.size() - 1)] var total: int = int(round(float(candidates.size()) * float(db.variation.percent_of_cells) / 100.0)) \ + (int(encounter.level_index) - 1) * int(db.variation.per_level_bonus) total = mini(total, candidates.size()) var placed := {} while placed.size() < total: var pack_size: int = mini(total - placed.size(), stream.randi_range(2, int(db.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 = [seed_c] var pack_set := {seed_c: true} while pack.size() < pack_size: 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) 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)] # 候选格是否与其他已布片(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: # 轮 facts 缓冲不设相位守卫——轮末最后一挖的揭示事件在 request_dig 翻相之后才到(事件序: # cell_revealed 批 → flag_judged 批 → round_end),phase==Active 守卫会丢最后一挖的事实; # initial 通道事件(install 初始展开——封锁格)resource=false,进入授予段被资格检查自然跳过。 # fx_reveal 通道事件旁路缓冲(迭代 20 T1——专长揭示的奖励由 _flush_pending_reveals 兑付点 # 直接入账,此处缓冲会导致二次兑付)。 if battle_result == "" and not _fx_revealing: if bool(evt.get("resource", false)): # 身份快照随缓冲捕获(批次隔离——轮内刷新后 mapping 已换批,授予必须用缓冲期身份, # 防旧图已挖格在轮末错拿新图同坐标身份)。雷格同样携带(雷安同池——负向兑付的来源格) var snap = _reward_economy.query_full_snapshot(evt.cell) if snap != null: evt = evt.duplicate() evt["reward_snapshot"] = snap _facts_reveals.append(evt) func _on_flag_judged(evt: Dictionary) -> void: _facts_flags.append(evt) func _on_round_end(_payload: Dictionary) -> void: _facts_path = String(_payload.get("path_tag", "")) _maybe_settle() func _maybe_settle() -> void: if _settling or battle_result != "": return if action_economy.phase != action_economy.PHASE_WAITING: return _settle() # ---- 每轮结算序列(五段——段序骨架冻结于 SettlementTable.segment_order)---- func _settle() -> void: _settling = true _defer_board_damage = true # 案C:授予段伤害单据只暂存不结账(2026-10-06 用户裁——兑付时点=战斗段末) var ri := round_index # ① 授予段:结算队列(开格/旗判定/清图)→ 极性路由逐批兑付(伤害形态单据暂存——见 _submit 案C 闸)。 # 判定基线 = 轮末在场旗(发射面已过滤——被挖开的旗已随挖掘擦除零兑付,插旗不可逆批 2026-09-19; # 开格事实不可撤)。 for evt in _facts_reveals: if not bool(evt.get("resource", false)): continue # 封锁格(initial 通道)恒无资源——不入账(战斗结算 义务②) var snap = evt.get("reward_snapshot") if not (snap is Dictionary): continue if evt.kind == "safe": if bool(evt.get("dug_under_flag", false)): # 插旗不可逆四情况④(2026-09-19 用户拍板):插安全格再挖开 = 零惩罚零奖励——正向兑付 # 作废(归因行在案);标空判定已随旗擦除自然缺席(被挖的旗不在轮末在场基线)。 _emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(插旗格直接挖开——零惩罚零奖励)", 0, {}, "") continue # dig_reveal 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价: # 乘区收集 → 正向兑付 → 计数器 → 追加产出(深挖)→ 奥术矢。 # 迭代 20 T1——payload 扩三事实:is_edge_cell〔边陲矿工〕/is_blank_zero〔跳格采掘〕/ # new_resource_type〔地质学家——快照 cell_type 首见判定,见后集登记〕) var board_b: BoardData = minesweeper.current_board() var snap_pre = evt.get("reward_snapshot") var ctype := String((snap_pre as Dictionary).get("cell_type", "")) if snap_pre is Dictionary else "" var is_new_type := ctype != "" and not _fx_new_resource_types.has(ctype) if is_new_type: _fx_new_resource_types[ctype] = true var dr_hits := _fx_emit("dig_reveal", {"cell": evt.cell, "round": ri, "is_first_dig_of_battle": not _feat_first_dig_used, "terrain_kind": _harmful_variants(evt.get("variant_ids", []) as Array), "is_edge_cell": evt.cell.x == 0 or evt.cell.y == 0 or evt.cell.x == board_b.width - 1 or evt.cell.y == board_b.height - 1, "is_blank_zero": board_b.digit_of(evt.cell) == 0, "new_resource_type": is_new_type}) var pos_m := 0 var neg_m := 0 var combo_cap := -1 for h in dr_hits: if String(h.get("kind", "")) != "yield_mult": continue pos_m += int(h.get("mult", 0)) neg_m += int(h.get("cost_mult", 0)) if int(h.get("cap", 0)) > 0: combo_cap = int(h.cap) # 矿脉直觉连击上限 _grant_routed(_feat_yield_adjust(snap, true, pos_m, neg_m, combo_cap), "positive", "safe_cell", evt.cell, ri, "") # 计数器执行(连击计数——加法族,乘区收集后执行=本格兑付用旧值,与迁移前逐位一致) for h in dr_hits: if String(h.get("kind", "")) == "counter_add": var set_v = h.get("set", null) _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) # 揭示效果入队(迭代 20 T1——地质学家/跳格采掘 reveal_tiles 订阅面;授予段末 # _flush_pending_reveals 统一掷格兑付) if not _hits_of(dr_hits, "reveal_tiles").is_empty(): _fx_enqueue_reveal() # 追加产出(深挖钻头 v2——本格双份制:grant_material 命中=配额内追加第二份;标签随行) for h in dr_hits: if String(h.get("kind", "")) == "grant_material": var dd_snap = _reward_economy.grant(evt.cell) if dd_snap is Dictionary: _grant_routed(dd_snap, "positive", "safe_cell", evt.cell, ri, "", String(h.get("label", ""))) # 奥术矢(魔能共鸣——每战首次开安全格追加魔法矢;任意真族单次生效) var spell_hits: Array = _hits_of(dr_hits, "spawn_spell") if not spell_hits.is_empty() and not _feat_first_dig_used: _feat_first_dig_used = true var spell_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell", "奥术矢", "board", int((spell_hits[0] as Dictionary).get("value", 0)), DamageController.TARGET_FRONT_ALIVE, ri, evt.cell, "magic") _submit(spell_t, false) # 掘进蓄能计数(迭代 11 同调批 T6 → 内容数据驱动扩展批 T4:积点账本迁效果引擎计数器 # dig_charge_bank——行 triggers counter_spend(treasure_used) 受理闸同账本。模判定留编排器侧: # 「每 N 格积 1 点」在发射点施加架构下无法行内表达先增后判(when 于 dispatch 预评估), # 逐位等价优先——词汇表边界注记随 commit 留痕;charge_n 读行零字面量) if _attune_has("dig_charge"): _attune_safe_seen += 1 var cn_b: int = int(db.treasure_item_row("dig_charge").get("charge_n", 999)) if cn_b > 0 and _attune_safe_seen % cn_b == 0: _fx.counter_add("dig_charge_bank", 1) elif evt.kind == "mine": # 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴), # 标中不发(旗已随挖掘擦除、不入轮末判定基线)。 # mine_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价: # 减半收集 → 负向兑付〔带减半系数〕→ 反打开单 → 连击清零计数器。 # 迭代 20 T1——发射前移+payload 扩 dug_under_flag〔破封一击〕;新增施加面: # damage_reduce〔安全帽耗层减伤——value_counter 层值,一次清零〕/ heal〔血蜜〕/ # blast_scaled〔陷阱爆发——雷伤×倍率全体敌〕/ bonus_delta〔血税〕/ reveal_tiles〔见血开窍〕) var mine_power := int((snap as Dictionary).get("power", 0)) var mh_hits := _fx_emit("mine_hit", {"cell": evt.cell, "round": ri, "damage": mine_power, "dug_under_flag": bool(evt.get("dug_under_flag", false)), "hp_ratio": int(round(100.0 * float(int(_enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(_enemy_system.hero_unit().get("max_hp", 1))))))}) var halve_pct := 0 var mine_reduce := 0 for h in mh_hits: match String(h.get("kind", "")): "damage_mult": halve_pct += int(h.get("value", 0)) "damage_reduce": if h.has("value_counter") and String(h.get("value_counter", "")) != "": mine_reduce += _fx.counter(String(h.get("value_counter", ""))) # 安全帽——层值减免 else: mine_reduce += int(h.get("value", 0)) # 背水——定值减免 "heal": var hu_heal: Dictionary = _enemy_system.hero_unit() hu_heal["hp"] = mini(int(hu_heal.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_heal.get("max_hp", 1)))) "bonus_delta": var axis := String(h.get("axis", "")) if _fx_bonus_delta.has(axis): _fx_bonus_delta[axis] += int(h.get("delta", 0)) "blast_scaled": var blast_v := maxi(int(db.settlement.damage_floor), int(float(mine_power) * float(h.get("mult", 1)))) var bs_t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_blast_scaled", "陷阱爆发", "board", blast_v, DamageController.TARGET_ALL_ALIVE, ri, evt.cell, "", 0, "", {}) _submit(bs_t, false) if mine_reduce > 0: snap["power"] = maxi(0, mine_power - mine_reduce) # 减伤落快照(负向兑付按减后值——安全帽每层减 1) for h in mh_hits: if String(h.get("kind", "")) == "counter_add": var set_v = h.get("set", null) _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) if not _hits_of(mh_hits, "reveal_tiles").is_empty(): _fx_enqueue_reveal() # 见血开窍——每颗雷一次揭示(授予段末统一兑付) _grant_routed(_feat_yield_adjust(snap, false, 0, 0, -1), "negative", "mine_hit", evt.cell, ri, "", "", halve_pct) for h in mh_hits: if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "reflect": var nct := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_counter", "惊巢反制·反弹", "board", int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, evt.cell, "", 0, "", {}) _submit(nct, false) # 地形变异 兑付段(授予段尾——触发枚举 on_open:挖开即伤、chain 通道同口径;伤害单据案C 暂存到 # 战斗段末兑付〔2026-10-06 用户裁〕;信息型 hide_number 不进结算——呈现层读取,见 board_view 数字遮蔽; # 雷格恒无变异——布置段排除,双层叠罚不存在;插旗格直挖照吃变异伤害——地形后果同 mine_hit 口径, # 非奖励不作废;吞噬流资源兑付〔荆棘编织/地脉补偿〕中伤害形态行同样经案C闸暂存,非伤害形态照常当场) for evt in _facts_reveals: var vids: Array = evt.get("variant_ids", []) for vid in vids: var vrow: Dictionary = db.variation.row_by_id(String(vid)) if String(vrow.get("effect", "")) != "flat_damage": continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面) # 地形变异伤害单据(伤害管线单据化批 2026-09-24)——无类型必中,下限保底在控制器管线内 # 迭代 20 T1——damage_taken 发射前移(单据开立前):damage_reduce〔背水——hp_ratio when # 面〕先收集再按减后值开单;counter_add〔保险丝/石肤〕+ ac_bonus〔石肤〕照常施加; # grant_material〔荆棘/地脉〕移到单据提交后(与迁移前兑付值序一致——收益不因减伤变化) var dt_amount := int(vrow.get("effect_value", 0)) var dt_hits := _fx_emit("damage_taken", {"kind": "terrain", "amount": dt_amount, "source": String(vid), "cell": evt.cell, "round": ri, "terrain_kind": String(vid), "hp_ratio": int(round(100.0 * float(int(_enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(_enemy_system.hero_unit().get("max_hp", 1))))))}) for h in dt_hits: match String(h.get("kind", "")): "damage_reduce": dt_amount = maxi(0, dt_amount - int(h.get("value", 0))) "ac_bonus": _apply_ac_bonus(h) "counter_add": var set_v = h.get("set", null) _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) var vt := _damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)), "board", \ dt_amount, DamageController.TARGET_HERO, ri, evt.cell, "", 0, "", {"variant_id": String(vid)}) _submit(vt, false) for h in dt_hits: if String(h.get("kind", "")) == "grant_material": var tw_snap = _reward_economy.grant(evt.cell) if tw_snap is Dictionary: _grant_routed(tw_snap, "positive", "safe_cell", evt.cell, ri, "", String(h.get("label", ""))) for evt in _facts_flags: _judge_flag(evt, ri) # 战旗军规开单段(迭代 12 T3 标旗流——flag_judged 批后统一开单:轮末每面标中旗对最前存活敌 # 结算 board_typed 必中伤害〔复用零新配方;「旗所在巢穴敌」简化为 default_target 最前存活敌—— # 台账披露待追认〕;完美布防=本轮旗批全中(hit>0 且 miss==0)时旗伤 ×mult——全对推理奖励)。 # 伤害单据案C 暂存战斗段末兑付。 # flag_settle 发射+施加(内容数据驱动扩展批 T3——战旗 open_ticket + 完美布防 modify_ticket # 改写窗口:旗伤基值×完美倍率后逐旗开单,与迁移前算序一致) var fs_hits := 0 var fs_misses := 0 for fevt in _facts_flags: if bool(fevt.get("hit", false)): fs_hits += 1 else: fs_misses += 1 var fs_fx := _fx_emit("flag_settle", {"flags_hit_all": fs_hits > 0 and fs_misses == 0, "flags_count": fs_hits, "round": ri}) var banner_hits: Array = [] for h in fs_fx: if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "attack": banner_hits.append(h) if not banner_hits.is_empty(): var strike_value := int((banner_hits[0] as Dictionary).get("value", 0)) var flag_mult := 1 for h in fs_fx: if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "flag_mult": flag_mult *= maxi(1, int(h.get("value", 1))) if fs_misses == 0 and fs_hits > 0 and flag_mult > 1: strike_value *= flag_mult # 完美布防:同轮全中旗伤倍增(全对推理奖励) for fevt in _facts_flags: if not bool(fevt.get("hit", false)): continue var st_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_flag_strike", "战旗军规", "board", strike_value, DamageController.TARGET_FRONT_ALIVE, ri, fevt.cell, "normal") _submit(st_t, false) # round_end 发射+施加(内容数据驱动扩展批 T3——每轮结算收口:熔火之躯反打单开立〔open_ticket # 即时执行〕+积层清零〔counter_add set〕;整备转蓄力由 finish_round_stop 承载——停手侧 AP 已在 # request_stop 记账,转存决策=grant_ap(mode:transfer) 订阅定夺,无订阅走 RAP 转存缺省) var re_fx := _fx_emit("round_end", {"round": ri}) for h in re_fx: if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "blast": var mg_layers := _fx.counter(String(h.get("per", ""))) var mg_total := int(h.get("value", 0)) * mg_layers if mg_total > 0: # 迭代 20 T9——source_class 中性名(旧件名 feat_magma_body 随熔火之躯退役;per-counter 反打通道保留泛用面) var mg_t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "fx_blast_counter", "积层反噬 ×%d" % mg_layers, "board", mg_total, DamageController.TARGET_ALL_ALIVE, ri, minesweeper.hero_pos(), "", 0, "", {}) _submit(mg_t, false) for h in re_fx: if String(h.get("kind", "")) == "counter_add": var set_v = h.get("set", null) _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) # 迭代 20 T1——round_end 新施加面:grant_gp〔爆破会计/收盘——escrow 战斗域缓冲,随 gp_dropped 上抛〕/ # grant_ap mode:queue〔封印回响——返 1 AP 下轮,queue_ap_delta 通道与轮始加成同面〕 for h in re_fx: if String(h.get("kind", "")) == "grant_gp": escrow += int(h.get("value", 0)) _emit_row("effect", "fx_grant_gp", Vector2i(-1, -1), ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": escrow}, "") elif String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue": action_economy.queue_ap_delta(int(h.get("value", 0))) action_economy.finish_round_stop(_fx != null and _fx.has_effect_on("round_end", "grant_ap", "transfer")) if _facts_path == "cleared": _grant_clear_reward(ri) _flush_pending_reveals(ri) # 迭代 20 T1——揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入) _damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界) # ② 增益/减益段(续存实例组合行;本结算新授予的已在授予段逐 grant 出行——增益状态 义务⑦) _emit_composition_effects("hero", ri) _emit_composition_effects("enemy", ri) # ③ 战斗段(调用序:主角先手 → 逐敌还手 → 地图伤害账殿后〔案C 2026-10-06 用户裁〕;hero hp=0 短路) _combat_segment(ri) # ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减) var k: int = _status_effects.decay_round() _emit_row("decay", "", Vector2i(-1, -1), ri, "状态衰减", k, {}, "") # ⑤ 判定段(防御性优先级 hero 先判——双死结构不可达的纵深防御) var verdict := "continue" if int(_enemy_system.hero_unit().get("hp", 0)) <= 0: verdict = "defeat" elif _enemy_system.query_alive("enemy").is_empty(): verdict = "victory" _emit_settlement_snapshot(verdict) # 全量状态快照(despawn 前组装——终局含 result) if verdict == "defeat": _emit_row("verdict", "", Vector2i(-1, -1), ri, "战斗失败", 0, {}, "") _battle_over("defeat") return if verdict == "victory": _emit_row("verdict", "", Vector2i(-1, -1), ri, "战斗胜利", 1, {}, "") _battle_over("victory") return _emit_row("verdict", "", Vector2i(-1, -1), ri, "下一轮次", 0, {}, "") _exit_distribution() _settling = false # ---- 授予段 helpers(极性路由)---- # 开格产出乘区(内容数据驱动扩展批 T3 参数化——贪婪矿灯/矿脉直觉/赌徒的血签事件收集器注入): # positive=power ×(1+静态 mult% + 矿脉直觉连击加成 min(cap,combo)×10%);negative=power ×(1+cost%)。 # 全零收集器=原快照原样返回(零扰动)。combo 读数=效果引擎 dig_combo 计数器(safe+1/mine 清零, # 跨轮累计——与迁移前 _feat_combo 逐位一致)。 func _feat_yield_adjust(snap: Dictionary, positive: bool, pos_m: int, neg_m: int, combo_cap: int) -> Dictionary: var combo_val := _fx.counter("dig_combo") if _fx != null else 0 if positive and pos_m <= 0 and combo_cap <= 0: return snap if not positive and neg_m <= 0: return snap var out: Dictionary = snap.duplicate() var power := int(out.get("power", int(_reward_economy.polarity_row(String(out.get("cell_type", "")), "positive" if positive else "negative").power))) var pct := pos_m if positive else neg_m if positive and combo_cap > 0: pct += mini(combo_cap, combo_val) * 10 out["power"] = maxi(1, int(round(float(power) * (1.0 + float(pct) / 100.0)))) return out # 伤害型变异过滤(terrain_kind fact 取数面——variation 表 effect=flat_damage 判据; # 🔴 R4 甲永不对冲边界=仅限伤害型,信息型(hide_number 族)永不出现在本集) func _harmful_variants(vids: Array) -> Array: var out: Array = [] for vid_v in vids: var vid := String(vid_v) var vrow: Dictionary = db.variation.row_by_id(vid) if String(vrow.get("effect", "")) == "flat_damage": out.append(vid) return out # 邻接伤害型变异收集(terrain_kind fact 取数面——flag_hit 发射点用:标中旗相邻八格的伤害型变异集; # 🔴 R4 甲永不对冲边界=仅限伤害型,信息型〔hide_number 族〕不入集) func _harmful_adjacent(cell: Vector2i) -> Array: var out: Array = [] var board: BoardData = minesweeper.current_board() if board == null: return out for n in board.neighbors8(cell): out.append_array(_harmful_variants(board.variant_ids(n))) return out # 效果命中过滤(按效果类型取命中子集——发射点分桶施加面) func _hits_of(hits: Array, kind: String) -> Array: var out: Array = [] for h in hits: if String((h as Dictionary).get("kind", "")) == kind: out.append(h) return out # 「动作 × 格型」矩阵的执行面:snap = 缓冲期冻结身份快照(cell_type = 该格装板期摇定的类型身份), # polarity 由动作决定(开安全格/标中旗 = positive,开雷格/标错旗 = negative), # 兑付行 = 奖励字典(类型, 极性)行。返回 true = percent 行实际生效(flat 行恒 false——兑付结果由行协议承载)。 # ---- 秘宝(use_treasure 四步受理编排保留给同调主动件复用——迭代 11 第①批 T7:过渡授予路由 # _grant_treasure 随掉落/停点渠道退役拆除;能力件注入归 LevelRunner.set_attune_charges 骑库存面)---- # use_treasure 四步受理(registry use_orchestration 规范序——唯一调用权归本类;序即原子: # ① 相位 gate(Active——行动经济 phase getter)→ ② validate_use(目标校验读 排雷玩法 只读面,零消耗)→ # ③ 逐 kind 应用(顺序钉死:空间先、ap_grant 后——effect_kinds 列表序非运行时语义)→ ④ consume。 # 拒绝全部发生在 ①②(后两步合法输入下无失败路径);使用零 AP 成本(不经 try_spend)。 # 结算明细无道具行(GDD——效果即时可见:overlay 呈现+AP 条刷新;AP 过账本 grant_ap 走 _reject 面)。 func use_treasure(handle: int, target: Vector2i) -> Dictionary: if treasure_inventory == null: return {"ok": false, "reason": "库存未接线"} if action_economy.phase != action_economy.PHASE_ACTIVE: return {"ok": false, "reason": "非挖掘期(使用窗口=Active)"} var board: MinesweeperRules = minesweeper # 恒等包装退役(2026-10-02 修复批——use_treasure 直引公有成员) var v: Dictionary = treasure_inventory.validate_use(handle, target, Vector2(board.width, board.height), func(c: Vector2i): return not board.is_revealed(c)) if not bool(v.ok): return v # ② 拒绝零消耗 # treasure_used 发射+施加(内容数据驱动扩展批 T4——使用主动秘宝受理面:counter_spend 订阅=受理闸 # (掘进蓄能积点账本),消耗成功才受理;积点不足拒绝零消耗。按 item_id 特判随本批拆除) var tu_hits := _fx_emit("treasure_used", {"item_id": String(v.item_id), "target": target}) for h in tu_hits: if String(h.get("kind", "")) == "counter_spend": if not _fx.counter_spend(String(h.get("counter_id", "")), int(h.get("value", 1))): var cn_info: int = int(db.treasure_item_row("dig_charge").get("charge_n", 0)) return {"ok": false, "reason": "掘进蓄能:安全格积点未达(%d/%d)" % [_attune_safe_seen % cn_info if cn_info > 0 else 0, cn_info]} var kinds: Array = v.kinds if kinds.has("mine_reveal"): treasure_inventory.apply_overlay("mine_reveal", v.resolved) if kinds.has("reward_preview"): treasure_inventory.apply_overlay("reward_preview", v.resolved) if kinds.has("ap_grant"): if not action_economy.grant_ap(int(v.magnitude)): return {"ok": false, "reason": "行动点加量被拒(相位防御)"} if kinds.has("ap_charge_safe"): # 掘进蓄能兑付(迭代 11 同调批 T6——临时 AP 当轮生效,grant_ap 同 sapper_order 通道) if not action_economy.grant_ap(int(v.magnitude)): return {"ok": false, "reason": "行动点加量被拒(相位防御)"} if kinds.has("cell_removal"): # 爆破工兵包兑付(迭代 13 T8/D6——中立移除雷格:未揭示/无旗/是雷过滤归排雷玩法; # P1 甲邻接数字冻结零重算、清图分母零涉(雷格不入分母);空集拒绝零消耗—— # consume 之前 return(拒绝面注记扩「③内零消耗分支」),拒绝 reason 可呈现) var blasted: Array = board.remove_mines_in(v.resolved) if blasted.is_empty(): return {"ok": false, "reason": "爆破工兵包:3×3 区域内无可移除雷格(须未揭示且未插旗)"} var rec: Dictionary = treasure_inventory.consume(handle) # 同调主动件充能扣减(迭代 11 第①批 T6——共享账本直改:adventure/level/settlement 同一字典引用) if not rec.is_empty() and String(rec.get("source_class", "")) == "attunement": var used_iid: String = String(rec.get("item_id", "")) if _attune_charges_ref.has(used_iid): _attune_charges_ref[used_iid] = maxi(0, int(_attune_charges_ref[used_iid]) - 1) return {"ok": true, "item_id": v.item_id, "resolved": v.resolved, "record": rec} # aux 解锁门控转发(迭代 7 T5——LevelRunner 注入;E1/E3 封装:外部经公有面,禁直读 _reward_economy) func set_treasure_unlocked(ids: Dictionary) -> void: _reward_economy.set_treasure_unlocked(ids) func _grant_routed(snap: Dictionary, polarity: String, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "", halve_pct: int = 0) -> bool: # 迭代 11 第①批 T7——aux 授予分支随掉落/停点渠道退役拆除(reward 词表第十型 treasure_item 保留但零投放: # 无 aux 行入池、CURIO_KINDS 词表已除名);剩余类型走极性字典路由。 var row: Dictionary = _reward_economy.polarity_row(String(snap.get("cell_type", "")), polarity) if row.is_empty(): _fatal("极性字典缺行: %s %s" % [String(snap.get("cell_type", "")), polarity]) return false if polarity == "positive": row["power"] = int(snap.get("power", int(row.power))) # 区间行快照 power(身份批 roll 值)覆盖表值——授予期零随机口径(E14) # 标中兑付溢价(迭代14②判式换代批 T4——条款3 条件二旗溢价形态 a,2026-10-04 用户裁 P3): # δ 读 settlement.flag_hit_premium(percent 域 125=×1.25 provisional);100=无溢价直通零扰动。 # 兑付公式一处=本块:只乘标中旗正向(flat power 乘后入单据/治疗/排队;percent 幅度经 # premium_pct 贯穿 _apply_percent_row 应用与呈现同乘)——标空/挖格/清图不乘,雷安同池冻结不动。 # 取整=四舍五入(与伤害管线 round_direction=floor 分域——registry clause3_inequality 随批登记; # 1 点量纲行 ac_up/action 取整后不变=整数粒度地板)。 if source_class == "flag_hit" and int(db.settlement.flag_hit_premium) != 100: var prem_pct: int = int(db.settlement.flag_hit_premium) row = row.duplicate() row["power"] = int(round(int(row.power) * prem_pct / 100.0)) row["premium_pct"] = prem_pct # 惊巢反制(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:按 id 特判拆除——减半系数=mine_hit # damage_mult 订阅命中(行 value 50=减 50%,取整 ceil 与迁移前逐位一致);反弹单=mine_hit # open_ticket channel reflect 订阅施加(编排器 mine_hit 施加段);duplicate 隔离——减半不回写极性字典) if halve_pct > 0 and polarity == "negative" and source_class == "mine_hit": row = row.duplicate() var half: int = int(ceil(float(int(row.power)) * float(100 - halve_pct) / 100.0)) _emit_row("effect", source_class, cell, ri, "惊巢反制——挖雷惩罚减半(%d→%d)" % [int(row.power), half], 0, {"effect_id": "nest_counter"}, highlight) row.power = half return _dispatch_row(row, source_class, cell, ri, highlight, label_suffix) # 兑付形态分派:effect_id 非空 = percent 行(进增益状态容器);空 = flat 行(当场兑付) func _dispatch_row(row: Dictionary, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "") -> bool: var eff := String(row.get("effect_id", "")) if eff != "": return _apply_percent_row(row, source_class, cell, ri, highlight, label_suffix) _apply_flat_row(row, source_class, cell, ri, highlight, label_suffix) return false # percent 行兑付:GP 型行走战斗内缓冲记账(GP+商店房实施批);其余 apply_effect 入容器; # 满层拒收 = "奖励未生效"行(零静默) func _apply_percent_row(row: Dictionary, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "") -> bool: var eff := String(row.effect_id) if eff == "gp_gain" or eff == "gp_loss": return _apply_gp_row(row, source_class, cell, ri, highlight, label_suffix) var srow: Dictionary = db.status_effect_row(eff) # 旗溢价贯穿(迭代14② T4——仅标中旗正向行携带 premium_pct;其余授予路径缺省 100 零扰动): # 应用幅度与呈现行值同乘,容器经 magnitude_override 拿同一快照(抗表重载口径保持)。 var prem_pct: int = int(row.get("premium_pct", 100)) var eff_mag: int = int(srow.magnitude) var src := {"source_class": source_class, "origin_cell": cell, "round_index": ri} if prem_pct != 100: eff_mag = int(round(eff_mag * prem_pct / 100.0)) src["magnitude_override"] = eff_mag var ok: bool = _status_effects.apply_effect(eff, src) if ok: _emit_row("effect", source_class, cell, ri, String(srow.display_name) + label_suffix, eff_mag, {"effect_id": eff, "pool_value": _pool_effective(eff)}, highlight) return true _emit_row("reward_ineffective", source_class, cell, ri, "奖励未生效(满层)%s" % label_suffix, 0, {"batch_key": eff}, highlight) return false # GP 型行兑付(GP+商店房实施批 2026-09-21):战斗内缓冲 escrow 记账——不入钱包不入增益容器。 # 正向(gp_gain)= 挖开金币安全格/标中旗;负向(gp_loss)= 挖雷格/标错旗。兑付行经既有 effect 行 # 通道外发(escrow 随行——归因可读),战斗结束随 gp_dropped 一次上抛后即弃。 func _apply_gp_row(row: Dictionary, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "") -> bool: var eff := String(row.effect_id) var delta: int = int(row.power) if eff == "gp_gain" else -int(row.power) escrow += delta _emit_row("effect", source_class, cell, ri, "%s %+d%s" % [String(row.display_name), delta, label_suffix], delta, {"effect_id": eff, "escrow": escrow}, highlight) return true # flat 行兑付(负向恒即时,无库存语义):伤害型分支全部改开单据(伤害管线单据化批 2026-09-24—— # 管线/下限/目标解析归 伤害控制器,本函数只保留非伤害分支与单据装配): # action = 行动点排队(非伤害);blast 正向 = 群伤单据(无类型恒定表值打全体存活敌;负向 = 警觉 percent 行走 _apply_percent_row); # heal 正向 = 主角回复 clamp(满血 → 奖励未生效行);heal 负向 = 主角直伤单据(不经增益状态); # pierce/magic 正向 = 单目标克制直伤单据(attack_type 查克制矩阵,必中全额不掷点);负向 = 主角直伤单据。 # 返回 "redeemed" = 实际有效兑付 / "zero" = 零兑付(行已自记)——读数 = 控制器结账 outcome(A 模式提交即结)。 # sync = 同步开单(清图批——流程依赖完美第二份/高光份数读数,无视结账政策当场结账)。 func _apply_flat_row(row: Dictionary, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "", sync := false) -> String: var ctype := String(row.cell_type) var power := int(row.power) var display := String(row.display_name) + label_suffix if ctype == "action": var delta: int = power if String(row.get("polarity", "positive")) == "positive" else -power action_economy.queue_ap_delta(delta) _emit_row("effect", source_class, cell, ri, "%s(下轮行动点 %+d)" % [display, delta], delta, {"effect_id": "ap_delta", "pool_value": action_economy.pending_ap_delta()}, highlight) return "redeemed" if ctype == "blast": var t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, source_class, display, "board", power, \ DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, highlight, {"batch_key": String(row.reward_id)}) return _outcome_of(_submit(t, sync)) if ctype == "pierce" or ctype == "magic": # 伤害道具批(2026-09-18)语义不变:正向 = 单目标克制直伤(必中全额不掷点——棋盘来源伤害既有口径); # 负向 = 主角直伤(复用反噬通道)。 if String(row.get("polarity", "positive")) == "negative": return _submit_flat_hero(source_class, display, power, cell, ri, highlight, sync) var atk_type: String = String(row.get("attack_type", ctype)) var t2 := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, source_class, display, "board", power, \ DamageController.TARGET_FRONT_ALIVE, ri, cell, atk_type, 0, highlight, {"batch_key": String(row.reward_id)}) return _outcome_of(_submit(t2, sync)) if ctype == "heal": if String(row.get("polarity", "positive")) == "negative": return _submit_flat_hero(source_class, display, power, cell, ri, highlight, sync) if int(_enemy_system.hero_unit().get("hp", 0)) <= 0: # 主角已倒下(同轮更早的负向直伤致死)——治疗不再合法作用,按"奖励未生效"留痕 _emit_row("reward_ineffective", source_class, cell, ri, "%s 未生效(主角已倒下)" % display, 0, {"batch_key": String(row.reward_id)}, highlight) return "zero" if int(_enemy_system.hero_unit().hp) >= int(_enemy_system.hero_unit().max_hp): _emit_row("reward_ineffective", source_class, cell, ri, "%s 未生效(满血)" % display, 0, {"batch_key": String(row.reward_id)}, highlight) return "zero" var healed: int = _enemy_system.heal(_enemy_system.hero_unit(), power) # clamp 归 战斗结算 _emit_row("heal", source_class, cell, ri, "%s 回复" % display, healed, {"target": "hero"}, highlight) return "redeemed" _fatal("flat 行未知类型: %s" % ctype) return "zero" # 开单分流:sync(清图批)走控制器同步开单——先按序结清段内排队单据、再当场兑现本单; # 常规开单按结账时机政策(A 即结 / B 入队)。 func _submit(t: Dictionary, sync: bool) -> int: if _defer_board_damage: _deferred_board.append(t) # 案C:授予段伤害单据暂存——战斗段怪物还手后按事件序补提交(sync 同闸,清图批一致延后) return -1 return _damage.flush_submit(t) if sync else _damage.submit(t) # 案C 结账读数:暂存单(_submit 返回 -1)以「将兑付」回报——授予段兑付决策(清图完美第二份等)只能 # 依据开单时点事实(该时点敌人存活有保证);战斗段末实际结账若目标已灭,控制器自记未生效行。 func _outcome_of(ticket_id: int) -> String: return "redeemed" if ticket_id < 0 else _damage.outcome(ticket_id) # 主角直伤单据(反噬/治疗包负向——无类型必中,下限保底在控制器管线内) func _submit_flat_hero(source_class: String, display: String, power: int, cell: Vector2i, ri: int, highlight: String, sync := false) -> String: var t := _damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, source_class, display, "board", power, \ DamageController.TARGET_HERO, ri, cell, "", 0, highlight, {}) return _outcome_of(_submit(t, sync)) # 旗判定批:标中 = 该雷格资源的正向兑付(每雷实例一次;高光 = 强制检查点); # 标空 = "标旗未中"归因行 + 该安全格资源的负向兑付(同键去重——每格实例一次)。 func _judge_flag(evt: Dictionary, ri: int) -> void: var cell: Vector2i = evt.cell if evt.hit: if _flag_consumed.has(cell): return # 去重跳过(资格以雷格实例为键——排雷玩法 诚实重复上报的对侧);零明细行 _flag_consumed[cell] = true _perfect_hit_true += 1 # flag_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:殉爆〔T4 秘宝面〕→ # 净化改写 → 图谱额外资源 → 基线兑付) var fh_hits := _fx_emit("flag_hit", {"cell": cell, "round": ri, "terrain_kind": _harmful_adjacent(cell)}) # 标中殉爆(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:订阅施加——行 triggers open_ticket # channel blast value_from magnitude;每标中一面旗全体存活敌吃 N 群伤,无类型走 blast 通道。 # 迭代 20 T1——封印爆裂〔迁入专长〕同面消费:行 triggers open_ticket channel blast) for h in _hits_of(fh_hits, "open_ticket"): if String(h.get("channel", "")) != "blast": continue var fbt := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_flag_burst", "封印爆裂", "board", int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, "highlight", {}) _submit(fbt, false) # 迭代 20 T1——标中新施加面:counter_add〔血税 标中+1 攻——bonus_delta atk 轴〕/ # ac_bonus〔封印桩 本轮 AC+N〕/ preview_area〔示警烟火 3×3 资源类型预告〕/ # remove_mines〔封印匠 每第 3 次该危险格中立移除——mod_eq 闸在行 when〕。 # (净化工兵 rewrite_board 分支随老 27 件退役摘除——效果类型同批除名,T1 落法 commit 留痕) for h in fh_hits: match String(h.get("kind", "")): "counter_add": var set_v = h.get("set", null) _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) "ac_bonus": _apply_ac_bonus(h) "bonus_delta": var axis_f := String(h.get("axis", "")) if _fx_bonus_delta.has(axis_f): _fx_bonus_delta[axis_f] += int(h.get("delta", 0)) "remove_mines": var removed: Array = minesweeper.remove_mines_in([cell]) if not removed.is_empty(): _emit_row("mine_neutralized", "fx_seal_smith", cell, ri, "封印匠·危险格中立移除", 0, {}, "") "preview_area": var board_p: BoardData = minesweeper.current_board() var types: Array = [] for n in board_p.neighbors8(cell): var n_snap = _reward_economy.query_full_snapshot(n) if n_snap is Dictionary and not types.has(String((n_snap as Dictionary).get("cell_type", ""))): types.append(String((n_snap as Dictionary).get("cell_type", ""))) if treasure_inventory != null and not types.is_empty(): treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell)) _emit_row("fx_preview_area", "", cell, ri, "示警烟火·周围资源类型", 0, {"types": types}, "") # 雷区图谱(迭代 12 T3 标旗流——标中雷额外获得一份格面资源:插旗开始产出对标开格; # 复用 grant 单点,额外兑付走同一 _grant_routed 正向路由) for h in fh_hits: if String(h.get("kind", "")) == "grant_material": var extra_snap = _reward_economy.grant(cell) if extra_snap is Dictionary: _grant_routed(extra_snap, "positive", "flag_hit", cell, ri, "", String(h.get("label", ""))) var snap = _reward_economy.grant(cell) if not (snap is Dictionary): return _grant_routed(snap, "positive", "flag_hit", cell, ri, "flag_hit") else: _perfect_hit_false += 1 if _flag_miss_seen.has(cell): return # 每格实例一次去重(归因行与负向兑付同键——防逐轮惩罚轰炸) _flag_miss_seen[cell] = true # 完美保险(迭代 20 T4——本战下一次标错不破坏完美资格:_perfect_hit_false 回退=本次标错 # 对完美谓词无痕;只保一次/件,节点级用完即止) if _fx_insurance > 0: _fx_insurance -= 1 _perfect_hit_false -= 1 _emit_row("insurance_used", "", cell, ri, "完美保险·标错豁免", 0, {"left": _fx_insurance}, "") var hl := "" if _perfect_hit_false == 1: hl = "perfect_broken" # 本板完美谓词已破的归因锚 _emit_row("flag_miss", "flag_miss", cell, ri, "标旗未中", 0, {}, hl) # 危险直觉·守挂点(迭代 21 T3——flag_missed 发射:契约 12 触发主语=「当回合发生的标错 # 动作」——去重闸=新旧标错判别器〔后续轮重判旧旗被闸挡回零触发〕;双条件②「结算时旗仍在 # 场」由发射面既有过滤承载〔被挖开的旗已随挖掘擦除零兑付〕;grant_ap mode:queue 下一轮 # 开轮兑现——末轮判出无下一轮=合法作废自动成立〔编排器每战新建,队列不跨战〕;限次=合成行 # adventure 域计数器闸) for h in _fx_emit("flag_missed", {"cell": cell, "round": ri}): if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue": action_economy.queue_ap_delta(int(h.get("value", 0))) var snap = _reward_economy.grant(cell) if not (snap is Dictionary): return _grant_routed(snap, "negative", "flag_miss", cell, ri, "") # 清图批:清图奖励对象恰好发一次;完美排雷追加第二份同身份兑付(grant_clear 本身恰一次)。 # 高光行 value = 实际有效兑付份数(禁记名义值;首份零兑付 → 降级不发)。 func _grant_clear_reward(ri: int) -> void: if _board_from_midround_refresh: return # 轮内刷新换来的新图不发清图奖励(行动经济/战斗结算 明文——刷新图不是清图挣来的) var snap = _reward_economy.grant_clear() if not (snap is Dictionary): return var perfect: bool = _perfect_hit_false == 0 and _perfect_hit_true >= 1 var first_ok := _grant_clear_identity(snap, ri, "(清图奖励)") var second_ok := false if perfect and first_ok: second_ok = _grant_clear_identity(snap, ri, "(完美第二份)") if perfect and first_ok: _emit_row("highlight", "clear_reward", Vector2i(-1, -1), ri, "完美排雷!清图奖励 ×2", (2 if second_ok else 1), {}, "perfect_sweep") # 清图奖励 = 板级独立对象(不属于任何一格):兑付形态按其 cell_type 的正向字典形态走, # 行值全部取快照(身份批 roll 值)。返回实际有效兑付。 # 同步开单(sync)——完美第二份与高光份数的流程依赖需要同步读数(开单权归流程 用户裁决③); # 同步路径内先按序结清段内排队单据(B 模式保序),随后本批无视结账政策当场兑现。 func _grant_clear_identity(snap: Dictionary, ri: int, suffix: String) -> bool: var row := { "reward_id": String(snap.get("reward_id", "")), "cell_type": String(snap.get("cell_type", "")), "polarity": "positive", "display_name": String(snap.get("display_name", "")), "power": int(snap.get("power", 0)), "effect_id": String(snap.get("effect_id", "")), "stacks": int(snap.get("stacks", 1)), } var eff := String(row.effect_id) if eff != "": return _apply_percent_row(row, "clear_reward", Vector2i(-1, -1), ri, "clear_reward_h2", suffix) return _apply_flat_row(row, "clear_reward", Vector2i(-1, -1), ri, "clear_reward_h2", suffix, true) == "redeemed" # ---- 开轮摇号(敌人技能池批——共识点 B:技能选择前置到轮次开始,数值计算留结算段)---- # 每实例:保底判定(连续执行普攻 ≥ M → 必出特殊技、特殊技内按权重、不受冷却限制)→ 排除冷却中技能 # (中签率 = 权重 ÷ 可摇权重合计——份额归一)→ 按行序逐敌从统一流顺序取号摇定本拍动作。 # 随机消费只在换算层(本方法 + SkillLottery);单位容器收确定值(零随机纪律——armor_roll 先例同构)。 # 并流批(ADR-0007 裁丙 2026-09-27):behavior_roll 子流派生向量(spawn_index×STRIDE+序)退役—— # 「单敌轨迹独立复放」前提随之移除(位置交错,单点定位由事件日志承载)。摇定后、兑现前死亡 → 本拍技能作废。 func _roll_round_actions() -> void: var pity_m: int = int(db.skill.pity_threshold) for u in _enemy_system.query_alive("enemy"): # Boss 半血解锁闩(迭代14④ T4——开轮摇号前逐敌检查;一次过门单向闩;解锁出一次归因明细行 # 「XX 解锁了 YY」带 skill_unlock 高光键〔type=highlight 既有行型零新增〕,呈现层弹一次性 # 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机 # 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏) for sid in _enemy_system.check_skill_unlocks(u): var srow_u: Dictionary = db.skill_row(sid) _emit_row("highlight", "enemy_action", Vector2i(-1, -1), round_index, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))], 0, {"skill_id": sid}, "skill_unlock") # enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射, # 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 unlock_hp_pct 表值) var pct_v := 0 for pe in (u.get("pool", []) as Array): if String((pe as Dictionary).get("skill_id", "")) == sid and (pe as Dictionary).has("unlock_hp_pct"): pct_v = int((pe as Dictionary).unlock_hp_pct) break # 迭代 20 T1——target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记, # 主角攻击面 _resolve_hero_attack 通用消费——fx_atk_bonus 键加算进 base_value) var eht_hits := _fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v, "is_boss": String(encounter.get("node_type", "")) == "boss"}) for h in eht_hits: if String(h.get("kind", "")) == "target_attack_bonus": u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0)) var entries: Array = _enemy_system.rollable_skills(u) if _enemy_system.basic_streak(u) >= pity_m: var specials: Array = _enemy_system.special_skills(u) if not specials.is_empty(): entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合 var stream: RandomNumberGenerator = rng.open_stream("behavior_roll") _enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream)) _enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记) # ---- 战斗段 ---- # 伤害管线单据化批(2026-09-24):本段只开单——主角先手单 + 敌方伤害技单;掷点/折扣/暴击/克制/ # 池乘区/下限/交付/commit 全部收口 伤害控制器(A 模式提交即结=与迁移前逐位一致;B 模式段尾 flush)。 # 开单守卫归流程(用户裁决③——流程决定给谁开单):主角存活+目标存在才开先手单;hero hp=0 立即 # 停开(B 模式下若 flush 中主角倒下,由控制器死亡事件终止余单——两模式同一结果)。 func _combat_segment(ri: int) -> void: _defer_board_damage = false # 案C 闸复位——战斗段起开单即入队(主角/敌方/补提交的地图单按开单序统一结账) # 本轮扫雷事实推导(迭代 12 T4——执拗 streak/连珠矢段数共用取数面):本轮 safe 事件数= # streak;踩雷即清零(mine_hit 监听同口——执拗「踩雷清零」承载)。攻击在战斗段=授予段后 # 同一次 _settle 内,facts 已定。 var round_safe := 0 var round_mine := false for evt in _facts_reveals: if evt.kind == "safe": round_safe += 1 elif evt.kind == "mine": round_mine = true _damage.dig_streak = 0 if round_mine else round_safe # 蓄力层注入(内容数据驱动扩展批 T3——整备军备「每层下轮首段 +1」/厚积薄发满层 spike/铁壁整备 # 减伤——三件 fact charge_layers 同读 action_economy;battle 域每轮刷新) _damage.charge_layers = action_economy.charge_layers # ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动。案C 后授予段不再产出伤害, # 主角进入战斗段时恒存活——先手必兑,除非场上无敌)。 # 单分量伤害构成(2026-09-19 充能退役批·用户拍板):主角攻击 = 唯一基础分量(atk,吃掷点含暴击; # 类型 = 主角 attack_type 表值——2026-09-18 营地扩展前置批:量与类型抽象分离;查甲效矩阵对应行取克制修正)。 # 原"基础 + 穿刺/魔法充能三分量"模型随充能系统整体退役——克制策略归冒险前配置(营地换型 / 未来冒险前参数面), # 局内不再有挖掘攒弹药的第二伤害供给。attack_up 增益经 hero_offense 池乘攻击合计(见池乘区注记)。 # 护甲等级掷点批(2026-09-16):基础分量吃掷点判定(含暴击);棋盘来源伤害不掷不折; # 任何伤害落地 ≥ damage_floor(全局 max(1,·)——2026-09-20 用户拍板扩全来源,下限在控制器管线单点收口)。 var hero_alive: bool = int(_enemy_system.hero_unit().get("hp", 0)) > 0 var hero_target = _enemy_system.default_target() if hero_alive and hero_target != null: var ht := _damage.open_ticket(DamageController.KIND_HERO_ATTACK, "hero_attack", "主角攻击", "combat", _hero_atk, \ DamageController.TARGET_FRONT_ALIVE, ri, Vector2i(-1, -1), String(_enemy_system.hero_unit().get("attack_type", "normal"))) _damage.submit(ht) # 连珠矢(内容数据驱动扩展批 T3——attack_commit 订阅施加面:开格节奏变攻击段数,本轮每开 # ~3 安全格 +1 段穿刺附加(上限 cap;段值 provisional 固定值不乘 atk——台账披露)。施加位 # 紧随主角攻击单开立=迁移前开单位(先于敌方行动单入队——flush 兑现序逐位一致) for h in _hits_of(ht.context.get("fx_attack_hits", []) as Array, "attack_segments"): var segments := mini(int(h.get("cap", 2)), _damage.dig_streak / maxi(1, int(h.get("divisor", 3)))) for s in segments: # 迭代 20 T9——source_class 中性名(旧件名 feat_chain_arrows 随连珠矢退役;attack_segments 兑付通道保留泛用面) var ct := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "fx_streak_segments", "连击分段", "board", int(h.get("value", 0)), DamageController.TARGET_FRONT_ALIVE, ri, minesweeper.hero_pos(), "pierce") _damage.submit(ct) # 敌方行动(敌人技能池批——本拍动作开轮已摇定,此处按技能行兑现;行序 = 生成序)。 # 死亡即不动手:摇定后、兑现前死亡的敌不在存活序中——本拍技能作废,不 commit、不进冷却。 for u in _enemy_system.query_alive("enemy"): if int(_enemy_system.hero_unit().hp) <= 0: break # hero hp=0 立即短路(死尸挨打行是归因噪音——用户拍板) var skill_id := _enemy_system.pending_action(u) var srow: Dictionary = db.skill_row(skill_id) if srow.is_empty(): _fatal("敌实例待发动作悬空: %s" % skill_id) continue if String(srow.type) == "buff" or String(srow.type) == "debuff": # 增益/妨害技(新效应词汇批 2026-09-19)= 0 伤害施放行:不掷不折;状态授予即数值兑现,技能冷却与增益持续两计时器分账。 # (原"本轮喂的伏击逢施放拍不兑现"读怪奖励随伏击退役一并作废——2026-09-20 八型批) # 落点由状态行 target_side 路由(buff → 敌方侧 / debuff → 主角侧——容器零改动,proposal-status-vocabulary §1)。 var dur: int = int(srow.get("buff_duration", 1)) _status_effects.apply_effect(String(srow.buff_ref), {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": ri}, dur) _emit_row("cast", "enemy_action", Vector2i(-1, -1), ri, "%s 施放 %s" % [u.display_name, String(srow.display_name)], 0, {"skill_id": skill_id, "effect_id": String(srow.buff_ref), "duration": dur}) _enemy_system.commit_action(u) continue # 伤害技(含普攻——倍率恒 1)= 敌方伤害技单据:基线 = round(atk×倍率)、rider 预计算归开单侧 # (表值算术零随机),掷点/克制恒中性/hero_defense×enemy_output 池乘区/下限/rider 后置/commit # 全归控制器管线(兑现序 = 开单序;来源敌已死在兑现时作废——死亡即不动手)。 var baseline: int = int(round(float(u.atk) * float(srow.get("damage_mult", 1.0)))) var rider := 0 if srow.has("rider_mult"): rider = maxi(int(db.settlement.damage_floor), int(floor(float(u.atk) * float(srow.rider_mult)))) var et := _damage.open_ticket(DamageController.KIND_ENEMY_SKILL, "enemy_action", "%s %s" % [u.display_name, String(srow.display_name)], "combat", \ baseline, DamageController.TARGET_HERO, ri, Vector2i(-1, -1), "", rider) et["source_unit"] = u et["context"] = {"skill_id": skill_id, "baseline": baseline} _damage.submit(et) # 地图伤害兑付(案C 2026-10-06 用户裁——战斗段末:怪物还手后按授予段事件序补提交统一结账; # 主角死于怪物 → 地图账不兑,逐单留「未生效」行不作废静默——没生效也要留痕口径; # 补提交后若地图直伤反杀主角,控制器 _halted 作废余单——战斗已败,胜负归判定段) if int(_enemy_system.hero_unit().get("hp", 0)) <= 0: for t in _deferred_board: _emit_row("reward_ineffective", String(t.get("source_class", "")), t.get("origin_cell", Vector2i(-1, -1)), ri, "%s 未生效(主角已倒下)" % String(t.get("display", "")), 0, {"forfeit": "hero_down"}, "") else: for t in _deferred_board: _damage.submit(t) _deferred_board.clear() _damage.flush() # 战斗段界结账(B 模式——主角单+敌方单+地图单按开单序统一兑现;A 模式地图单补提交时即结) # ---- 算术读数(伤害管线单据化批 2026-09-24 迁移注记)---- # 掷点/折扣/克制点数/池乘区/攻击行文本已全部迁入 伤害控制器(damage_controller.gd 管线单点); # 本类仅保留一处薄委托——_defender_ac(快照面消费)。_clamped_pool 委托壳已随 2026-10-02 修复批删除 # (冒烟白盒断言消费面改直引 settlement._damage.clamped_pool——并行卡同批改 smoke_all)。 # 防守方护甲等级读数——实现归 伤害控制器 defender_ac(本委托供结算快照/呈现快照) func _defender_ac(unit: Dictionary) -> int: return _damage.defender_ac(unit) func _pool_effective(effect_id: String) -> int: var side := "hero" var row: Dictionary = db.status_effect_row(effect_id) if not row.is_empty(): side = String(row.target_side) for eff in _status_effects.query_active(side): if eff.effect_id == effect_id: return eff.effective_value return 0 func _emit_composition_effects(side: String, ri: int) -> void: for eff in _status_effects.query_active(side): if eff.round_index < ri: # 续存实例(前轮授予)——每结算兑付一次的可见面(合并实例只持首授归因——增益状态 义务⑦) _emit_row("effect", eff.source_class, eff.origin_cell, ri, eff.display_name, eff.snap_magnitude, {"effect_id": eff.effect_id, "pool_value": eff.effective_value}) # ---- 结算快照(playtest 数值分析数据面——每次结算全量状态;运行时零决策读取)---- func _emit_settlement_snapshot(verdict: String) -> void: var enemies: Array[Dictionary] = [] for u in _enemy_system.query_alive("enemy"): enemies.append({"id": String(u.enemy_id), "hp": int(u.hp), "max_hp": int(u.max_hp), "atk": int(u.atk), "pending_skill": _enemy_system.pending_action(u), "armor": String(u.get("armor_type", "light")), "armor_class": int(u.get("armor_class", 0))}) var buffs: Array[Dictionary] = [] for eff in _status_effects.query_active("hero"): buffs.append({"id": eff.effect_id, "side": "hero", "layers": eff.layers, "left": eff.remaining_settlements}) for eff in _status_effects.query_active("enemy"): buffs.append({"id": eff.effect_id, "side": "enemy", "layers": eff.layers, "left": eff.remaining_settlements}) var mines_hit := 0 var safe_n := 0 for evt in _facts_reveals: if evt.kind == "mine": mines_hit += 1 elif evt.kind == "safe": safe_n += 1 var board_stats := {"seq": _board_seq} if minesweeper.current_board() != null: board_stats["w"] = minesweeper.current_board().width board_stats["h"] = minesweeper.current_board().height board_stats["revealed"] = minesweeper.revealed_cells().size() board_stats["flags"] = minesweeper.flag_count() settlement_snapshot.emit({ "kind": "settlement", "level": int(encounter.get("level_index", 0)), "depth": int(encounter.get("node_depth", 0)), "round": round_index, "verdict": verdict, "unit_count": enemies.size(), "spawn_count": _enemy_system.all_enemies().size(), # 本战斗实例化总数(含死亡残位)——unit_count=0 时区分「首轮全灭」vs「空场」(playtest-2026-09-12 报告 Bug#1 误读教训) "hero": {"hp": int(_enemy_system.hero_unit().get("hp", 0)), "max_hp": int(_enemy_system.hero_unit().get("max_hp", 0)), "atk": int(_enemy_system.hero_unit().get("atk", 0)), "armor_class": _defender_ac(_enemy_system.hero_unit())}, "economy": {"phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "refresh_used": _refresh_used_this_round}, "board": board_stats, "enemies": enemies, "buffs": buffs, "round_facts": {"path": _facts_path, "safe": safe_n, "mines": mines_hit, "flag_events": _facts_flags.size(), }, }) # ---- 出口分发与战斗结束序列 ---- func _exit_distribution() -> void: var mode := "same_board" if _facts_path == "cleared": mode = "new_board" # cleared 轮恒走 new_board(契约义务);非清图轮恒同图(等待期刷新通道已移除) if mode == "new_board": _reward_economy.retire_mapping() minesweeper.retire_board() if not _install_new_board(): _fatal("换图编排失败") return # 工兵晨令续队直调拆除(内容数据驱动扩展批 T4——round_start 订阅行承载,见 begin_battle 注) action_economy.notify_settlement_done(mode) # round_start 发射先于行动经济轮切换(queue 型 grant_ap 当轮入队当轮兑现——与首轮同口径) round_index += 1 _emit_round_start() action_economy.notify_round_start() _reset_round_facts() _refresh_used_this_round = 0 _roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道) round_started.emit(round_index) # 呈现层查询面(HUD 刷新按钮可用性)——挖掘期唯一通道 func can_refresh() -> bool: return battle_result == "" \ and _refresh_used_this_round < int(db.action_economy.refresh_cap_per_round) \ and action_economy.phase == action_economy.PHASE_ACTIVE \ and action_economy.rap >= int(db.action_economy.rap_refresh_cost) # 玩家经 HUD框架发起、战斗结算 代理调用(禁止 RAP 满自动刷)。 # 挖掘期唯一通道(2026-09-12 方案 A 拍板 + 2026-09-15 等待期通道整体移除):Active 相位立即换图 # 继续挖(RAP -8;AP 保留、相位不动、不触发结算);非挖掘期请求一律拒绝——非清图轮恒同图。 func player_request_refresh() -> bool: if not can_refresh(): return false if not action_economy.request_refresh(): return false _refresh_used_this_round += 1 _reward_economy.retire_mapping() # 旧图批次退役(地图战斗资源 Ready→retired;E6 新批重置前提) minesweeper.retire_board(true) # 旧板退役(轮内通道) return _install_new_board(true) # 当场换图——失败即编排违例 fail-fast func _battle_over(result: String) -> void: battle_result = result # battle_won 发射+施加(迭代 20 T1——战地包扎 heal 订阅面;败北不发射。heal=英雄单位回血 # 〔封顶单位上限〕,战后生命经 hero_hp_left 回写冒险账本——时点在捕获前=回血计入带入下一层) if result == "victory": for h in _fx_emit("battle_won", {"round": round_index}): if String(h.get("kind", "")) == "heal": var hu_w: Dictionary = _enemy_system.hero_unit() hu_w["hp"] = mini(int(hu_w.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_w.get("max_hp", 1)))) # bonus_delta 战末汇入(迭代 20 T1——血税双向账:本战累积并入冒险加算区共享引用后清零)+ # adventure 域计数器导出回写(T6——共享引用覆写,attune_charges 先例) if _fx_bonus_ref != null and not _fx_bonus_ref.is_empty(): for axis in _fx_bonus_delta.keys(): if _fx_bonus_ref.has(axis): _fx_bonus_ref[axis] = int(_fx_bonus_ref.get(axis, 0)) + int(_fx_bonus_delta[axis]) _fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} if _fx != null and not _fx_adventure_counters_ref.is_empty(): var exported: Dictionary = _fx.counters_export() for cid in exported.keys(): _fx_adventure_counters_ref[cid] = int(exported[cid]) # 映射随战斗结束退役(地图战斗资源 义务④——下一场战斗 = 新板新 scope 批,roll_board 相位守卫的前提) var hero_hp_left := int(_enemy_system.hero_unit().get("hp", 0)) # despawn 前捕获(信号载荷供呈现层"HP 带入下一层") var gp_dropped := _roll_gp_drop() # 战斗掉落(GP+商店房实施批——escrow 随此值上抛即弃) _reward_economy.retire_mapping() _enemy_system.despawn_all() _status_effects.notify_battle_end() if treasure_inventory != null: treasure_inventory.retire_overlays() # 迭代 7 T3——叠加层随战斗结束清空(库存不动——关卡域跨战斗) action_economy.notify_combat_end() # → Idle,RAP 清零(2026-10-02 修复批:原被 tab 事故并进上一行行尾注释从不执行——每战新建实例掩盖了症状;P0-3) db.set_reload_gate(false) _reset_round_facts() battle_finished.emit({"result": result, "hero_hp": hero_hp_left, "gp_dropped": gp_dropped}) # 战斗掉落掷定(GP+商店房实施批 2026-09-21):tier = Boss 节点 ? "boss" : "normal"(elite 留分支位); # gp_dropped = gp_drop 封闭流掷定(tier 行的 roll_min..roll_max 均匀整数——每战斗末恰一 scope) # + level_gp_bonus(level_index) + escrow。掷定行缺位 = 数据面缺行(fail 报错按 0 兑付——不哑掉)。 func _roll_gp_drop() -> int: var tier := "boss" if String(encounter.get("node_type", "")) == "boss" else "normal" var drops: Dictionary = db.shop_gp_drop_row(tier) var roll := 0 if drops.is_empty(): push_error("[战斗结算] gp_drops 缺行 tier=%s——掉落掷定按 0 兑付" % tier) else: var stream: RandomNumberGenerator = rng.open_stream("gp_drop") roll = stream.randi_range(int(drops.get("roll_min", 0)), int(drops.get("roll_max", 0))) var drop := roll + db.shop_level_gp_bonus(int(encounter.get("level_index", 0))) # 财富之道·聚(迭代 21 T3——战斗掉落 GP 乘算·入账前:只乘战斗掉落两成分〔掷定+关卡加成〕, # 店内 escrow 不乘〔范围=仅战斗掉落渠道,卡片§11②裁决〕;本函数返回值=结算框「GP 增量」行 # 显示源,落在入账前=显示即实得;乘数 provisional 150%,值归 M4 数值批) if (encounter.get("tree_rules", []) as Array).has("economy_hoard"): drop = int(round(float(drop) * 1.5)) return drop + escrow # ---- 呈现状态面(E1 封装收口 2026-09-13):呈现层 / headless 自动玩家的唯一读取出口—— # 协作对象(_enemy_system/_status_effects/_reward_economy)私有化后,跨层读取一律经本快照或下列命名面(diagnostic_snapshot 同款模式, # 禁再链式穿透 settlement.enemy_system.hero 一类路径)。载荷 schema 成文(code-conventions 2.1): # round / battle_result / phase / ap / rap / can_refresh / hero{hp,max_hp,atk,alive,armor_class} / # enemies[{order,display_name,hp,max_hp,enemy_id,armor_type,armor_class,pending_skill,pending_type,pending_name,effects[],alive}] / # (敌人技能池批批 2 定稿:beat/ring_size/ring_index 拆除 → pending_* 三字段 + 敌方增益 effects 投影;池与权重不公开; # 八型批 2026-09-20:ambush_layers 拆除——伏击退役,警觉并入 effects 投影)hero_effects[](增益状态 query_active 投影) func presentation_snapshot() -> Dictionary: var enemies: Array[Dictionary] = [] var all: Array = _enemy_system.all_enemies() # 敌方增益投影(side-wide 状态——蓄力/硬化/削弱/暴走 percent 行 + 警觉 ac 行,八型批 2026-09-20) var enemy_effects: Array[Dictionary] = [] for eff in _status_effects.query_active("enemy"): var erow: Dictionary = db.status_effect_row(eff.effect_id) if not erow.is_empty() and String(erow.unit) in ["percent", "ac"]: enemy_effects.append({"effect_id": eff.effect_id, "display_name": String(eff.display_name), "layers": int(eff.layers)}) for i in all.size(): var u: Dictionary = all[i] var alive: bool = bool(u.get("alive", false)) var pending: String = String(u.get("pending_skill", "")) if alive else "" var pending_row: Dictionary = db.skill_row(pending) enemies.append({ "order": i + 1, "display_name": String(u.display_name), "hp": int(u.hp), "max_hp": int(u.max_hp), "enemy_id": String(u.enemy_id), "armor_type": String(u.get("armor_type", "light")), "armor_class": int(u.get("armor_class", 0)), # 敌人技能池批(批 2 定稿):摇定动作三字段——普通攻击零表示 / 特殊技类型级动画占位。 # 池与权重不公开(proposal D4/D5)——实例池的权重/冷却不投影,实例池可见面 = 摇定结果本身。 "pending_skill": pending, "pending_type": String(pending_row.get("type", "")) if not pending_row.is_empty() else "", "pending_name": String(pending_row.get("display_name", "")) if not pending_row.is_empty() else "", "effects": enemy_effects, "alive": alive, }) var hero := {} var h: Dictionary = _enemy_system.hero_unit() if not h.is_empty(): hero = {"hp": int(h.hp), "max_hp": int(h.max_hp), "atk": int(h.atk), "alive": bool(h.get("alive", false)), "armor_class": _defender_ac(h)} return { "round": round_index, "battle_result": battle_result, "phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "can_refresh": can_refresh(), "hero": hero, "enemies": enemies, "hero_effects": _status_effects.query_active("hero").duplicate(true), # 值拷贝出户(code-review 2026-09-29 P2-3——快照消费方直改不渗入状态容器) # 板级变异投影(全局作用域变异首发批 2026-10-04——呈现横幅/常驻角标唯一读取面; # 空数组 = 无全局型合法态:普通图与全局池空回落图恒空) "board_variants": (minesweeper.current_board().board_variant_ids().duplicate() if minesweeper.current_board() != null else ([] as Array[String])), } # 战斗进行中谓词(hero 在场且存活)——组合根高频相位判断的低成本面 func is_battle_live() -> bool: return battle_result == "" and not _enemy_system.hero_unit().is_empty() # 奖励身份解析(组合根注入 BoardView 徽章回调的只读代理——Ready 相位守卫归 地图战斗资源) func query_reward_identity(cell: Vector2i): return _reward_economy.query_identity(cell) # 诊断观测面(组合根诊断转储 / 行为探针消费):私有编排状态的成文只读出口—— # 消费方禁止再直读本类下划线字段(code-review 2026-09-12 封装收口批) func diagnostic_snapshot() -> Dictionary: return { "battle_result": battle_result, "round": round_index, "settling": _settling, "refresh_used_round": _refresh_used_this_round, "board_seq": _board_seq, "facts_path": _facts_path, "facts_reveals": _facts_reveals.size(), "facts_flags": _facts_flags.size(), "action_economy": {"phase": String(action_economy.phase), "ap": action_economy.ap, "rap": action_economy.rap}, "minesweeper_board_installed": minesweeper.current_board() != null, "minesweeper_revealed": minesweeper.revealed_cells().size() if minesweeper.current_board() != null else 0, "minesweeper_flags": minesweeper.flag_count() if minesweeper.current_board() != null else 0, "reward_economy_phase": _reward_economy.phase(), "enemies_alive": _enemy_system.query_alive("enemy").size(), "hero_hp": int(_enemy_system.hero_unit().get("hp", -1)) if not _enemy_system.hero_unit().is_empty() else -1, } func _reset_round_facts() -> void: _facts_reveals.clear() _facts_flags.clear() _facts_path = "" # ---- 行协议(Core Rule:公共字段 + 行类型特有字段)---- func _emit_row(type: String, source_class: String, origin_cell: Vector2i, ri: int, display: String, value: int, extra: Dictionary, highlight: String = "") -> void: _assert_row_source(type, source_class, origin_cell) var row := { "type": type, "source_class": source_class, "origin_cell": [origin_cell.x, origin_cell.y], "round_index": ri, "display_name": display, "value": value, } if highlight != "": row["highlight"] = highlight for k in extra.keys(): row[k] = extra[k] step_row.emit(row) # 行来源断言(战斗结算 明细流水:"任何一行找不到来源即断言失败——宁可报错"): # 来源字段 = source_class(公共字段)+ origin_cell(棋盘来源行的真实格)。 # 豁免按两层成文(见 SOURCE_EXEMPT_TYPES / SOURCE_EXEMPT_CLASSES 常量注释),其余行必须带真实棋盘格。 func _assert_row_source(type: String, source_class: String, origin_cell: Vector2i) -> void: if SOURCE_EXEMPT_TYPES.has(type): return assert(source_class != "", "[战斗结算] 明细行缺来源类别(type=%s)——任何一行找不到来源即断言失败" % type) if SOURCE_EXEMPT_CLASSES.has(source_class): return assert(origin_cell.x >= 0 and origin_cell.y >= 0, "[战斗结算] 明细行来源格越界(type=%s source=%s cell=%s)——棋盘来源行必须指回真实格" % [type, source_class, str(origin_cell)]) func _fatal(msg: String) -> void: push_error("[战斗结算/fatal] " + msg) battle_result = "fatal_error" # release 构建剥离 assert 后的唯一阻断面——受理/结算/刷新入口全查 battle_result == "" assert(false, msg)