1292 lines
79 KiB
GDScript
1292 lines
79 KiB
GDScript
# 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:授予段尾〔含清图批前〕+ 战斗段尾)。
|
||
# 死亡事件 = 单位容器 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"]
|
||
# 行来源断言豁免集合(来源类别层)——事实来源是单位 / 清图而非棋盘格
|
||
const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward"]
|
||
|
||
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 _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_bank := 0 # 掘进蓄能积点(battle 域——每 charge_n 安全格 +1 点)
|
||
var _attune_safe_seen := 0 # 掘进蓄能 battle 域安全开格计数
|
||
# 特性运行态(迭代 12 T2——relic 持有集经 enc.relics 注入;求值旗注 damage_controller.relic_flags;
|
||
# 魔矢每战首次计数 battle 域。空集=零扰动)
|
||
var _relic_first_dig_used := false
|
||
var _relic_combo := 0 # 矿脉直觉连击计数(battle 域——safe +1/mine 清零)
|
||
var _relic_deep_given := 0 # 深挖钻头本轮已追加份数(battle 域——v2 本格双份制:safe 事件按付费数上限)
|
||
var _relic_paid_at_round_start := 0 # 轮初付费计数快照(v2——本轮付费次数=当前值−快照)
|
||
var _relic_magma_layers := 0 # 熔火之躯积层(battle 域——受地形伤 +1,轮末反打清零)
|
||
|
||
|
||
func _attune_has(item_id: String) -> bool:
|
||
return attunement.has(item_id)
|
||
|
||
|
||
# 装备件表值读取(magnitude/effective_rounds 等——TreasureItemTable 行;未装配不读表)
|
||
func _attune_row(item_id: String) -> Dictionary:
|
||
return db.treasure_item_row(item_id) if _attune_has(item_id) else {}
|
||
|
||
|
||
func _attune_mag(item_id: String) -> int:
|
||
return int(_attune_row(item_id).get("magnitude", 0))
|
||
|
||
|
||
# 特性求值(迭代 12 T2——持有集 × relic_table → damage_controller.relic_flags 注入面):
|
||
# rider_flat = Σ rider 件 value(奥术余烬 +1 / 魔能共鸣 +2);magic 档一(≥2 件)rider +1、
|
||
# 档二(≥4 件)rider 可暴击;clamp 两件直调键;first_dig_spell 旗(魔能共鸣第二效果——魔矢值
|
||
# = 件 effects.value)。空集/空表 = 零旗零扰动。
|
||
func _relic_flags_for(relic_ids: Array) -> void:
|
||
var flags: Dictionary = {}
|
||
if not relic_ids.is_empty() and db.relics != null and not db.relics.rows.is_empty():
|
||
var hooks := RelicsEngine.hooks_for(relic_ids, db.relics)
|
||
var tiers := RelicsEngine.tiers_for(relic_ids, db.relics)
|
||
for h in hooks:
|
||
var hook: String = String(h.get("hook", ""))
|
||
var fx: Dictionary = h.get("effects", {}) as Dictionary
|
||
match hook:
|
||
"hero_attack_rider":
|
||
flags["rider_flat"] = int(flags.get("rider_flat", 0)) + int(fx.get("value", 0))
|
||
"clamp_modifier":
|
||
# clamp 方向按件 note 词分流(魔网=magic-vs-magic 罚修正 / 湛蓝=magic-vs-heavy 加成)
|
||
if String(fx.get("polarity", "")) == "magic_vs_magic":
|
||
flags["clamp_magic_magic"] = int(flags.get("clamp_magic_magic", 0)) + int(fx.get("value", 0))
|
||
elif String(fx.get("polarity", "")) == "magic_vs_heavy":
|
||
flags["clamp_magic_heavy"] = int(flags.get("clamp_magic_heavy", 0)) + int(fx.get("value", 0))
|
||
elif String(fx.get("polarity", "")) == "pierce_vs_light":
|
||
flags["clamp_pierce_light"] = int(flags.get("clamp_pierce_light", 0)) + int(fx.get("value", 0))
|
||
"first_dig_spell":
|
||
flags["first_dig_spell"] = true
|
||
flags["spell_value"] = int(fx.get("value", 0))
|
||
"flag_resource":
|
||
flags["flag_resource_extra"] = int(fx.get("value", 0))
|
||
"nest_aura":
|
||
flags["nest_aura_value"] = int(fx.get("value", 0))
|
||
"flag_all_hit":
|
||
flags["flag_all_hit_mult"] = int(fx.get("value", 0))
|
||
"flag_strike":
|
||
flags["flag_strike_value"] = int(fx.get("value", 0))
|
||
"kill_overflow":
|
||
flags["kill_overflow"] = true
|
||
"dig_count_segments":
|
||
flags["chain_value"] = int(fx.get("value", 0))
|
||
flags["chain_cap"] = int(fx.get("cap", 2))
|
||
"roll_discount":
|
||
flags["discount_shift"] = int(fx.get("value", 0))
|
||
"round_dig_bonus":
|
||
flags["round_dig_value"] = int(fx.get("value", 0))
|
||
flags["round_dig_cap"] = int(fx.get("cap", 5))
|
||
"hit_trade":
|
||
flags["hit_trade_atk"] = int(fx.get("trade_atk", 0))
|
||
flags["hit_trade_hit"] = int(fx.get("trade_hit", 0))
|
||
"armor_roll_rewrite":
|
||
flags["armor_roll_rewrite"] = int(fx.get("value", 0))
|
||
"heavy_debuff":
|
||
flags["crack_value"] = int(fx.get("value", 0))
|
||
"dig_yield_mult":
|
||
# 开格产出乘区族(贪婪矿灯/矿脉直觉/赌徒的血签——正负双向,同 hook 累加)
|
||
flags["dig_yield_pos_mult"] = int(flags.get("dig_yield_pos_mult", 0)) + int(fx.get("mult", 0))
|
||
flags["dig_yield_neg_mult"] = int(flags.get("dig_yield_neg_mult", 0)) + int(fx.get("cost_mult", 0))
|
||
if int(fx.get("cap", 0)) > 0:
|
||
flags["combo_cap"] = int(fx.get("cap", 0)) # 矿脉直觉连击上限
|
||
"deep_dig":
|
||
flags["deep_dig_cost"] = int(fx.get("ap_cost", 0))
|
||
flags["deep_dig_cap"] = int(fx.get("cap", 4))
|
||
"ap_carry":
|
||
flags["ap_carry_value"] = int(fx.get("value", 0))
|
||
"charge_shield":
|
||
flags["shield_per_layer"] = int(fx.get("shield_per_layer", 0))
|
||
"charge_spike":
|
||
flags["spike_mult"] = int(fx.get("value", 0))
|
||
"terrain_refund":
|
||
flags["terrain_refund"] = int(fx.get("value", 0))
|
||
"terrain_dig_double":
|
||
flags["terrain_dig_double"] = int(fx.get("value", 0))
|
||
"flag_neutralize":
|
||
flags["flag_neutralize"] = int(fx.get("value", 0))
|
||
"variant_counter":
|
||
flags["variant_counter_value"] = int(fx.get("value", 0))
|
||
var mt := int(tiers.get("magic", 0))
|
||
if mt >= 1:
|
||
flags["rider_flat"] = int(flags.get("rider_flat", 0)) + 1 # 档一质变(≥2 件):rider +1(provisional)
|
||
if mt >= 2:
|
||
flags["rider_crit"] = true # 档二质变(≥4 件):rider 可暴击(crit 时乘暴击倍率)
|
||
_damage.relic_flags = flags
|
||
# 蓄力流/深挖钻头 action_economy 注入(求值即注入——冒烟直呼 _relic_flags_for 同面生效)
|
||
action_economy.relic_ap_carry = int(flags.get("ap_carry_value", 0)) > 0
|
||
action_economy.relic_dig_extra = int(flags.get("deep_dig_cost", 0))
|
||
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
|
||
_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 类行协议 + 来源断言不外泄)
|
||
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 即确定值)。
|
||
var erow_pre: Dictionary = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth))
|
||
var roll_lo: int = int(erow_pre.get("unit_count", 1))
|
||
var roll_hi: int = int(erow_pre.get("unit_count_max", roll_lo))
|
||
var needs_count: bool = roll_hi > roll_lo
|
||
var needs_atk: bool = erow_pre.has("atk_min")
|
||
var rolled := roll_lo
|
||
var rolled_atk := -1
|
||
if needs_count or needs_atk:
|
||
var estream: RandomNumberGenerator = rng.open_stream("encounter")
|
||
if needs_count:
|
||
rolled = roll_lo + estream.randi_range(0, roll_hi - roll_lo)
|
||
if needs_atk:
|
||
rolled_atk = int(erow_pre.atk_min) + estream.randi_range(0, int(erow_pre.atk_max) - int(erow_pre.atk_min))
|
||
# 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), rolled,
|
||
_hero_atk, int(enc.get("hero_max_hp", -1)), rolled_atk):
|
||
_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_bank = 0
|
||
_attune_safe_seen = 0
|
||
_damage.attune_venom = _attune_mag("venom_edge") if _attune_has("venom_edge") else 0
|
||
_reward_economy.luck_bias = _attune_mag("luck_coin") if _attune_has("luck_coin") else 0
|
||
# 特性运行态注入(迭代 12 T2 魔法流——enc.relics 携带持有集;RelicsEngine 求值后注伤害控制器
|
||
# relic_flags;空集/空表=零扰动。魔矢每战首次开格计数同步归零)
|
||
_relic_flags_for((enc.get("relics", []) as Array))
|
||
_relic_first_dig_used = false
|
||
_relic_combo = 0
|
||
_relic_deep_given = 0
|
||
_relic_paid_at_round_start = 0
|
||
_relic_magma_layers = 0
|
||
# 树 rule 键消费(迭代 12 T10——Fork 激活侧 effects;真实接线首期两键:先手压制=战斗首轮
|
||
# AP+1(queue_ap_delta——先手压制语义)/稳健拓荒=敌方攻击减 1(伤害控制器 flags bulwark);
|
||
# 其余 rule 键=stat 面投影不入战斗(T8 表内容 provisional 口径披露))
|
||
var tree_rules: Array = enc.get("tree_rules", []) as Array
|
||
if tree_rules.has("posture_first_strike"):
|
||
action_economy.queue_ap_delta(1)
|
||
_damage.relic_flags["bulwark"] = 1 if tree_rules.has("posture_bulwark") else 0
|
||
# 迭代 10 奖励重构批——跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现
|
||
# 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后
|
||
# 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节)
|
||
var ap_bonus: int = int(enc.get("hero_ap_bonus", 0))
|
||
if ap_bonus > 0:
|
||
action_economy.queue_ap_delta(ap_bonus)
|
||
# 工兵晨令(迭代 11 同调批 T5——前 effective_rounds 轮每轮 +1 AP;首轮在此入队,后续轮在轮始段续队)
|
||
if _attune_has("dawn_order"):
|
||
action_economy.queue_ap_delta(_attune_mag("dawn_order"))
|
||
if not action_economy.notify_round_start():
|
||
_fatal("行动经济 首轮 notify_round_start 被拒")
|
||
return false
|
||
round_index = 1
|
||
_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,进入授予段被资格检查自然跳过。
|
||
if battle_result == "":
|
||
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
|
||
var ri := round_index
|
||
# ① 授予段:结算队列(开格/旗判定/清图)→ 极性路由逐批兑付。
|
||
# 判定基线 = 轮末在场旗(发射面已过滤——被挖开的旗已随挖掘擦除零兑付,插旗不可逆批 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
|
||
_grant_routed(_relic_yield_adjust(snap, true), "positive", "safe_cell", evt.cell, ri, "")
|
||
# 开格流连击/深层产出(迭代 12 T5):矿脉直觉连击 = safe +1(mine 清零在负向分支);
|
||
# 深挖钻头深层产出 = 每 cap 格额外 grant 一份(收益面补偿额外 AP 成本——R4 甲非信息税对价)
|
||
_relic_combo += 1
|
||
# 深挖钻头 v2(2026-10-04 二次修正——用户实测「付 2AP 无可见提升纯挨打」:v1 配额制
|
||
# 4 次付费 8AP 换 1 份随机资源=净负期望+收益埋明细带不可感知):改**本格双份**——
|
||
# 付费挖掘的本格资源直接双份(多花 1AP 买多一份同格产出,账目直白期望微正)。
|
||
# 本轮付费次数 N=deep_paid_count−轮初快照;safe 事件按 N 上限逐格追加第二份(「深挖」)。
|
||
var dd_cost := int(_damage.relic_flags.get("deep_dig_cost", 0))
|
||
if dd_cost > 0:
|
||
var paid_this_round := int(action_economy.deep_paid_count) - _relic_paid_at_round_start
|
||
if paid_this_round > 0 and _relic_deep_given < paid_this_round:
|
||
_relic_deep_given += 1
|
||
var dd_snap = _reward_economy.grant(evt.cell)
|
||
if dd_snap is Dictionary:
|
||
_grant_routed(dd_snap, "positive", "safe_cell", evt.cell, ri, "", "(深挖)")
|
||
# 魔能共鸣·每战首次开安全格追加魔法矢(迭代 12 T2——relic 件「每关首次开格必出魔法矢」
|
||
# 落地为每战首次〔战斗域单事务无跨战状态——「每关」字面改「每战」台账披露待追认〕;
|
||
# board_typed magic 必中,扫雷结算触发点=relic 挂 first_dig_spell 承载红线①)
|
||
if bool(_damage.relic_flags.get("first_dig_spell", false)) and not _relic_first_dig_used:
|
||
_relic_first_dig_used = true
|
||
var spell_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "relic_first_dig_spell",
|
||
"奥术矢", "board", int(_damage.relic_flags.get("spell_value", 0)),
|
||
DamageController.TARGET_FRONT_ALIVE, ri, evt.cell, "magic")
|
||
_damage.submit(spell_t)
|
||
# 掘进蓄能计数(迭代 11 同调批 T6——battle 域安全开格计数,每 charge_n 格积 1 点)
|
||
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:
|
||
_attune_bank += 1
|
||
elif evt.kind == "mine":
|
||
# 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴),
|
||
# 标中不发(旗已随挖掘擦除、不入轮末判定基线)。
|
||
_grant_routed(_relic_yield_adjust(snap, false), "negative", "mine_hit", evt.cell, ri, "")
|
||
_relic_combo = 0 # 矿脉直觉连击清零(踩雷断连——「推理密度直接奖励」的对侧)
|
||
# 地形变异 兑付段(授予段尾——触发枚举 on_open:挖开即伤、chain 通道同口径,轮末结算逐行兑现;
|
||
# 信息型 hide_number 不进结算——呈现层读取,见 board_view 数字遮蔽;雷格恒无变异——布置段排除,
|
||
# 双层叠罚不存在;插旗格直挖照吃变异伤害——地形后果同 mine_hit 口径,非奖励不作废)
|
||
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)——无类型必中,下限保底在控制器管线内
|
||
var vt := _damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)), "board", \
|
||
int(vrow.get("effect_value", 0)), DamageController.TARGET_HERO, ri, evt.cell, "", 0, "", {"variant_id": String(vid)})
|
||
_damage.submit(vt)
|
||
# 吞噬流四面(迭代 12 T6——地形从纯税到第三资源带;伤害仍发生=乙稿判据 3 承载):
|
||
# ①荆棘编织者=挖开伤害型地形格额外掉落一次格面资源(主动吃地形换资源)
|
||
if int(_damage.relic_flags.get("terrain_dig_double", 0)) > 0:
|
||
var tw_snap = _reward_economy.grant(evt.cell)
|
||
if tw_snap is Dictionary:
|
||
_grant_routed(tw_snap, "positive", "safe_cell", evt.cell, ri, "", "(荆棘编织)")
|
||
# ②地脉采撷=受地形伤掉落小额资源补偿(薄矿化——每次受伤兑一份)
|
||
if int(_damage.relic_flags.get("terrain_refund", 0)) > 0:
|
||
var lh_snap = _reward_economy.grant(evt.cell)
|
||
if lh_snap is Dictionary:
|
||
_grant_routed(lh_snap, "positive", "safe_cell", evt.cell, ri, "", "(地脉补偿)")
|
||
# ③熔火之躯=受地形伤积层(触发键=「受地形伤」通用不按种类特化——定稿红线承载)
|
||
if int(_damage.relic_flags.get("variant_counter_value", 0)) > 0:
|
||
_relic_magma_layers += 1
|
||
for evt in _facts_flags:
|
||
_judge_flag(evt, ri)
|
||
# 战旗军规开单段(迭代 12 T3 标旗流——flag_judged 批后统一开单:轮末每面标中旗对最前存活敌
|
||
# 结算 board_typed 必中伤害〔复用零新配方;「旗所在巢穴敌」简化为 default_target 最前存活敌——
|
||
# 台账披露待追认〕;完美布防=本轮旗批全中(hit>0 且 miss==0)时旗伤 ×mult——全对推理奖励)。
|
||
# 扫雷结算触发点=flag_strike/flag_all_hit/nest_aura 挂点承载红线①。
|
||
if int(_damage.relic_flags.get("flag_strike_value", 0)) > 0:
|
||
var hits := 0
|
||
var misses := 0
|
||
for fevt in _facts_flags:
|
||
if bool(fevt.get("hit", false)):
|
||
hits += 1
|
||
else:
|
||
misses += 1
|
||
if hits > 0:
|
||
var mult := int(_damage.relic_flags.get("flag_all_hit_mult", 1))
|
||
var strike_value := int(_damage.relic_flags.get("flag_strike_value", 0))
|
||
if misses == 0 and mult > 1:
|
||
strike_value *= mult # 完美布防:同轮全中旗伤倍增(全对推理奖励)
|
||
for fevt in _facts_flags:
|
||
if not bool(fevt.get("hit", false)):
|
||
continue
|
||
var st_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "relic_flag_strike",
|
||
"战旗军规", "board", strike_value, DamageController.TARGET_FRONT_ALIVE, ri,
|
||
fevt.cell, "normal")
|
||
_damage.submit(st_t)
|
||
# 熔火之躯轮末反打(迭代 12 T6 吞噬流——受地形伤积层,轮末每层对全体敌 ~1 必中反伤;
|
||
# 触发键=「受地形伤」通用不按地形种类特化——定稿红线承载;反打后清零)
|
||
var magma := int(_damage.relic_flags.get("variant_counter_value", 0))
|
||
if magma > 0 and _relic_magma_layers > 0:
|
||
var mg_t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "relic_magma_body",
|
||
"熔火反噬 ×%d" % _relic_magma_layers, "board", magma * _relic_magma_layers,
|
||
DamageController.TARGET_ALL_ALIVE, ri, minesweeper.hero_pos(), "", 0, "", {})
|
||
_damage.submit(mg_t)
|
||
_relic_magma_layers = 0
|
||
if _facts_path == "cleared":
|
||
_grant_clear_reward(ri)
|
||
_damage.flush() # 授予段界结账(B 模式——段内开单攒批到此统一兑现;A 模式 no-op;清图批前另有同步结账点)
|
||
# ② 增益/减益段(续存实例组合行;本结算新授予的已在授予段逐 grant 出行——增益状态 义务⑦)
|
||
_emit_composition_effects("hero", ri)
|
||
_emit_composition_effects("enemy", ri)
|
||
# ③ 战斗段(调用序:掷点判定 → apply_damage → commit_action;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()
|
||
# 深挖钻头 v2 轮初快照滚动(本轮付费次数已消费——下一轮 N=当前值−新快照)
|
||
_relic_paid_at_round_start = int(action_economy.deep_paid_count)
|
||
_settling = false
|
||
|
||
|
||
# ---- 授予段 helpers(极性路由)----
|
||
|
||
# 开格产出乘区(迭代 12 T5 开格流——贪婪矿灯/矿脉直觉/赌徒的血签;duplicate 隔离不回写快照):
|
||
# positive=power ×(1+静态 mult% + 矿脉直觉连击加成 min(cap,combo)×10%);negative=power ×(1+cost%)。
|
||
# 零旗=原快照原样返回(零扰动)。
|
||
func _relic_yield_adjust(snap: Dictionary, positive: bool) -> Dictionary:
|
||
var pos_m := int(_damage.relic_flags.get("dig_yield_pos_mult", 0))
|
||
var neg_m := int(_damage.relic_flags.get("dig_yield_neg_mult", 0))
|
||
if positive and pos_m <= 0 and not _damage.relic_flags.has("combo_cap"):
|
||
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 _damage.relic_flags.has("combo_cap"):
|
||
pct += mini(int(_damage.relic_flags.get("combo_cap", 5)), _relic_combo) * 10
|
||
out["power"] = maxi(1, int(round(float(power) * (1.0 + float(pct) / 100.0))))
|
||
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 # ② 拒绝零消耗
|
||
# 掘进蓄能受理闸(迭代 11 同调批 T6——bank>0 才可发起,一行一点;积点不足拒绝零消耗)
|
||
if String(v.item_id) == "dig_charge" and _attune_has("dig_charge"):
|
||
if _attune_bank <= 0:
|
||
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]}
|
||
_attune_bank -= 1
|
||
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 = "") -> 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——底稿 §3:挖开雷格该次惩罚减半 + 反弹 N;duplicate 隔离——减半
|
||
# 不回写极性字典;仅 source_class=mine_hit 受理,标错旗/负向字典行走原通道)
|
||
if polarity == "negative" and source_class == "mine_hit" and _attune_has("nest_counter"):
|
||
row = row.duplicate()
|
||
var half: int = int(ceil(float(int(row.power)) / 2.0))
|
||
_emit_row("effect", source_class, cell, ri, "惊巢反制——挖雷惩罚减半(%d→%d)" % [int(row.power), half], 0, {"effect_id": "nest_counter"}, highlight)
|
||
row.power = half
|
||
var reflect: int = _attune_mag("nest_counter")
|
||
if reflect > 0:
|
||
var nct := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_counter", "惊巢反制·反弹", "board",
|
||
reflect, DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, highlight, {})
|
||
_submit(nct, false)
|
||
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 _damage.outcome(_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 _damage.outcome(_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:
|
||
return _damage.flush_submit(t) if sync else _damage.submit(t)
|
||
|
||
|
||
# 主角直伤单据(反噬/治疗包负向——无类型必中,下限保底在控制器管线内)
|
||
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 _damage.outcome(_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
|
||
# 标中殉爆(迭代 11 同调批 T5——底稿 §3:每标中一面旗,全体存活敌吃 N 群伤;无类型走 blast 通道)
|
||
if _attune_has("flag_burst"):
|
||
var fbt := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_flag_burst", "标中殉爆", "board",
|
||
_attune_mag("flag_burst"), DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, "highlight", {})
|
||
_submit(fbt, false)
|
||
# 净化工兵(迭代 12 T6 吞噬流——标中旗中和相邻伤害型地形格):🔴 R4 甲永不对冲边界=
|
||
# **仅限伤害型**(thorn/magma/rockfall——variation 表 effect=flat_damage 判据),信息型
|
||
# (hide_number 族)永不清除(红线①看门税不可对冲);中和=变普通格(BoardData.clear_variants)。
|
||
if int(_damage.relic_flags.get("flag_neutralize", 0)) > 0:
|
||
var board: BoardData = minesweeper.current_board()
|
||
for n in board.neighbors8(cell):
|
||
var vids_n: Array = board.variant_ids(n)
|
||
var harmful := false
|
||
for vid_n in vids_n:
|
||
var vrow_n: Dictionary = db.variation.row_by_id(String(vid_n))
|
||
if String(vrow_n.get("effect", "")) == "flat_damage":
|
||
harmful = true
|
||
break
|
||
if harmful:
|
||
board.clear_variants(n)
|
||
_emit_row("terrain_neutralized", "relic_purge_sapper", n, ri, "净化工兵·地形中和", 0, {}, "")
|
||
# 雷区图谱(迭代 12 T3 标旗流——标中雷额外获得一份格面资源:插旗开始产出对标开格;
|
||
# 复用 grant 单点,额外兑付走同一 _grant_routed 正向路由)
|
||
if int(_damage.relic_flags.get("flag_resource_extra", 0)) > 0:
|
||
var extra_snap = _reward_economy.grant(cell)
|
||
if extra_snap is Dictionary:
|
||
_grant_routed(extra_snap, "positive", "flag_hit", cell, ri, "", "(雷区图谱)")
|
||
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
|
||
var hl := ""
|
||
if _perfect_hit_false == 1:
|
||
hl = "perfect_broken" # 本板完美谓词已破的归因锚
|
||
_emit_row("flag_miss", "flag_miss", cell, ri, "标旗未中", 0, {}, hl)
|
||
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")
|
||
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:
|
||
# 本轮扫雷事实推导(迭代 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.relic_flags["dig_streak"] = 0 if round_mine else round_safe
|
||
# 蓄力层注入(迭代 12 T5——整备军备「每层下轮首段 +1」/厚积薄发满层 spike/铁壁整备减伤——
|
||
# 三件同读 action_economy.charge_layers;battle 域每轮刷新)
|
||
_damage.relic_flags["charge_layers"] = action_economy.charge_layers
|
||
# ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动;授予段负向直伤致死 → 致电短路不先手)。
|
||
# 单分量伤害构成(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)
|
||
# 连珠矢(迭代 12 T4 穿刺流——开格节奏变攻击段数):本轮每开 ~3 安全格 +1 段穿刺附加
|
||
# (上限 cap;段值 provisional 固定值不乘 atk——台账披露)。扫描雷结算触发点=dig_count_segments。
|
||
var chain_value := int(_damage.relic_flags.get("chain_value", 0))
|
||
if chain_value > 0:
|
||
var segments := mini(int(_damage.relic_flags.get("chain_cap", 2)), int(_damage.relic_flags.get("dig_streak", 0)) / 3)
|
||
for s in segments:
|
||
var ct := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "relic_chain_arrows",
|
||
"连珠矢", "board", chain_value, 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)
|
||
_damage.flush() # 战斗段界结账(B 模式——段内开单攒批到此统一兑现;A 模式 no-op)
|
||
|
||
|
||
# ---- 算术读数(伤害管线单据化批 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
|
||
# 工兵晨令续队(迭代 11 同调批 T5——upcoming 轮号 = round_index+1 ≤ effective_rounds 才续,P4 甲表值化)
|
||
if _attune_has("dawn_order") and round_index + 1 <= int(db.treasure_item_row("dawn_order").get("effective_rounds", 0)):
|
||
action_economy.queue_ap_delta(_attune_mag("dawn_order"))
|
||
action_economy.notify_settlement_done(mode)
|
||
action_economy.notify_round_start()
|
||
_reset_round_facts()
|
||
_refresh_used_this_round = 0
|
||
round_index += 1
|
||
_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
|
||
# 映射随战斗结束退役(地图战斗资源 义务④——下一场战斗 = 新板新 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)))
|
||
return roll + db.shop_level_gp_bonus(int(encounter.get("level_index", 0))) + 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)
|