801 lines
45 KiB
GDScript
801 lines
45 KiB
GDScript
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||
## 战斗结算编排中枢:轮次生命周期唯一 owner,五段结算(授予→增益/减益→战斗→衰减→判定)与明细行协议发射(垂直切片验证:一场完整战斗逐行归因)。
|
||
## 效果引擎施加面归 BattleFxHost、奖励授予管线归 RewardGrant(两拆件,经组件出口 fx_host()/
|
||
## reward_grant() 取件,禁链式穿透其内部);本类保留宿主职责面=状态机与生命周期、轮 facts 缓冲、
|
||
## A2 三事件订阅、五段驱动、attune 族、行发射基建(_emit_row/_assert_row_source/_fatal)、呈现投影、刷新面。
|
||
## 零 await;随机只经 rng 封闭流(_facts.encounter/behavior_roll/armor_roll/gp_drop)——同种子同输入逐位复现。
|
||
## 呈现层读取唯一出口 = presentation_snapshot()/diagnostic_snapshot(),禁链式穿透内部协作对象。
|
||
## 伤害只向 DamageController 开单(何时开单归编排、怎么算归控制器);刷新唯一通道 player_request_refresh,禁 RAP 满自动刷。
|
||
class_name BattleOrchestrator
|
||
extends RefCounted
|
||
|
||
signal step_row(row: Dictionary)
|
||
signal battle_finished(payload: Dictionary) ## {result, hero_hp, gp_dropped}——hero 在 despawn 前捕获(呈现层销毁后读状态的时序防护);gp_dropped = 战斗掉落合计
|
||
signal round_started(round_index: int)
|
||
signal board_changed()
|
||
signal settlement_snapshot(snap: Dictionary) ## 每结算末尾的全量状态快照(playtest_logger 消费——JSONL 数值分析)
|
||
|
||
# 明细流水行型协议(战斗结算 Core Rule;两层词表——基础 9 类封闭+扩展行 1 类;
|
||
# 清单改动须同步 battle_log_view 的 _format_row 与本文档头注):
|
||
# 基础 9 类:效果行 effect · 伤害行 damage · 治疗行 heal ·
|
||
# 奖励未生效行 reward_ineffective · 封印未中行 seal_miss · 施放行 cast · 衰减行 decay ·
|
||
# 判定行 verdict · 高光行 highlight
|
||
# 扩展行:行型值=extended(单一扩展行型),效果身份由 effect_id 载荷携带(_emit_row 强制必载,
|
||
# 缺载即断言失败零发射)。现役效果身份与发射点:
|
||
# fx_reveal(专长揭示掷格——battle_fx_host flush_pending_reveals 三发射点:兑付行+落空行×2)
|
||
# mine_neutralized(封印匠·危险格中立移除——grant_segment 封印命中 remove_mines 面)
|
||
# fx_preview_area(示警烟火·周围资源类型——grant_segment 封印命中 preview_area 面)
|
||
# insurance_used(完美保险·标错豁免——grant_segment 标错 forgive_seal_miss 面)
|
||
# 新增专长效果只新增 effect_id 身份不再新增行型;行型词表改动须同步 owner 文档「扩展行」节。
|
||
# 行来源断言豁免集合(行型层)——判定段 / 衰减段本就无棋盘来源,勿一刀切误炸
|
||
const SOURCE_EXEMPT_TYPES := ["decay", "verdict"]
|
||
# 行来源断言豁免集合(来源类别层)——事实来源是单位 / 清图而非棋盘格;
|
||
# fx_reveal=专长揭示掷格(落空行无真实格);
|
||
# fx_grant_gp=专长·GP 入账(钱包面而非棋盘面,行无格子可指);
|
||
# fx_board_refresh=成功刷新事实族(迭代 46 卡A——崩壁冲击群伤行源=换图动作而非棋盘格)
|
||
const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "fx_reveal", "fx_grant_gp", "fx_board_refresh"]
|
||
|
||
var _facts: BattleFacts # 战斗事实件(battle_facts.gd——事实族 15 成员单点)
|
||
var _rounds: RoundCycle # 轮循环驱动件(settlement/round_cycle.gd——五段持有/段序/重入闸/摇号)
|
||
var db: ConfigDb
|
||
var rng: RngServiceCore
|
||
# E1 封装收口:协作对象 enemy_system/status_effects/reward_economy 私有化——跨层读取一律经 presentation_snapshot()/diagnostic_snapshot() 成文出口。
|
||
# minesweeper/action_economy 保持公有 = 架构文档 API Boundaries 的 HUD框架消费面(意图提交 settle_batch + compute_move_path
|
||
# 只读;ap/rap/相位读取)——非本文档登记的契约面,勿增新用途。
|
||
# 装板域(BoardGenerator+装板六函数族)已拆 BoardInstaller——生成器实例随件自建,宿主零成员。
|
||
var minesweeper: MinesweeperRules
|
||
var action_economy: ActionEconomy
|
||
var _enemy_system: UnitContainer
|
||
var _status_effects: StatusContainer
|
||
var _reward_economy: RewardEconomy
|
||
var _damage: DamageController # 伤害控制器(单据唯一兑付管线)
|
||
var _fx_host: BattleFxHost # 效果引擎宿主件(fx 族持有与兑付——拆件定稿轴件一)
|
||
var _reward_grant: RewardGrant # 奖励授予件(授予管线与秘宝使用——拆件定稿轴件二)
|
||
|
||
var round_index := 0
|
||
var battle_result := "" # "" / "victory" / "defeat" / "fatal_error"
|
||
|
||
# 结算五段 Segment(段边界=审计五段,段序骨架冻结于
|
||
# SettlementTable.segment_order 语义:授予→增益/减益→战斗→衰减→判定。本类做段序编排+
|
||
# 行发射/开单原语单点——段体逻辑归 game/core/settlement/ 五类)
|
||
# 同调装配运行态(begin_battle 经 enc 携带 attunement 键注入 facts;off=空集全部效果零扰动)
|
||
# 转存时点=roll_round_actions 轮末(轮内部分结算不更新 last);开战清零归 begin_battle
|
||
var nest_preview_count := -1 # 巢穴预告读数口(board_installed 命中落值;-1=未装配/未发射;呈现桥消费)
|
||
# 特性运行态(relic 持有集经 enc.feats 注入:效果行为=数据行 triggers 经
|
||
# 效果引擎分发;魔矢每战首次计数 battle 域。空集=零扰动)
|
||
|
||
|
||
# 主角攻击基值派生读(enc 携带 hero_atk 键受理后只读——E4 收口语义不变:表值运行期不可变,
|
||
# 派生值恒等;「读面即引用、无第二账」同款先例)
|
||
func _hero_atk() -> int:
|
||
return int(_facts.encounter.get("hero_atk", int(db.combat.hero.atk)))
|
||
|
||
|
||
# 加算日记读面(begin_battle 经 enc["journal"] 派生——encounter 受理 assign 后全生命期可读;
|
||
# null=调试/单场直构面,账面消费跳过。领域拆分批成员删除后的引用承接,读面即引用、无第二账)
|
||
func _journal() -> GainJournal:
|
||
return _facts.encounter.get("journal", null) if _facts.encounter.get("journal", null) is GainJournal else null
|
||
|
||
|
||
# 同调配装件读面(begin_battle 经 enc["loadout"] 派生——充能单源化的 reset_cycle/consume_charge
|
||
# 消费面;null=直构零装配,充能面跳过)
|
||
func _attune_loadout() -> AttuneLoadout:
|
||
return _facts.encounter.get("loadout", null)
|
||
|
||
|
||
# 授予件战斗域回调束(bind_battle 注入——键集封闭三键:充能消耗/真视计数/负向踩雷计数;
|
||
# 宿主侧各转 _attune_loadout/_journal/_rounds.mines_hits 单点,授予件对账本零强引用)
|
||
func _battle_hooks() -> Dictionary[Variant, Variant]:
|
||
return {
|
||
"attune_consume": _hook_attune_consume,
|
||
"vision_add": _hook_vision_add,
|
||
"negative_blast": _book_negative_blast,
|
||
}
|
||
|
||
|
||
func _hook_attune_consume(item_id: String) -> void:
|
||
if _attune_loadout() != null and _reward_grant.treasure_inventory != null:
|
||
_attune_loadout().consume_charge(item_id, _reward_grant.treasure_inventory)
|
||
|
||
|
||
func _hook_vision_add() -> void:
|
||
if _journal() != null:
|
||
_journal().record_vision_use() # 受理成功恰计一次(未注入=调试直构不计——空账守卫同语义)
|
||
|
||
|
||
# 负向 blast(雷)踩雷计数入账(触发条件事实面——正向 spark_aoe/清图奖励不计入;
|
||
# 极性判定在授予件 _flat_blast 传点,本口只记账)
|
||
func _book_negative_blast() -> void:
|
||
_rounds.mines_hits["round"] += 1
|
||
_rounds.mines_hits["total"] += 1
|
||
|
||
|
||
# 效果引擎宿主侧事实单点(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":
|
||
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(_facts.encounter.get("battle_index", 1)) # 关卡域战斗序(1 基——level_runner enc 携带;开局姿态窗条件面)
|
||
"seals_count":
|
||
var hits_n := 0
|
||
for fevt in _facts.seals:
|
||
if bool(fevt.get("hit", false)):
|
||
hits_n += 1
|
||
return hits_n
|
||
"hp_ratio":
|
||
var hu: Dictionary[Variant, Variant] = _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
|
||
|
||
|
||
# attune 积点读投影(RewardGrant attune_safe_seen_cb 注入面——use_treasure 拒绝文案读数;经读投影出户防组件回调宿主私有族)
|
||
func _attune_safe_seen_read() -> int:
|
||
return _facts.attune_safe_seen
|
||
|
||
|
||
# 组件出口(拆件后取件面——呈现与测试经此取件,禁链式穿透协作对象内部)
|
||
func fx_host() -> BattleFxHost:
|
||
return _fx_host
|
||
|
||
|
||
func reward_grant() -> RewardGrant:
|
||
return _reward_grant
|
||
|
||
|
||
func _attune_has(item_id: String) -> bool:
|
||
return _facts.attunement.has(item_id)
|
||
|
||
|
||
# 装配集只读出口(_attune_has 契约化出口,save v6 只读访问器先例。只读零副作用——
|
||
# 呈现侧判定专用,非结算写入口)
|
||
func attune_has(item_id: String) -> bool:
|
||
return _attune_has(item_id)
|
||
|
||
|
||
# 装备件表值读取(charge_n 等——TreasureItemTable 行;未装配不读表。
|
||
# 行值经 value_from 在 rebuild 定影,零运行期按件取值)
|
||
# 装配集参数合计(常时参数面 kind 的装配集 Σmagnitude 读数口;
|
||
# kind 分发查询先例=_attune_refresh_x_total bonus_refresh。零装配=0)
|
||
func attune_param_sum(kind: String) -> int:
|
||
var total := 0
|
||
for iid_v in _facts.attunement:
|
||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v))
|
||
if (arow.get("effect_kinds", []) as Array).has(kind):
|
||
total += int(arow.get("magnitude", 0))
|
||
return total
|
||
|
||
|
||
# 装配集内 ap_charge_safe 行读取(取数源=装配集行扫描;
|
||
# 首行语义=装配集内至多一件积点制〔试炼镐〕)
|
||
func _attune_charge_safe_row() -> Dictionary:
|
||
for iid_v in _facts.attunement:
|
||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v))
|
||
if (arow.get("effect_kinds", []) as Array).has("ap_charge_safe"):
|
||
return arow
|
||
return {}
|
||
|
||
|
||
# 充能重置(按周期回满:账本回 charges_per_adventure+骑库存差额补注入
|
||
# (used handle 不回收=本轮已耗充能作废,补新 handle=「回满」;差额注入防双份=level_runner
|
||
# set_attune_charges 同构)。reset ∈ {battle, level}——adventure 周期归冒险开始种子面)
|
||
func _reseed_attune_charges(reset: String) -> void:
|
||
if _attune_loadout() == null or _reward_grant.treasure_inventory == null:
|
||
return # 直构零装配/库存未就位——充能回满跳过(空装配同语义)
|
||
_attune_loadout().reset_cycle(reset, _reward_grant.treasure_inventory) # 写时种行/补行(按周期筛——语义逐字承原循环体)
|
||
|
||
|
||
# 积点制件进度读数(自动兑付无玩家发起入口,读数口=C 区秘宝槽悬停浮窗进度行:
|
||
# assembled=装配在册/points=本战安全格进度(charge_n 取余)/need=charge_n/item_id=装配行 id。
|
||
# 未装配=assembled false)
|
||
func attune_convert_state() -> Dictionary:
|
||
var row := _attune_charge_safe_row()
|
||
if row.is_empty() or String(row.get("form", "")) != "active_charged":
|
||
return {"assembled": false, "points": 0, "need": 0, "display_name": "", "item_id": ""}
|
||
var cn := int(row.get("charge_n", 0))
|
||
return {"assembled": true, "points": _facts.attune_safe_seen % cn if cn > 0 else 0, "need": cn,
|
||
"display_name": String(row.get("display_name", "")), "item_id": String(row.get("item_id", ""))}
|
||
|
||
|
||
# 特性求值(效果行为=数据行 triggers 订阅,效果引擎 dispatch 分发;
|
||
# 流派档位质变=FeatsEngine 纯函数求值
|
||
# (档位机制本身系引擎基建非内容效果),两运行态注伤害控制器:档一 rider+1 / 档二 rider 可暴击)
|
||
|
||
|
||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||
db = db_ref
|
||
rng = rng_ref
|
||
action_economy = ActionEconomy.new(db)
|
||
minesweeper = MinesweeperRules.new(db, action_economy)
|
||
_enemy_system = UnitContainer.new(db)
|
||
_status_effects = StatusContainer.new(db)
|
||
_facts = BattleFacts.new() # 战斗事实件(受理事实/轮缓冲/封印账/溯源/条件计数——成员迁移见设计 §4)
|
||
_reward_economy = RewardEconomy.new(db, rng)
|
||
_reward_economy.fatal_handler = _fatal # 协作件违约→本类 _fatal 封印(单点接线)
|
||
_damage = DamageController.new(db, rng, _enemy_system, _status_effects)
|
||
_damage.emit_row_cb = _emit_row # 行协议归属本类(9 类行协议 + 来源断言不外泄)
|
||
_fx_host = BattleFxHost.new({"db": db, "rng": rng, "minesweeper": minesweeper, "action_economy": action_economy,
|
||
"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 件对授予面走注入回调)
|
||
# 业务类发射面接线归 host._init 尾段组装期完成(效果端口件 bind_port 转注——先于 begin_battle 任何发射)
|
||
_reward_economy.roller.luck_bias_query = _fx_host.luck_bias_for # 偏运查询口注入(掷格件缝拆形态随件迁移——reward_roll 发射归 fx 宿主)
|
||
minesweeper.cell_revealed.connect(_on_cell_revealed)
|
||
minesweeper.seal_judged.connect(_on_seal_judged)
|
||
minesweeper.round_end_board.connect(_on_round_end)
|
||
action_economy.round_end_stopped.connect(_on_round_end)
|
||
# 结算五段接线(授予/战斗两段持三引用=宿主+两协作件;信号连接纪律不涉段〔段零信号连接〕)
|
||
# 结算五段接线随轮循环驱动件迁驻(round_cycle.gd——段件持有/段序/重入闸/摇号单点;构造序禁环不变)
|
||
_rounds = RoundCycle.new(self, _fx_host, _reward_grant)
|
||
|
||
|
||
# ---- 战斗开始序列(编排专章①:门 → 增益状态 → 敌人系统 → 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_and_reset():
|
||
return false
|
||
_facts.reset_round() # 首板 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
|
||
_rounds.roll_round_actions() # 开轮摇号(首轮同样摇;先于 round_started,挖掘期动画位可读)
|
||
round_started.emit(round_index)
|
||
return true
|
||
|
||
|
||
# 受理段:_facts.encounter 装载+缓冲清零+护甲合流+重载闸+开域门+遭遇骰与敌人系统 spawn 受理门。
|
||
func _begin_battle_accept(enc: Dictionary) -> bool:
|
||
_facts.encounter.assign(enc)
|
||
_reward_grant.reset_battle() # 战斗域零 GP 持久(每场战斗缓冲从零起算;案C 闸复位)
|
||
_damage.set_hero_ac_override(int(enc.get("hero_armor_class", -1))) # 主角护甲合流入参(伤害控制器 defender_ac 消费)
|
||
db.set_reload_gate(true)
|
||
if not _status_effects.notify_battle_start():
|
||
_fatal("增益状态 开域被拒(编排违例)")
|
||
return false
|
||
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
|
||
# E4 收口:加成经 _facts.encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径;
|
||
# _hero_atk 派生读=受理后不变量,无快照成员)
|
||
if not _enemy_system.spawn({"level_index": int(enc.level_index), "node_depth": int(enc.node_depth), "hero_hp": int(enc.hero_hp), "unit_count_overrides": dice[0], "hero_atk_override": _hero_atk(), "hero_max_hp_override": int(enc.get("hero_max_hp", -1)), "enemy_atk_overrides": dice[1], "hero_attack_type": String(enc.get("attack_type", "")), "hero_armor_type": String(enc.get("armor_type", "")), "row_kind": 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 期整战持久值
|
||
# _facts.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.combat.enemy.resolve_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:
|
||
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 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._facts.attunement 携带;off=空集零扰动。淬毒 N 注入
|
||
# 伤害控制器、偏运 N 注入地图战斗资源——均须先于首板 install)
|
||
_facts.attunement = (enc.get("attunement", []) as Array).duplicate()
|
||
_facts.attune_safe_seen = 0
|
||
_fx_host.reset_battle_state(int(enc.get("hero_armor_class", -1)))
|
||
_rounds.mines_hits = {"total": 0, "last": 0, "round": 0} # 踩雷计数开战清零(事实面生命周期同款)
|
||
var adv_bucket: Variant = enc.get("adv_bucket", null)
|
||
_fx_host.mount_buckets(adv_bucket if adv_bucket is Dictionary else {}, enc.get("level_counters")) # 冒险桶宿主字典(直构兜底自有空字典);关卡桶 LevelRunner 每关 new 下传、非 Dictionary 即不挂
|
||
_reward_grant.bind_battle(_battle_hooks()) # 授予件战斗域回调束(充能消耗/真视计数/踩雷计数)
|
||
var feat_ids: Array[Variant] = enc.get("feats", []) as Array
|
||
# 流派档位质变不设(奇物.md v1「不设流派标签、
|
||
# 不设档位机制」):rider_tier 两运行态复位恒零,散件单体效果=唯一形态
|
||
_fx_host.facts_inject["rider_tier_bonus"] = 0
|
||
_fx_host.facts_inject["rider_tier_crit"] = false
|
||
_facts.feat_first_dig_used = false
|
||
# 树 rule 键消费(Fork 激活侧效果=效果引擎合成
|
||
# 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1)。其余 rule 键
|
||
# =stat 面投影不入战斗(T8 表内容 provisional 口径披露))
|
||
var tree_rules: Array[Variant] = enc.get("tree_rules", []) as Array
|
||
# 效果引擎订阅重建(装载序=feats 表序→treasure 表序→树规则,写死保种子复放——细则见 battle_fx_host.rebuild_subscriptions)
|
||
_fx_host.rebuild_subscriptions(feat_ids, _facts.attunement, tree_rules)
|
||
|
||
|
||
# 秘宝常时参数注入(装配集 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
|
||
_reseed_attune_charges("battle")
|
||
if int(enc.get("battle_index", 1)) == 1:
|
||
_reseed_attune_charges("level")
|
||
|
||
|
||
# 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()
|
||
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)
|
||
var voucher_rap: int = int(enc.get("voucher_rap", 0))
|
||
if voucher_rap > 0:
|
||
action_economy.grant_reserve_ap(voucher_rap)
|
||
|
||
|
||
# ---- 装板受理薄壳(BoardInstaller 拆件后的宿主承接面——begin_battle/中途刷新/轮内刷新三调用点唯一入口)----
|
||
# 件内装板(生成→全局变异决议→装板受理→资源摇号→变异布置)成功后执行宿主态复位:五处计数清零+
|
||
# 板序推进+board_changed 发射+秘宝叠加层退役——时序逐字;复位职责留宿主(成员单点),不随件。
|
||
func _install_and_reset(during_round := false) -> bool:
|
||
var installer := BoardInstaller.new({"db": db, "rng": rng, "minesweeper": minesweeper,
|
||
"reward_economy": _reward_economy, "encounter": _facts.encounter, "fatal_cb": _fatal})
|
||
if not installer.install(during_round):
|
||
return false
|
||
_facts.seal_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8)
|
||
_facts.seal_miss_seen.clear()
|
||
_facts.perfect_hit_true = 0 # 整图完美计数随 install 重置
|
||
_facts.perfect_hit_false = 0
|
||
_facts.perfect_forgiven = 0 # 豁免累计随 install 重置(domain=battle 每战清零)
|
||
_facts.board_from_midround = during_round # 轮内刷新装板 = 刷新图(清图奖励不发);常规换图/开战装板 = false
|
||
_facts.board_seq += 1
|
||
board_changed.emit()
|
||
if _reward_grant.treasure_inventory != null:
|
||
_reward_grant.treasure_inventory.retire_overlays() # 叠加层随图消亡(换图/刷新图装板点;已花道具不返还)
|
||
return true
|
||
|
||
|
||
# ---- 事件缓冲(轮 facts 收集;结算触发 = Waiting 相位编排点,不依赖到达序)----
|
||
|
||
func _on_cell_revealed(evt: Dictionary) -> void:
|
||
# 轮 facts 缓冲不设相位守卫——轮末最后一挖的揭示事件在 request_dig 翻相之后才到(事件序:
|
||
# cell_revealed 批 → seal_judged 批 → round_end),phase==Active 守卫会丢最后一挖的事实;
|
||
# initial 通道事件(install 初始展开——封锁格)resource=false,进入授予段被资格检查自然跳过。
|
||
# fx_reveal 通道事件旁路缓冲(专长揭示的奖励由 fx_host flush_pending_reveals 兑付点
|
||
# 直接入账,此处缓冲会导致二次兑付)。
|
||
if battle_result == "" and not _fx_host.reveal_busy():
|
||
# 积点自动兑付:
|
||
# 满 charge_n 直接当轮 grant_ap,无玩家发起入口、无积点账中转〔账即时清空,battle 域重置
|
||
# 自然无感〕。落位=揭示缓冲点——事件序 cell_revealed 批先于本格扣费,AP 归零恰逢积点满时
|
||
# 先兑再扣,轮次不因本格结束;授予段已无积点段(本段唯一计数点)。fx_reveal 旁路不计数=
|
||
# 缓冲路径口径〔天眼开格不计积点〕。grant_ap 相位防御在 ACTIVE 挖格链内恒过)
|
||
if String(evt.get("kind", "")) == "safe":
|
||
var cs_row: Dictionary[Variant, Variant] = _attune_charge_safe_row()
|
||
if not cs_row.is_empty():
|
||
_facts.attune_safe_seen += 1
|
||
var cn_b: int = int(cs_row.get("charge_n", 999))
|
||
if cn_b > 0 and _facts.attune_safe_seen % cn_b == 0:
|
||
action_economy.grant_ap(int(cs_row.get("magnitude", 1)))
|
||
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_seal_judged(evt: Dictionary) -> void:
|
||
_facts.seals.append(evt)
|
||
|
||
|
||
func _on_round_end(_payload: Dictionary) -> void:
|
||
_facts.path = String(_payload.get("path_tag", ""))
|
||
_rounds.settle_if_due()
|
||
|
||
|
||
|
||
|
||
func _feat_yield_adjust(snap: Dictionary, positive: bool, pos_m: int, neg_m: int) -> Dictionary:
|
||
if positive and pos_m <= 0:
|
||
return snap
|
||
if not positive and neg_m <= 0:
|
||
return snap
|
||
var out: Dictionary[Variant, Variant] = 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
|
||
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[Variant] = []
|
||
for vid_v in vids:
|
||
var vid := String(vid_v)
|
||
var vrow: Dictionary[Variant, Variant] = db.combat.variation.row_by_id(vid)
|
||
if String(vrow.get("effect", "")) == "flat_damage":
|
||
out.append(vid)
|
||
return out
|
||
|
||
|
||
# 邻接伤害型变异收集(terrain_kind fact 取数面——seal_hit 发射点用:封印命中相邻八格的伤害型变异集;
|
||
# 🔴 R4 甲永不对冲边界=仅限伤害型,信息型〔hide_number 族〕不入集)
|
||
func _harmful_adjacent(cell: Vector2i) -> Array:
|
||
var out: Array[Variant] = []
|
||
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[Variant] = []
|
||
for h in hits:
|
||
if String((h as Dictionary).get("kind", "")) == kind:
|
||
out.append(h)
|
||
return out
|
||
|
||
|
||
# aux 解锁门控转发(LevelRunner 注入;E1/E3 封装:外部经公有面,禁直读 _reward_economy)
|
||
func set_treasure_unlocked(ids: Dictionary) -> void:
|
||
_reward_economy.roller.set_treasure_unlocked(ids)
|
||
|
||
|
||
# ---- 开轮摇号(共识点 B:技能选择前置到轮次开始,数值计算留结算段)----
|
||
# 每实例:保底判定(连续执行普攻 ≥ M → 必出特殊技、特殊技内按权重、不受冷却限制)→ 排除冷却中技能
|
||
# (中签率 = 权重 ÷ 可摇权重合计——份额归一)→ 按行序逐敌从统一流顺序取号摇定本拍动作。
|
||
# 随机消费只在换算层(本方法 + SkillLottery);单位容器收确定值(零随机纪律——armor_roll 先例同构)。
|
||
# 统一流:behavior_roll 无子流派生向量(位置交错,单点定位由事件日志承载)。
|
||
# 摇定后、兑现前死亡 → 本拍技能作废。
|
||
|
||
func _defender_ac(unit: Dictionary) -> int:
|
||
return _damage.defender_ac(unit)
|
||
|
||
|
||
func _pool_effective(effect_id: String) -> int:
|
||
var side := "hero"
|
||
var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(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({"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 数值分析数据面——每次结算全量状态;运行时零决策读取)----
|
||
|
||
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": _facts.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["seals"] = minesweeper.seal_count()
|
||
settlement_snapshot.emit({
|
||
"kind": "settlement",
|
||
"level": int(_facts.encounter.get("level_index", 0)),
|
||
"depth": int(_facts.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「空场」
|
||
"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": _facts.refresh_used},
|
||
"board": board_stats,
|
||
"enemies": enemies,
|
||
"buffs": buffs,
|
||
"round_facts": {"path": _facts.path, "safe": safe_n, "mines": mines_hit, "seal_events": _facts.seals.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_and_reset():
|
||
_fatal("换图编排失败")
|
||
return
|
||
# 工兵晨令续队直调拆除(内容数据驱动扩展批 T4——round_start 订阅行承载,见 begin_battle 注)
|
||
action_economy.notify_settlement_done(mode)
|
||
# round_start 发射先于行动经济轮切换(queue 型 grant_ap 当轮入队当轮兑现——与首轮同口径)
|
||
round_index += 1
|
||
_fx_host.emit_round_start()
|
||
action_economy.notify_round_start()
|
||
_facts.reset_round()
|
||
_facts.refresh_used = 0
|
||
_rounds.roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道)
|
||
round_started.emit(round_index)
|
||
|
||
|
||
# 呈现层查询面(HUD 刷新按钮可用性)——挖掘期唯一通道(折价成本同源:判据归 BattleRefreshFlow)
|
||
func can_refresh() -> bool:
|
||
return BattleRefreshFlow.can_refresh(self)
|
||
|
||
|
||
# 玩家经 HUD框架发起、战斗结算 代理调用(禁止 RAP 满自动刷)。
|
||
# 挖掘期唯一通道:Active 相位立即换图继续挖(RAP 按折后价扣;AP 保留、相位不动、不触发结算);
|
||
# 非挖掘期请求一律拒绝——非清图轮恒同图。刷新门/折价扣费/换图/成功事实归 BattleRefreshFlow
|
||
# (迭代 46 卡A T3 拆出——编排器入口与唯一通道不变,C10 结构预算只降不升)。
|
||
func player_request_refresh() -> bool:
|
||
return BattleRefreshFlow.execute(self)
|
||
|
||
|
||
func _battle_over(result: String) -> void:
|
||
battle_result = result
|
||
# battle_won 发射+施加(战地包扎 heal 订阅面;败北不发射。heal=英雄单位回血
|
||
# 〔封顶单位上限〕,战后生命经 hero_hp_left 回写冒险账本——时点在捕获前=回血计入带入下一层)
|
||
if result == "victory":
|
||
for h in _fx_host.fx_emit("battle_won", {"round": round_index}):
|
||
if String(h.get("kind", "")) == "heal":
|
||
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 战末汇入(血税双向账:本战累积经加算日记单写原语汇入获得日记三层后清零)
|
||
var drained := _fx_host.bonus_delta_drain()
|
||
if _journal() != null:
|
||
for axis in drained.keys():
|
||
_journal().gain_bonus(String(axis), int(drained[axis]), "battle_export", String(axis))
|
||
# 映射随战斗结束退役(地图战斗资源 义务④——下一场战斗 = 新板新 scope 批,roll_board 相位守卫的前提)
|
||
var hero_hp_left := int(_enemy_system.hero_unit().get("hp", 0)) # despawn 前捕获(信号载荷供呈现层"HP 带入下一层")
|
||
var gp_dropped := _roll_gp_drop() # 战斗掉落(escrow 随此值上抛即弃)
|
||
_reward_economy.retire_mapping()
|
||
_enemy_system.despawn_all()
|
||
_status_effects.notify_battle_end()
|
||
if _reward_grant.treasure_inventory != null:
|
||
_reward_grant.treasure_inventory.retire_overlays() # 叠加层随战斗结束清空(库存不动——关卡域跨战斗)
|
||
action_economy.notify_combat_end() # → Idle,RAP 清零
|
||
db.set_reload_gate(false)
|
||
_facts.reset_round()
|
||
battle_finished.emit({"result": result, "hero_hp": hero_hp_left, "gp_dropped": gp_dropped})
|
||
|
||
|
||
# 战斗掉落掷定: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(_facts.encounter.get("node_type", "")) == "boss" else "normal"
|
||
var drops: Dictionary[Variant, Variant] = db.run.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.run.shop.level_gp_bonus_at(int(_facts.encounter.get("level_index", 0)))
|
||
# 财富之道·聚(战斗掉落 GP 乘算·入账前:只乘战斗掉落两成分〔掷定+关卡加成〕,
|
||
# 店内 escrow 不乘〔范围=仅战斗掉落渠道〕;本函数返回值=结算框「GP 增量」行
|
||
# 显示源,落在入账前=显示即实得;乘数 provisional 150%)
|
||
if (_facts.encounter.get("tree_rules", []) as Array).has("economy_hoard"):
|
||
drop = int(round(float(drop) * 1.5))
|
||
return drop + _reward_grant.escrow
|
||
|
||
|
||
# ---- 呈现状态面(E1 封装收口):呈现层 / 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}] /
|
||
# (字段面=pending_* 三字段 + 敌方增益 effects 投影;池与权重不公开;警觉走 effects 投影)
|
||
# hero_effects[](增益状态 query_active 投影)
|
||
func presentation_snapshot() -> Dictionary:
|
||
var enemies: Array[Dictionary] = []
|
||
var all: Array[Variant] = _enemy_system.all_enemies()
|
||
# 敌方增益投影(side-wide 状态——蓄力/硬化/削弱/暴走 percent 行 + 警觉 ac 行)
|
||
var enemy_effects: Array[Dictionary] = []
|
||
for eff in _status_effects.query_active("enemy"):
|
||
var erow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(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[Variant, Variant] = 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[Variant, Variant] = db.combat.skill.row_by_id(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[Variant, Variant] = _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(),
|
||
"refresh_used": _facts.refresh_used, # 受理计数入呈现成文面(HUD 余量单源读此处)
|
||
"hero": hero,
|
||
"enemies": enemies,
|
||
"hero_effects": _status_effects.query_active("hero").duplicate(true), # 值拷贝出户(快照消费方直改不渗入状态容器)
|
||
# 整图变异投影(呈现横幅/常驻角标唯一读取面;
|
||
# 空数组 = 无全局型合法态:普通图与全局池空回落图恒空)
|
||
"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) -> Variant:
|
||
return _reward_economy.query_identity(cell)
|
||
|
||
|
||
# 诊断观测面(组合根诊断转储 / 行为探针消费):私有编排状态的成文只读出口——
|
||
# 消费方禁止再直读本类下划线字段(封装收口)
|
||
func diagnostic_snapshot() -> Dictionary:
|
||
return {
|
||
"battle_result": battle_result,
|
||
"round": round_index,
|
||
"settling": _rounds.is_settling(),
|
||
"refresh_used_round": _facts.refresh_used,
|
||
"board_seq": _facts.board_seq,
|
||
"facts_path": _facts.path,
|
||
"facts_reveals": _facts.reveals.size(),
|
||
"facts_seals": _facts.seals.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_seals": minesweeper.seal_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,
|
||
}
|
||
|
||
|
||
# ---- 行协议(Core Rule:公共字段 + 行类型特有字段)----
|
||
|
||
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", "")) == "":
|
||
assert(false, "[战斗结算] 扩展行缺 effect_id 载荷——行协议违约")
|
||
return
|
||
_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)
|
||
|
||
|
||
## 完美封印谓词(两账相减:有效误标=误标总数−豁免数)
|
||
## 契约 2:_facts.perfect_hit_false 保持纯净,豁免只累加 _facts.perfect_forgiven,禁回退式改写与拦截式吞事件。
|
||
func perfect_sweep_earned() -> bool:
|
||
return _facts.perfect_hit_false - _facts.perfect_forgiven <= 0 and _facts.perfect_hit_true >= 1
|