diff --git a/game/core/battle_fx_host.gd b/game/core/battle_fx_host.gd index 90fcc273..e9c164c6 100644 --- a/game/core/battle_fx_host.gd +++ b/game/core/battle_fx_host.gd @@ -20,7 +20,7 @@ var rng: RngServiceCore var minesweeper: MinesweeperRules var action_economy: ActionEconomy var damage: DamageController -var emit_cb: Callable # 宿主 _emit_row——行协议面单点(束形改形后随束形) +var emit_cb: Callable # 宿主 _emit_row——行协议面单点(5 参 head 束形:head/ri/display/value/extra) var facts_cb: Callable # 宿主 _fx_host_fact——round_index/battle_index/seals_count/nest_count/hp_ratio 五事实宿主侧单点 var grant_cb: Callable = Callable() # 揭示兑付回调=RewardGrant 授予面(bind_grant 注入;构造序解法——本件对授予面零强引用) var engine: EffectEngine # 效果引擎(原编排器 _fx——宿主 _init 经此转注 damage/action_economy/reward_economy 三 set_fx) @@ -99,7 +99,7 @@ func flush_pending_reveals(ri: int) -> void: if not minesweeper.is_revealed(c) and not board.is_mine(c): cands.append(c) if cands.is_empty(): - emit_cb.call("extended", "fx_reveal", Vector2i(-1, -1), ri, "揭示落空(无未揭示安全格)", 0, {"effect_id": "fx_reveal"}, "") + emit_cb.call({"type": "extended", "source_class": "fx_reveal", "origin_cell": Vector2i(-1, -1)}, ri, "揭示落空(无未揭示安全格)", 0, {"effect_id": "fx_reveal"}) continue var rg: RandomNumberGenerator = rng.open_stream("fx_reveal") var cell: Vector2i = cands[rg.randi_range(0, cands.size() - 1)] @@ -107,10 +107,10 @@ func flush_pending_reveals(ri: int) -> void: var ok: bool = minesweeper.fx_reveal(cell) reveal.busy = false if not ok: - emit_cb.call("extended", "fx_reveal", Vector2i(-1, -1), ri, "揭示落空(格子不可揭)", 0, {"effect_id": "fx_reveal"}, "") + emit_cb.call({"type": "extended", "source_class": "fx_reveal", "origin_cell": Vector2i(-1, -1)}, ri, "揭示落空(格子不可揭)", 0, {"effect_id": "fx_reveal"}) continue grant_cb.call(cell, ri) - emit_cb.call("extended", "fx_reveal", cell, ri, "专长揭示", 0, {"effect_id": "fx_reveal"}, "fx_reveal") + emit_cb.call({"type": "extended", "source_class": "fx_reveal", "origin_cell": cell, "highlight": "fx_reveal"}, ri, "专长揭示", 0, {"effect_id": "fx_reveal"}) # 效果引擎订阅重建(每战从持有集/装备集重建,中途获得下一战生效;树规则合成订阅同面装载。 diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 44bc78a6..fc1a1258 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -1,9 +1,8 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION ## 战斗结算编排中枢:轮次生命周期唯一 owner,五段结算(授予→增益/减益→战斗→衰减→判定)与明细行协议发射(垂直切片验证:一场完整战斗逐行归因)。 -## 效果引擎施加面归 BattleFxHost(core/battle_fx_host——fx 族持有与兑付)、奖励授予管线归 RewardGrant -## (core/reward_grant——极性路由/行应用/秘宝使用/escrow);本类保留宿主职责面=状态机与生命周期、 -## 轮 facts 缓冲、A2 三事件订阅、五段驱动、attune 族、行发射基建(_emit_row/_assert_row_source/_fatal)、 -## 呈现投影、刷新面;两协作件经组件出口 fx_host()/reward_grant() 取件,禁链式穿透其内部。 +## 效果引擎施加面归 BattleFxHost、奖励授予管线归 RewardGrant(两拆件,经组件出口 fx_host()/ +## reward_grant() 取件,禁链式穿透其内部);本类保留宿主职责面=状态机与生命周期、轮 facts 缓冲、 +## A2 三事件订阅、五段驱动、attune 族、行发射基建(_emit_row/_assert_row_source/_fatal)、呈现投影、刷新面。 ## 零 await;随机只经 rng 封闭流(encounter/behavior_roll/armor_roll/gp_drop)——同种子同输入逐位复现。 ## 呈现层读取唯一出口 = presentation_snapshot()/diagnostic_snapshot(),禁链式穿透内部协作对象。 ## 伤害只向 DamageController 开单(何时开单归编排、怎么算归控制器);刷新唯一通道 player_request_refresh,禁 RAP 满自动刷。 @@ -37,8 +36,7 @@ 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() 成文出口(呈现层链式穿透 settlement._enemy_system.hero_unit() 的根因收口)。 +# E1 封装收口:协作对象 board_generator/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 @@ -86,8 +84,7 @@ var nest_preview_count := -1 # 巢穴预告读数口(board_insta var _feat_first_dig_used := false -# 效果引擎宿主侧事实单点(fx_host facts_cb 注入——round_index/battle_index/seals_count/nest_count/ -# hp_ratio 五事实;charge_layers 归 fx_host 自答不经此;未知事实回落 null) +# 效果引擎宿主侧事实单点(fx_host facts_cb 注入——round_index/battle_index/seals_count/nest_count/hp_ratio 五事实;charge_layers 归 fx_host 自答;未知事实回落 null) func _fx_host_fact(fact: String) -> Variant: match fact: "nest_count": @@ -112,8 +109,7 @@ func _fx_host_fact(fact: String) -> Variant: return null -# attune 积点读投影(RewardGrant attune_safe_seen_cb 注入面——use_treasure 拒绝文案读数; -# attune 族原位保留,经读投影出户防组件回调宿主私有族) +# attune 积点读投影(RewardGrant attune_safe_seen_cb 注入面——use_treasure 拒绝文案读数;经读投影出户防组件回调宿主私有族) func _attune_safe_seen_read() -> int: return _attune_safe_seen @@ -217,12 +213,11 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: _reward_economy = RewardEconomy.new(db, rng) _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, "damage": _damage, "emit_cb": _emit_row, "facts_cb": _fx_host_fact}) - _reward_grant = RewardGrant.new({"db": db, "reward_economy": _reward_economy, "damage": _damage, - "fx_host": _fx_host, "action_economy": action_economy, "status_effects": _status_effects, - "units": _enemy_system, "emit_cb": _emit_row, "fatal_cb": _fatal, - "attune_safe_seen_cb": _attune_safe_seen_read, "pool_effective_cb": _pool_effective}) + _fx_host = BattleFxHost.new({"db": db, "rng": rng, "minesweeper": minesweeper, "action_economy": action_economy, + "damage": _damage, "emit_cb": _emit_row, "facts_cb": _fx_host_fact}) + _reward_grant = RewardGrant.new({"db": db, "reward_economy": _reward_economy, "damage": _damage, "fx_host": _fx_host, + "action_economy": action_economy, "status_effects": _status_effects, "units": _enemy_system, "emit_cb": _emit_row, + "fatal_cb": _fatal, "attune_safe_seen_cb": _attune_safe_seen_read, "pool_effective_cb": _pool_effective}) _fx_host.bind_grant(_reward_grant.reveal_redeem) # 构造序禁环:两件建齐后接线(fx 件对授予面走注入回调) _damage.set_fx(_fx_host.engine, _fx_host.counters_ctx) # 效果引擎注入(attack_commit/clamp_check/armor_roll/kill/damage_taken 发射面) action_economy.set_fx(_fx_host.engine, _fx_host.counters_ctx) # dig_paid/charge_full 发射面 @@ -240,11 +235,34 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: # ---- 战斗开始序列(编排专章①:门 → 增益状态 → 敌人系统 → install+roll → 行动经济)---- - +# 段序=受理与敌人 spawn → 装板 → 同调/fx 运行态与订阅重建 → 常时参数与充能重置 → 巢穴预告与经济注入 → 轮始发射;各段=_begin_<段> 私有族。 func begin_battle(enc: Dictionary) -> bool: if round_index != 0: push_warning("[战斗结算] begin_battle 重复受理") return false + if not _begin_battle_accept(enc): + return false + if not _install_new_board(): + return false + _reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险) + _begin_battle_fx_state(enc) + _begin_battle_attune_params(enc) + _begin_battle_preview_economy(enc) + # 工兵晨令=round_start grant_ap(mode:queue) 订阅行承载(when round_index ≤ effective_rounds + # value_from 定影);round_start 发射先于行动经济轮切换=树规则 queue 型当轮入队当轮兑现 + # (先手压制=首轮 AP+1 语义锚);计数器 round_start 重置点同点生效 + round_index = 1 + _fx_host.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 + + +# 受理段:encounter 装载+缓冲清零+护甲合流+重载闸+开域门+遭遇骰与敌人系统 spawn 受理门。 +func _begin_battle_accept(enc: Dictionary) -> bool: encounter.assign(enc) _reward_grant.reset_battle() # 战斗域零 GP 持久(每场战斗缓冲从零起算;案C 闸复位) _damage.set_hero_ac_override(int(enc.get("hero_armor_class", -1))) # 主角护甲合流入参(伤害控制器 defender_ac 消费) @@ -252,27 +270,30 @@ func begin_battle(enc: Dictionary) -> bool: if not _status_effects.notify_battle_start(): _fatal("增益状态 开域被拒(编排违例)") return false - # 遭遇骰(难度曲线与Boss GDD Core Rule 8):行有 unit_count_max 时按 [unit_count, max] 均匀骰; - # 行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值 - # (每拍抖动否决——数字可读性锚;中点 = 行值)。encounter 封闭流恰一 scope(数量骰先、atk 骰后); - # 挖掘决策域之外——P1 合规;敌人系统 零随机纪律保持(收 override 即确定值)。 - # 遭遇骰(难度曲线与Boss GDD Core Rule 8):行有 unit_count_max 时按 [unit_count, max] 均匀骰; - # 行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值 - # (每拍抖动否决——数字可读性锚;中点 = 行值)。encounter 封闭流恰一 scope(数量骰先、atk 骰后); - # 挖掘决策域之外——P1 合规;敌人系统 零随机纪律保持(收 override 即确定值)。 - # 逐行骰:混编行集**每行各掷自己的区间骰**—— - # 先按行序逐行数量骰、再按行序逐行 atk 骰(单行行集 = 既有一次数量骰 + 一次 atk 骰,判定条件、 - # 开流条件、消费序、消费次数逐位一致——零内容期随机流零变化;区间宽度为零的行不掷 = 「定值不骰」 - # 既有口径;atk 区间逐行同构 = 行值语义完整,属「每行一骰」语义内的 - # 行字段区间骰逐行化,非新增随机源形态)。 - # 行类维度:节点身份 battle/boss 决定行集键域(缺省 battle 只服务测试直呼面——生产路径恒传 node_type) - var enc_kind := String(enc.get("node_type", "battle")) - var erowset_pre: Array[Variant] = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth), enc_kind) - if erowset_pre.is_empty(): - _fatal("敌人表 行集解析为空(%d,%d,%s)" % [int(enc.level_index), int(enc.node_depth), enc_kind]) + var dice := _begin_battle_dice(enc) + if dice.is_empty(): + _fatal("敌人表 行集解析为空(%d,%d,%s)" % [int(enc.level_index), int(enc.node_depth), String(enc.get("node_type", "battle"))]) return false - var count_overrides: Array[Variant] = [] - var atk_overrides: Array[Variant] = [] + # 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), dice[0], + _hero_atk, int(enc.get("hero_max_hp", -1)), dice[1], + String(enc.get("attack_type", "")), String(enc.get("armor_type", "")), String(enc.get("node_type", "battle"))): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss) + _fatal("敌人系统 spawn 被拒") + return false + return true + + +# 遭遇骰(难度曲线与Boss GDD Core Rule 8):行有 unit_count_max 时按 [unit_count, max] 均匀骰; +# 行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值 +# encounter 封闭流恰一 scope(数量骰先、atk 骰后——区间宽度为零的行不掷=「定值不骰」既有口径); +# 敌人系统零随机纪律保持(收 override 即确定值)。 +# 返回 [count_overrides, atk_overrides];行集为空 = 返回 [](fail 判据交受理段 _fatal)。 +func _begin_battle_dice(enc: Dictionary) -> Array: + # 行类维度:节点身份 battle/boss 决定行集键域(缺省 battle 只服务测试直呼面——生产路径恒传 node_type) + var erowset_pre: Array[Variant] = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth), String(enc.get("node_type", "battle"))) + if erowset_pre.is_empty(): + return [] var needs_count := false var needs_atk := false for r in erowset_pre: @@ -280,38 +301,38 @@ func begin_battle(enc: Dictionary) -> bool: 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, - String(enc.get("attack_type", "")), String(enc.get("armor_type", "")), enc_kind): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss) - _fatal("敌人系统 spawn 被拒") - return false - if not _install_new_board(): - return false - _reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险) + if not (needs_count or needs_atk): + return [[], []] # 区间全定值 = 零骰合法态(不开流零取号) + var estream: RandomNumberGenerator = rng.open_stream("encounter") + return [_begin_battle_dice_roll(erowset_pre, estream, false), _begin_battle_dice_roll(erowset_pre, estream, true)] + + +# 区间骰单轴(roll_atk=false=数量骰〔宽度为零的行不掷=「定值不骰」〕/true=atk 骰〔行缺 atk_min 记 -1=无覆盖表值〕;消费序=数量骰先、atk 骰后) +func _begin_battle_dice_roll(rows: Array, estream: RandomNumberGenerator, roll_atk: bool) -> Array: + var out: Array[Variant] = [] + for r in rows: + if roll_atk: + if r.has("atk_min"): + out.append(int(r.atk_min) + estream.randi_range(0, int(r.atk_max) - int(r.atk_min))) + else: + out.append(-1) + else: + var lo: int = int(r.get("unit_count", 1)) + var hi: int = int(r.get("unit_count_max", lo)) + out.append((lo + estream.randi_range(0, hi - lo)) if hi > lo else lo) + return out + + +# 淬毒/偏运=triggers 订阅行(淬毒=clamp_check +# modify 窗口、偏运=reward_roll bias_rewards;value 行字段 value_from 定影,begin_battle 不注入) +# 特性运行态(enc.feats 携带持有集:效果行为=数据行 triggers 经效果 +# 引擎分发;流派档位=FeatsEngine 纯函数求值两运行态注伤害控制器。魔矢每战首次开格计数同步归零) +# 扩签运行态就位(归 fx_host)+账本挂接+桶挂引用+揭示/AC 两回调+rider 档位恒零+订阅重建。 +func _begin_battle_fx_state(enc: Dictionary) -> void: # 同调装配运行态注入(enc.attunement 携带;off=空集零扰动。淬毒 N 注入 # 伤害控制器、偏运 N 注入地图战斗资源——均须先于首板 install) attunement = (enc.get("attunement", []) as Array).duplicate() _attune_safe_seen = 0 - # 淬毒/偏运=triggers 订阅行(淬毒=clamp_check - # modify 窗口、偏运=reward_roll bias_rewards;value 行字段 value_from 定影,begin_battle 不注入) - # 特性运行态(enc.feats 携带持有集:效果行为=数据行 triggers 经效果 - # 引擎分发;流派档位=FeatsEngine 纯函数求值两运行态注伤害控制器。魔矢每战首次开格计数同步归零) - # 扩签运行态就位(归 fx_host:AC 基线快照、累积器清零、揭示队列+再入闸清零、bonus_delta 四轴清零) _fx_host.reset_battle_state(int(enc.get("hero_armor_class", -1))) var ledger_ref = enc.get("ledger", null) _ledger = ledger_ref if ledger_ref is RunLedger else RunLedger.new() # 缺省新账=调试/单场零扰动面 @@ -329,51 +350,41 @@ func begin_battle(enc: Dictionary) -> bool: # 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1)。其余 rule 键 # =stat 面投影不入战斗(T8 表内容 provisional 口径披露)) var tree_rules: Array[Variant] = enc.get("tree_rules", []) as Array - # 效果引擎订阅重建(每战从持有集/装备集重建,中途获得下一战生效; - # 树规则合成订阅同面装载。装载序=feats 表序→treasure 表序→树规则,写死保种子复放。 - # 计数器四桶经 counters_ctx 引用宿主容器,无回灌时点(旧战首回灌时点删除)) + # 效果引擎订阅重建(装载序=feats 表序→treasure 表序→树规则,写死保种子复放——细则见 battle_fx_host.rebuild_subscriptions) _fx_host.rebuild_subscriptions(feat_ids, attunement, tree_rules) - # 秘宝常时参数注入(装配集 kind 分发:远程镐挖距放宽/信标网络封印可达豁免; - # 缺省 0/false=零装配零扰动。注入在首板 install 之后〔板在早段〕——板生成不读两参数, - # 参数只影响挖掘/插棋动作判据,时序安全) + + +# 秘宝常时参数注入(装配集 kind 分发:远程镐挖距放宽/信标网络封印可达豁免; +# 缺省 0/false=零装配零扰动。注入在首板 install 之后〔板在早段〕——板生成不读两参数, +# 参数只影响挖掘/插棋动作判据,时序安全)+充能重置周期(battle=每战回满/level=每关首战回满 +# 〔battle_index==1〕;adventure 周期维持冒险开始 _seed_attune_charges 单次) +func _begin_battle_attune_params(enc: Dictionary) -> void: minesweeper.dig_range_bonus = attune_param_sum("dig_range_bonus") minesweeper.seal_range_free = attune_param_sum("seal_range_free") > 0 - # 充能重置周期(battle=每战回满/level=每关首战回满〔battle_index==1〕; - # adventure 周期维持冒险开始 _seed_attune_charges 单次) _reseed_attune_charges("battle") if int(enc.get("battle_index", 1)) == 1: _reseed_attune_charges("level") - # board_installed 发射+施加(巢穴预告:装板后一次报本板危险格数量; - # 只报数量不标位置=信息红线,不碰分布=棋盘事实不可变契约 3。命中=读数口落值,呈现归 - # 呈现桥消费(无格来源不进结算明细行——行来源纪律);板已就绪〔早段 install〕、订阅行已在 rebuild 装载) + + +# board_installed 发射+施加(巢穴预告:装板后一次报本板危险格数量; +# 只报数量不标位置=信息红线,不碰分布=棋盘事实不可变契约 3。命中=读数口落值,呈现归 +# 呈现桥消费(无格来源不进结算明细行——行来源纪律);板已就绪〔早段 install〕、订阅行已在 rebuild 装载)+ +# 跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现 +# 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后 +# 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节)+ +# 储备金袋(节点级 RAP 注入)接收(完美保险注入= feats.tres 订阅承载,不经此通道) +func _begin_battle_preview_economy(enc: Dictionary) -> void: nest_preview_count = -1 var bi_hits := _fx_host.fx_emit("board_installed", {}) for h in bi_hits: if String(h.get("kind", "")) == "mine_count_preview": nest_preview_count = minesweeper.mine_total() - # 跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现 - # 「每场战斗开始首轮 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)) # 时沙同通道(首轮池增量,不改上限) if ap_bonus > 0: action_economy.queue_ap_delta(ap_bonus) - # 储备金袋(节点级 RAP 注入)接收(完美保险注入= feats.tres 订阅承载,不经此通道) var voucher_rap: int = int(enc.get("voucher_rap", 0)) if voucher_rap > 0: action_economy.grant_reserve_ap(voucher_rap) - # 工兵晨令=round_start grant_ap(mode:queue) 订阅行承载, - # when round_index ≤ effective_rounds 行字段 value_from 定影;发射位先于轮切换=当轮兑现 - # round_start 发射+施加(发射位先于行动经济轮切换:树规则 grant_ap - # mode:queue 当轮入队、notify_round_start 即兑现(先手压制=首轮 AP+1 语义锚); - # 计数器 round_start 重置点同点生效) - round_index = 1 - _fx_host.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: @@ -381,10 +392,7 @@ func _install_new_board(during_round := false) -> bool: var board: BoardData = _board_generator.generate(int(encounter.level_index), int(encounter.node_depth), stream, db) if board == null: return false - # 全局作用域变异决议(装板序内前置于 roll_board:图级偏斜旋钮须在资源 - # 摇号前定型;全局门控与板无关〔节点身份+unlock+board_types〕,确定性取用零取号)。 - # boss/elite 图命中全局型 → 写 board_variants(_place_variants 见非空即跳 region=替换制); - # 全局池空 → 整图字段留空(_place_variants 原路走 region=兜底回落合法态)。 + # 全局作用域变异决议(装板序内前置于 roll_board:图级偏斜旋钮须在资源摇号前定型——详见 _resolve_global_variant) _resolve_global_variant(board) var res: Dictionary[Variant, Variant] = minesweeper.install_board(board, during_round) if not res.accepted: @@ -424,7 +432,7 @@ func _install_new_board(during_round := false) -> bool: # boss/elite 图命中全局型:写整图 board_variants(恰一行)+ 注入资源面偏斜旋钮(图级投影在 # RewardEconomy.roll_board 消费);不消费 variant_place 流(全局取用确定性——零取号合法态)。 # 全局池空(含 L1 Boss 免疫/unlock 门控未达):字段留空、旋钮归零——_place_variants 原路走 -# region 布置 = 兜底回落(保守向后兼容,防配置缺行静默裸奔——provisional)。 +# region 布置 = 兜底回落合法态(防配置缺行静默裸奔——provisional)。 func _resolve_global_variant(board: BoardData) -> void: board.board_variants.clear() _reward_economy.board_skew = 0 @@ -441,12 +449,10 @@ func _resolve_global_variant(board: BoardData) -> void: # 池空 = 合法态,静默零布置(框架 P0 口径不变)。零特判:本段只写格子变异数据快照,不改扫雷规则本体。 # 候选格域 = 安全格 − 初始展开区(伤害型 placement=closed_only 的结构保障——布置发生在初始展开之后, # 已开格天然排除);危险格不吃变异(挖雷一层惩罚 + 变异伤害双层叠罚污染逐行归因,首版排除——provisional)。 -# 每图至多一种类型(候选池内均匀选一,混型布置归因复杂留待后续);数量 = round(候选池格数×percent) + -# (关卡序号−1)×per_level_bonus——基数为候选池而非总格数(初始展开 -# 占全板 56–67%,总格数基数使有效密度膨胀 4–6 倍、L1 小板极端全池覆盖;percent 语义 = 占可挖格比例, -# 关卡递增由 per_level_bonus 绝对格数承载);连通片生长(8 邻),单片 ≤ pack_max(2×2 / 1×4 / 1×3+1 等形)。 -# 替换制(全局作用域变异):整图 board_variants 非空 = 本图已带全局型,跳过 region -# 布置(每图一型的整图推广:普通图=region,Boss/精英图=global);空 = 原路径(含 Boss 图全局池空回落)。 +# 每图至多一种类型(候选池内均匀选一);数量 = 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 @@ -467,6 +473,11 @@ func _place_variants(board: BoardData) -> void: 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()) + _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.variation.pack_max))) @@ -478,18 +489,13 @@ func _place_variants(board: BoardData) -> void: continue free.append(c) if free.is_empty(): - break + 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: 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) + 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)] @@ -500,6 +506,17 @@ func _place_variants(board: BoardData) -> void: 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): @@ -561,8 +578,7 @@ func _maybe_settle() -> void: func _settle() -> void: _settling = true var ri := round_index - # 段序编排(五段 Segment;段体归 game/core/settlement/,本类只做段序+ - # 行发射/开单原语单点) + # 段序编排(五段——段体归 game/core/settlement/,本类只做段序+行发射/开单原语单点) _grant_seg.run(ri) # ① 授予段(案C 闸随段装载) _aura_seg.run(ri) # ② 增益/减益段(续存实例组合行) _combat_seg.run(ri) # ③ 战斗段(主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕) @@ -577,7 +593,7 @@ func _settle() -> void: # 开格产出乘区(贪婪矿灯/矿脉直觉/赌徒的血签事件收集器注入): # positive=power ×(1+静态 mult%);negative=power ×(1+cost%)。 -# 全零收集器=原快照原样返回(零扰动)。旧 dig_combo 连击加成死读数(全仓无声明行、combo_val 恒 0)已删——零行为差。 +# 全零收集器=原快照原样返回(零扰动)。 func _feat_yield_adjust(snap: Dictionary, positive: bool, pos_m: int, neg_m: int) -> Dictionary: if positive and pos_m <= 0: return snap @@ -643,9 +659,9 @@ func _roll_round_actions() -> void: # 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏) for sid in _enemy_system.check_skill_unlocks(u): var srow_u: Dictionary[Variant, Variant] = 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") + _emit_row({"type": "highlight", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "highlight": "skill_unlock"}, + round_index, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))], + 0, {"skill_id": sid}) # enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射, # 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 unlock_hp_pct 表值) var pct_v := 0 @@ -670,15 +686,8 @@ func _roll_round_actions() -> void: _enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记) -# ---- 战斗段 ---- -# 本段只开单——主角先手单 + 敌方伤害技单;掷点/折扣/暴击/克制/ -# 池乘区/下限/交付/commit 全部收口 伤害控制器(A 模式提交即结;B 模式段尾 flush)。 -# 开单守卫归流程(流程决定给谁开单):主角存活+目标存在才开先手单;hero hp=0 立即 -# 停开(B 模式下若 flush 中主角倒下,由控制器死亡事件终止余单——两模式同一结果)。 - -# ---- 算术读数 ---- -# 掷点/折扣/克制点数/池乘区/攻击行文本 归 伤害控制器(damage_controller.gd 管线单点); -# 本类只保留一处薄委托——_defender_ac(快照面消费)。 +# ---- 战斗段/算术读数(段体与掷点/折扣/暴击/克制/池乘区/下限/交付管线归 damage_controller.gd +# 与 settlement/combat_segment.gd;本类只保留薄委托——_defender_ac 供结算/呈现快照消费)---- # 防守方护甲等级读数——实现归 伤害控制器 defender_ac(本委托供结算快照/呈现快照) func _defender_ac(unit: Dictionary) -> int: @@ -700,7 +709,8 @@ 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}) + _emit_row({"type": "effect", "source_class": eff.source_class, "origin_cell": eff.origin_cell}, ri, + eff.display_name, eff.snap_magnitude, {"effect_id": eff.effect_id, "pool_value": eff.effective_value}) # ---- 结算快照(playtest 数值分析数据面——每次结算全量状态;运行时零决策读取)---- @@ -800,12 +810,10 @@ func _battle_over(result: String) -> void: var hu_w: Dictionary[Variant, Variant] = _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 战末汇入(血税双向账:本战累积并入冒险加算区共享引用后清零)+ - # adventure 域计数器导出回写(T6——共享引用覆写,attune_charges 先例) var drained := _fx_host.bonus_delta_drain() if not _ledger.bonus.is_empty(): for axis in drained.keys(): _ledger.bonus_add_axis(axis, int(drained[axis])) - # 搬运链删除:adventure 桶经 ctx 引用直写 RunLedger,无战末导出时点(旧导出/汇入搬运链删除) # 映射随战斗结束退役(地图战斗资源 义务④——下一场战斗 = 新板新 scope 批,roll_board 相位守卫的前提) var hero_hp_left := int(_enemy_system.hero_unit().get("hp", 0)) # despawn 前捕获(信号载荷供呈现层"HP 带入下一层") var gp_dropped := _roll_gp_drop() # 战斗掉落(escrow 随此值上抛即弃) @@ -938,7 +946,13 @@ func _reset_round_facts() -> void: # ---- 行协议(Core Rule:公共字段 + 行类型特有字段)---- -func _emit_row(type: String, source_class: String, origin_cell: Vector2i, ri: int, display: String, value: int, extra: Dictionary, highlight: String = "") -> void: +func _emit_row(head: Dictionary, ri: int, display: String, value: int, extra: Dictionary) -> void: + # head 归因束封闭键集:type/source_class/origin_cell/highlight(缺省"")——束形只改函数形参, + # 行字典字段与 step_row 载荷逐字不变(行协议词表零触碰) + var type := String(head.get("type", "")) + var source_class := String(head.get("source_class", "")) + var origin_cell: Vector2i = head.get("origin_cell", Vector2i(-1, -1)) + var highlight := String(head.get("highlight", "")) # 扩展行必载效果身份(行协议 fail-fast——违约行先炸后不发,零发射;开发期违约即炸, # 与 _assert_row_source 同族 assert 形态) if type == "extended" and String(extra.get("effect_id", "")) == "": diff --git a/game/core/damage_controller.gd b/game/core/damage_controller.gd index 3648ac9e..6b5428a0 100644 --- a/game/core/damage_controller.gd +++ b/game/core/damage_controller.gd @@ -33,7 +33,7 @@ var db: ConfigDb var rng: RngServiceCore # 注入成员类型注(单测经真实 RngService 构造,标注安全) var units: UnitContainer # UnitContainer——目标解析/伤害交付/存活查询(句柄即 Dictionary 引用) var effects: StatusContainer # StatusContainer——池值/AC 型效果读取(增益状态 只读消费面) -var emit_row_cb: Callable # 行协议注入口:func(type, source_class, origin_cell, ri, display, value, extra, highlight) +var emit_row_cb: Callable # 行协议注入口:func(head, ri, display, value, extra)——head 归因束键集 type/source_class/origin_cell/highlight(缺省"") var policy := POLICY_SEGMENT # 结账时机开关(B=攒批段界统一结;回切 A 改 POLICY_IMMEDIATE 即开单即结) var _queue: Array[Dictionary] = [] # 未结单据(B 模式攒批;A 模式恒空) @@ -430,7 +430,7 @@ func _hero_attack_deliver(t: Dictionary, ctx: Dictionary, redemption: Dictionary var label := _attack_label(String(t.display), bool(ctx["crit"]), int(ctx["discount"])) if int(redemption["hero_rider"]) > 0: label += "+奥术%d(必中)" % int(redemption["hero_rider"]) - emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), label, int(redemption["landed"]), payload, String(t.highlight)) + emit_row_cb.call({"type": "damage", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index), label, int(redemption["landed"]), payload) _outcomes[int(t.ticket_id)] = "redeemed" @@ -477,7 +477,7 @@ func _resolve_enemy_skill(t: Dictionary) -> void: var label := _attack_label(String(t.display), crit, discount) if int(t.rider) > 0: label += "+附加%d(必中)" % int(t.rider) - emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), label, total, payload, String(t.highlight)) + emit_row_cb.call({"type": "damage", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index), label, total, payload) units.commit_action(t.source_unit) _outcomes[int(t.ticket_id)] = "redeemed" @@ -501,8 +501,9 @@ func _resolve_board_typed(t: Dictionary) -> void: cdelta += int(h.delta) var dealt: int = DamageMath.counter_component(int(t.base_value), cdelta, int(db.settlement.damage_floor)) units.apply_damage(target, dealt) - emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), "%s → %s" % [String(t.display), String(target.display_name)], dealt, - {"target": String(target.display_name), "target_id": String(target.get("enemy_id", "")), "attack_type": String(t.attack_type), "effectiveness": cdelta}, String(t.highlight)) + emit_row_cb.call({"type": "damage", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index), + "%s → %s" % [String(t.display), String(target.display_name)], dealt, + {"target": String(target.display_name), "target_id": String(target.get("enemy_id", "")), "attack_type": String(t.attack_type), "effectiveness": cdelta}) _outcomes[int(t.ticket_id)] = "redeemed" @@ -516,8 +517,9 @@ func _resolve_board_blast(t: Dictionary) -> void: var blast_dmg: int = maxi(int(db.settlement.damage_floor), int(t.base_value)) # 伤害下限全来源 for u in alive: units.apply_damage(u, blast_dmg) # 恒定表值(下限保底),逐个;受击即死——死亡即不动手 - emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), "%s → 全体敌人 ×%d" % [String(t.display), alive.size()], blast_dmg, - {"target": "all_enemies", "count": alive.size()}, String(t.highlight)) + emit_row_cb.call({"type": "damage", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index), + "%s → 全体敌人 ×%d" % [String(t.display), alive.size()], blast_dmg, + {"target": "all_enemies", "count": alive.size()}) _outcomes[int(t.ticket_id)] = "redeemed" @@ -528,7 +530,7 @@ func _resolve_board_flat_hero(t: Dictionary) -> void: var extra: Dictionary[Variant, Variant] = {"target": "hero"} for k in t.context.keys(): extra[k] = t.context[k] - emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), "%s → 主角" % String(t.display), dealt, extra, String(t.highlight)) + emit_row_cb.call({"type": "damage", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index), "%s → 主角" % String(t.display), dealt, extra) _outcomes[int(t.ticket_id)] = "redeemed" @@ -625,5 +627,5 @@ func _target_for(rule: String) -> Variant: func _ineffective_row(t: Dictionary, reason: String) -> void: - emit_row_cb.call("reward_ineffective", String(t.source_class), t.origin_cell, int(t.round_index), - "%s 未生效(%s)" % [String(t.display), reason], 0, {"batch_key": String(t.context.get("batch_key", ""))}, String(t.highlight)) + emit_row_cb.call({"type": "reward_ineffective", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index), + "%s 未生效(%s)" % [String(t.display), reason], 0, {"batch_key": String(t.context.get("batch_key", ""))}) diff --git a/game/core/reward_grant.gd b/game/core/reward_grant.gd index b353c0c9..e0ea55bc 100644 --- a/game/core/reward_grant.gd +++ b/game/core/reward_grant.gd @@ -206,7 +206,7 @@ func grant_routed(snap: Dictionary, polarity: String, att: Dictionary) -> bool: if halve_pct > 0 and polarity == "negative" and source_class == "mine_hit": row = row.duplicate() var half: int = int(ceil(float(int(row.power)) * float(100 - halve_pct) / 100.0)) - emit_cb.call("effect", source_class, cell, ri, "惊巢反制——危险格挖掘惩罚减半(%d→%d)" % [int(row.power), half], 0, {"effect_id": "nest_counter"}, highlight) + emit_cb.call({"type": "effect", "source_class": source_class, "origin_cell": cell}, ri, "惊巢反制——危险格挖掘惩罚减半(%d→%d)" % [int(row.power), half], 0, {"effect_id": "nest_counter"}) row.power = half return _dispatch_row(row, att) @@ -235,12 +235,13 @@ func _apply_percent_row(row: Dictionary, att: Dictionary) -> bool: eff_mag = int(round(eff_mag * prem_pct / 100.0)) var ok: bool = status_effects.apply_effect(eff, _grant_src(att, prem_pct, eff_mag)) if ok: - emit_cb.call("effect", String(att.get("source_class", "")), att.get("cell", Vector2i(-1, -1)), int(att.get("ri", 0)), - String(srow.display_name) + String(att.get("label_suffix", "")), eff_mag, - {"effect_id": eff, "pool_value": int(pool_effective_cb.call(eff))}, String(att.get("highlight", ""))) + emit_cb.call({"type": "effect", "source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1))}, + int(att.get("ri", 0)), String(srow.display_name) + String(att.get("label_suffix", "")), eff_mag, + {"effect_id": eff, "pool_value": int(pool_effective_cb.call(eff))}) return true - emit_cb.call("reward_ineffective", String(att.get("source_class", "")), att.get("cell", Vector2i(-1, -1)), int(att.get("ri", 0)), - "奖励未生效(满层)%s" % String(att.get("label_suffix", "")), 0, {"batch_key": eff}, String(att.get("highlight", ""))) + emit_cb.call({"type": "reward_ineffective", "source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1)), "highlight": String(att.get("highlight", ""))}, + int(att.get("ri", 0)), "奖励未生效(满层)%s" % String(att.get("label_suffix", "")), 0, + {"batch_key": eff}) return false @@ -260,9 +261,9 @@ func _apply_gp_row(row: Dictionary, att: Dictionary) -> bool: var eff := String(row.effect_id) var delta: int = int(row.power) if eff == "gp_gain" else -int(row.power) escrow += delta - emit_cb.call("effect", String(att.get("source_class", "")), att.get("cell", Vector2i(-1, -1)), int(att.get("ri", 0)), - "%s %+d%s" % [String(row.display_name), delta, String(att.get("label_suffix", ""))], delta, - {"effect_id": eff, "escrow": escrow}, String(att.get("highlight", ""))) + emit_cb.call({"type": "effect", "source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1)), "highlight": String(att.get("highlight", ""))}, + int(att.get("ri", 0)), "%s %+d%s" % [String(row.display_name), delta, String(att.get("label_suffix", ""))], delta, + {"effect_id": eff, "escrow": escrow}) return true @@ -293,9 +294,9 @@ func _flat_action(row: Dictionary, display: String, att: Dictionary) -> String: var power := int(row.power) var delta: int = power if String(row.get("polarity", "positive")) == "positive" else -power action_economy.queue_ap_delta(delta) - emit_cb.call("effect", String(att.get("source_class", "")), att.get("cell", Vector2i(-1, -1)), int(att.get("ri", 0)), - "%s(下轮行动点 %+d)" % [display, delta], delta, - {"effect_id": "ap_delta", "pool_value": action_economy.pending_ap_delta()}, String(att.get("highlight", ""))) + emit_cb.call({"type": "effect", "source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1)), "highlight": String(att.get("highlight", ""))}, + int(att.get("ri", 0)), "%s(下轮行动点 %+d)" % [display, delta], delta, + {"effect_id": "ap_delta", "pool_value": action_economy.pending_ap_delta()}) return "redeemed" @@ -325,16 +326,18 @@ func _flat_heal(row: Dictionary, display: String, att: Dictionary, sync: bool) - return _submit_flat_hero(display, power, att, sync) if int(units.hero_unit().get("hp", 0)) <= 0: # 主角已倒下(同轮更早的负向直伤致死)——治疗不再合法作用,按"奖励未生效"留痕 - emit_cb.call("reward_ineffective", String(att.get("source_class", "")), att.get("cell", Vector2i(-1, -1)), int(att.get("ri", 0)), - "%s 未生效(主角已倒下)" % display, 0, {"batch_key": String(row.reward_id)}, String(att.get("highlight", ""))) + emit_cb.call({"type": "reward_ineffective", "source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1)), "highlight": String(att.get("highlight", ""))}, + int(att.get("ri", 0)), "%s 未生效(主角已倒下)" % display, 0, + {"batch_key": String(row.reward_id)}) return "zero" if int(units.hero_unit().hp) >= int(units.hero_unit().max_hp): - emit_cb.call("reward_ineffective", String(att.get("source_class", "")), att.get("cell", Vector2i(-1, -1)), int(att.get("ri", 0)), - "%s 未生效(满血)" % display, 0, {"batch_key": String(row.reward_id)}, String(att.get("highlight", ""))) + emit_cb.call({"type": "reward_ineffective", "source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1)), "highlight": String(att.get("highlight", ""))}, + int(att.get("ri", 0)), "%s 未生效(满血)" % display, 0, + {"batch_key": String(row.reward_id)}) return "zero" var healed: int = units.heal(units.hero_unit(), power) # clamp 归 战斗结算 - emit_cb.call("heal", String(att.get("source_class", "")), att.get("cell", Vector2i(-1, -1)), int(att.get("ri", 0)), - "%s 回复" % display, healed, {"target": "hero"}, String(att.get("highlight", ""))) + emit_cb.call({"type": "heal", "source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1)), "highlight": String(att.get("highlight", ""))}, + int(att.get("ri", 0)), "%s 回复" % display, healed, {"target": "hero"}) return "redeemed" @@ -375,7 +378,8 @@ func grant_clear_reward(ri: int, skip_on_refresh: bool, perfect: bool) -> void: if perfect and first_ok: second_ok = _grant_clear_identity(snap, ri, "(完美第二份)") if perfect and first_ok: - emit_cb.call("highlight", "clear_reward", Vector2i(-1, -1), ri, "完美封印!清图奖励 ×2", (2 if second_ok else 1), {}, "perfect_sweep") + emit_cb.call({"type": "highlight", "source_class": "clear_reward", "origin_cell": Vector2i(-1, -1), "highlight": "perfect_sweep"}, + ri, "完美封印!清图奖励 ×2", (2 if second_ok else 1), {}) # 清图奖励 = 整图独立对象(不属于任何一格):兑付形态按其 cell_type 的正向字典形态走, diff --git a/game/core/settlement/combat_segment.gd b/game/core/settlement/combat_segment.gd index 11a2130c..33962343 100644 --- a/game/core/settlement/combat_segment.gd +++ b/game/core/settlement/combat_segment.gd @@ -74,7 +74,7 @@ func run(ri: int) -> void: # 落点由状态行 target_side 路由(buff → 敌方侧 / debuff → 主角侧——容器零改动,proposal-status-vocabulary §1)。 var dur: int = int(srow.get("buff_duration", 1)) oc._status_effects.apply_effect(String(srow.buff_ref), {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": ri}, dur) - oc._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}) + oc._emit_row({"type": "cast", "source_class": "enemy_action", "origin_cell": 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}) oc._enemy_system.commit_action(u) continue # 伤害技(含普攻——倍率恒 1)= 敌方伤害技单据:基线 = round(atk×倍率)、rider 预计算归开单侧 @@ -95,8 +95,8 @@ func run(ri: int) -> void: # 补提交后若地图直伤反杀主角,控制器 _halted 作废余单——战斗已败,胜负归判定段) if int(oc._enemy_system.hero_unit().get("hp", 0)) <= 0: for t in (reward_grant.defer["queue"] as Array): - oc._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"}, "") + oc._emit_row({"type": "reward_ineffective", "source_class": String(t.get("source_class", "")), "origin_cell": t.get("origin_cell", Vector2i(-1, -1))}, ri, + "%s 未生效(主角已倒下)" % String(t.get("display", "")), 0, {"forfeit": "hero_down"}) else: for t in (reward_grant.defer["queue"] as Array): oc._damage.submit(t) diff --git a/game/core/settlement/decay_segment.gd b/game/core/settlement/decay_segment.gd index ce942ea6..5acb1606 100644 --- a/game/core/settlement/decay_segment.gd +++ b/game/core/settlement/decay_segment.gd @@ -13,4 +13,4 @@ func _init(host) -> void: func run(ri: int) -> void: var k: int = oc._status_effects.decay_round() - oc._emit_row("decay", "", Vector2i(-1, -1), ri, "状态衰减", k, {}, "") + oc._emit_row({"type": "decay", "source_class": "", "origin_cell": Vector2i(-1, -1)}, ri, "状态衰减", k, {}) diff --git a/game/core/settlement/grant_segment.gd b/game/core/settlement/grant_segment.gd index cf3ac256..3f1d2ab9 100644 --- a/game/core/settlement/grant_segment.gd +++ b/game/core/settlement/grant_segment.gd @@ -35,7 +35,7 @@ func run(ri: int) -> void: if bool(evt.get("dug_under_seal", false)): # 封印不可逆四情况④:插安全格再挖开 = 零惩罚零奖励——正向兑付 # 作废(归因行在案);标空判定已随封印擦除自然缺席(被挖的封印不在轮末在场基线)。 - oc._emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(封印格直接挖开——零惩罚零奖励)", 0, {}, "") + oc._emit_row({"type": "reward_ineffective", "source_class": "safe_cell", "origin_cell": evt.cell}, ri, "奖励作废(封印格直接挖开——零惩罚零奖励)", 0, {}) continue # dig_reveal 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价: # 乘区收集 → 正向兑付 → 计数器 → 追加产出(深挖)→ 奥术矢。 @@ -220,7 +220,7 @@ func run(ri: int) -> void: for h in re_fx: if String(h.get("kind", "")) == "grant_gp": reward_grant.escrow += int(h.get("value", 0)) - oc._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": reward_grant.escrow}, "") + oc._emit_row({"type": "effect", "source_class": "fx_grant_gp", "origin_cell": Vector2i(-1, -1)}, ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": reward_grant.escrow}) elif String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue": oc.action_economy.queue_ap_delta(int(h.get("value", 0))) # 转存率入参(迭代 22 秘宝批 T3——储备罐 rap_transfer_mult magnitude=目标转存率百分比 125=1.25:1; @@ -273,7 +273,7 @@ func _judge_seal(evt: Dictionary, ri: int) -> void: "remove_mines": var removed: Array[Variant] = oc.minesweeper.remove_mines_in([cell]) if not removed.is_empty(): - oc._emit_row("extended", "fx_seal_smith", cell, ri, "封印匠·危险格中立移除", 0, {"effect_id": "mine_neutralized"}, "") + oc._emit_row({"type": "extended", "source_class": "fx_seal_smith", "origin_cell": cell}, ri, "封印匠·危险格中立移除", 0, {"effect_id": "mine_neutralized"}) "preview_area": var board_p: BoardData = oc.minesweeper.current_board() var types: Array[Variant] = [] @@ -283,7 +283,7 @@ func _judge_seal(evt: Dictionary, ri: int) -> void: types.append(String((n_snap as Dictionary).get("cell_type", ""))) if oc.treasure_inventory != null and not types.is_empty(): oc.treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell)) - oc._emit_row("extended", "fx_warning_beacon", cell, ri, "示警烟火·周围资源类型", 0, {"effect_id": "fx_preview_area", "types": types}, "") + oc._emit_row({"type": "extended", "source_class": "fx_warning_beacon", "origin_cell": cell}, ri, "示警烟火·周围资源类型", 0, {"effect_id": "fx_preview_area", "types": types}) # 雷区图谱(封印流——标中雷额外获得一份格面资源:封印开始产出对标开格; # 复用 grant 单点,额外兑付走同一 _grant_routed 正向路由) for h in fh_hits: @@ -311,10 +311,10 @@ func _judge_seal(evt: Dictionary, ri: int) -> void: # _perfect_forgiven,绝不回写 _perfect_hit_false=契约 2 计数纯净;发射时机与消费点零改动=契约 3) oc._perfect_forgiven += 1 _apply_counter_hit(h) # 豁免同时记账(insurance_used battle 域——下次标错 when lt 1 不再命中) - oc._emit_row("extended", "seal_miss", cell, ri, "完美保险·标错豁免", 0, - {"effect_id": "insurance_used", "forgiven": oc._perfect_forgiven}, "") + oc._emit_row({"type": "extended", "source_class": "seal_miss", "origin_cell": cell}, ri, "完美保险·标错豁免", 0, + {"effect_id": "insurance_used", "forgiven": oc._perfect_forgiven}) var hl := "perfect_broken" if oc._perfect_hit_false - oc._perfect_forgiven == 1 else "" - oc._emit_row("seal_miss", "seal_miss", cell, ri, "封印未中", 0, {}, hl) + oc._emit_row({"type": "seal_miss", "source_class": "seal_miss", "origin_cell": cell, "highlight": hl}, ri, "封印未中", 0, {}) var snap = oc._reward_economy.grant(cell) if not (snap is Dictionary): return diff --git a/game/core/settlement/verdict_segment.gd b/game/core/settlement/verdict_segment.gd index a0dbb532..dba782c6 100644 --- a/game/core/settlement/verdict_segment.gd +++ b/game/core/settlement/verdict_segment.gd @@ -21,13 +21,13 @@ func run(ri: int) -> String: verdict = "victory" oc._emit_settlement_snapshot(verdict) # 全量状态快照(despawn 前组装——终局含 result) if verdict == "defeat": - oc._emit_row("verdict", "", Vector2i(-1, -1), ri, "战斗失败", 0, {}, "") + oc._emit_row({"type": "verdict", "source_class": "", "origin_cell": Vector2i(-1, -1)}, ri, "战斗失败", 0, {}) oc._battle_over("defeat") return verdict if verdict == "victory": - oc._emit_row("verdict", "", Vector2i(-1, -1), ri, "战斗胜利", 1, {}, "") + oc._emit_row({"type": "verdict", "source_class": "", "origin_cell": Vector2i(-1, -1)}, ri, "战斗胜利", 1, {}) oc._battle_over("victory") return verdict - oc._emit_row("verdict", "", Vector2i(-1, -1), ri, "下一轮次", 0, {}, "") + oc._emit_row({"type": "verdict", "source_class": "", "origin_cell": Vector2i(-1, -1)}, ri, "下一轮次", 0, {}) oc._exit_distribution() return verdict diff --git a/game/tests/unit/battle_settlement/damage_controller_test.gd b/game/tests/unit/battle_settlement/damage_controller_test.gd index 168ca0fb..c4907dc4 100644 --- a/game/tests/unit/battle_settlement/damage_controller_test.gd +++ b/game/tests/unit/battle_settlement/damage_controller_test.gd @@ -40,9 +40,9 @@ func _init() -> void: # ---- 夹具 ---- -func _on_row(type, source_class, origin_cell, ri, display, value, extra, highlight) -> void: - _rows.append({"type": type, "source_class": source_class, "origin_cell": origin_cell, - "round_index": ri, "display": display, "value": value, "extra": extra, "highlight": highlight}) +func _on_row(head, ri, display, value, extra) -> void: + _rows.append({"type": head.get("type", ""), "source_class": head.get("source_class", ""), "origin_cell": head.get("origin_cell", Vector2i(-1, -1)), + "round_index": ri, "display": display, "value": value, "extra": extra, "highlight": head.get("highlight", "")}) func _harness(db): diff --git a/game/tests/unit/battle_settlement/extended_row_violation_test.gd b/game/tests/unit/battle_settlement/extended_row_violation_test.gd index c2f31718..0198c410 100644 --- a/game/tests/unit/battle_settlement/extended_row_violation_test.gd +++ b/game/tests/unit/battle_settlement/extended_row_violation_test.gd @@ -23,13 +23,13 @@ func _init() -> void: oc.step_row.connect(func(row): hits.append(row)) # 违约直调(缺 effect_id 载荷——行协议违约):debug 下 a2 校验 assert 炸中断 _emit_row 本体, # 控制流回到本函数(headless assert 语义=中断当前函数不中止运行);release 下 return 零发射同效 - oc._emit_row("extended", "fx_reveal", Vector2i.ZERO, 1, "负路径", 0, {}, "") + oc._emit_row({"type": "extended", "source_class": "fx_reveal", "origin_cell": Vector2i.ZERO}, 1, "负路径", 0, {}) if not hits.is_empty(): push_error("[extended_row_violation_test] 违约行被发射——a2 校验失守(回归)") quit(2) return # 对照组:合法扩展行(带 effect_id)照常发射——校验只拦违约形态 - oc._emit_row("extended", "fx_reveal", Vector2i.ZERO, 1, "合法对照", 0, {"effect_id": "fx_reveal"}, "") + oc._emit_row({"type": "extended", "source_class": "fx_reveal", "origin_cell": Vector2i.ZERO}, 1, "合法对照", 0, {"effect_id": "fx_reveal"}) if hits.size() != 1 or String(hits[0].get("type", "")) != "extended": push_error("[extended_row_violation_test] 合法扩展行未发射——校验误拦(回归)") quit(2)