diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index e3e6ea5e..feb73f5a 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -70,6 +70,14 @@ var _facts_path := "" var _settling := false var _defer_board_damage := false # 案C 暂存闸(2026-10-06 用户裁:地图奖励伤害兑付迁战斗段末——怪物还手后) var _deferred_board: Array[Dictionary] = [] # 授予段暂存伤害单据(保授予段事件序;战斗段怪物还手后按序补提交统一结账) +# 结算五段 Segment(迭代 27 T2——P1-1 拆段;段边界=审计五段〔裁点③甲〕,段序骨架冻结于 +# SettlementTable.segment_order 语义:授予→增益/减益→战斗→衰减→判定。本类退为段序编排+ +# 行发射/开单原语单点——段体逻辑归 game/core/settlement/ 五类) +var _grant_seg = null # GrantSegment(①授予段——含旗判与 counter/grant 助手单点) +var _aura_seg = null # AuraSegment(②增益/减益段) +var _combat_seg = null # CombatSegment(③战斗段——原 _combat_segment 体) +var _decay_seg = null # DecaySegment(④衰减段) +var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归本类) var _refresh_used_this_round := 0 # 每轮刷新计数(Active 相位合计,兜底上限 = ActionEconomyTable.refresh_cap_per_round) var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用) var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换来的图 = true(清图奖励守卫——刷新图不发清图奖励) @@ -303,6 +311,12 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: minesweeper.flag_judged.connect(_on_flag_judged) minesweeper.round_end_board.connect(_on_round_end) action_economy.round_end_stopped.connect(_on_round_end) + # 结算五段接线(迭代 27 T2——段持宿主引用;信号连接纪律不涉段〔段零信号连接,ADR-0001 同构〕) + _grant_seg = GrantSegment.new(self) + _aura_seg = AuraSegment.new(self) + _combat_seg = CombatSegment.new(self) + _decay_seg = DecaySegment.new(self) + _verdict_seg = VerdictSegment.new(self) # ---- 战斗开始序列(编排专章①:门 → 增益状态 → 敌人系统 → install+roll → 行动经济)---- @@ -631,254 +645,16 @@ func _maybe_settle() -> void: func _settle() -> void: _settling = true - _defer_board_damage = true # 案C:授予段伤害单据只暂存不结账(2026-10-06 用户裁——兑付时点=战斗段末) var ri := round_index - # ① 授予段:结算队列(开格/旗判定/清图)→ 极性路由逐批兑付(伤害形态单据暂存——见 _submit 案C 闸)。 - # 判定基线 = 轮末在场旗(发射面已过滤——被挖开的旗已随挖掘擦除零兑付,插旗不可逆批 2026-09-19; - # 开格事实不可撤)。 - for evt in _facts_reveals: - if not bool(evt.get("resource", false)): - continue # 封锁格(initial 通道)恒无资源——不入账(战斗结算 义务②) - var snap = evt.get("reward_snapshot") - if not (snap is Dictionary): - continue - if evt.kind == "safe": - if bool(evt.get("dug_under_flag", false)): - # 插旗不可逆四情况④(2026-09-19 用户拍板):插安全格再挖开 = 零惩罚零奖励——正向兑付 - # 作废(归因行在案);标空判定已随旗擦除自然缺席(被挖的旗不在轮末在场基线)。 - _emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(插旗格直接挖开——零惩罚零奖励)", 0, {}, "") - continue - # dig_reveal 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价: - # 乘区收集 → 正向兑付 → 计数器 → 追加产出(深挖)→ 奥术矢。 - # 迭代 20 T1——payload 扩三事实:is_edge_cell〔边陲矿工〕/is_blank_zero〔跳格采掘〕/ - # new_resource_type〔地质学家——快照 cell_type 首见判定,见后集登记〕) - var board_b: BoardData = minesweeper.current_board() - var snap_pre = evt.get("reward_snapshot") - var ctype := String((snap_pre as Dictionary).get("cell_type", "")) if snap_pre is Dictionary else "" - var is_new_type := ctype != "" and not _fx_new_resource_types.has(ctype) - if is_new_type: - _fx_new_resource_types[ctype] = true - var dr_hits := _fx_emit("dig_reveal", {"cell": evt.cell, "round": ri, - "is_first_dig_of_battle": not _feat_first_dig_used, - "terrain_kind": _harmful_variants(evt.get("variant_ids", []) as Array), - "is_edge_cell": evt.cell.x == 0 or evt.cell.y == 0 - or evt.cell.x == board_b.width - 1 or evt.cell.y == board_b.height - 1, - "is_blank_zero": board_b.digit_of(evt.cell) == 0, - "new_resource_type": is_new_type}) - var pos_m := 0 - var neg_m := 0 - var combo_cap := -1 - for h in dr_hits: - if String(h.get("kind", "")) != "yield_mult": - continue - pos_m += int(h.get("mult", 0)) - neg_m += int(h.get("cost_mult", 0)) - if int(h.get("cap", 0)) > 0: - combo_cap = int(h.cap) # 矿脉直觉连击上限 - _grant_routed(_feat_yield_adjust(snap, true, pos_m, neg_m, combo_cap), "positive", "safe_cell", evt.cell, ri, "") - # 计数器执行(连击计数——加法族,乘区收集后执行=本格兑付用旧值,与迁移前逐位一致) - for h in dr_hits: - if String(h.get("kind", "")) == "counter_add": - var set_v = h.get("set", null) - _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) - # 揭示效果入队(迭代 20 T1——地质学家/跳格采掘 reveal_tiles 订阅面;授予段末 - # _flush_pending_reveals 统一掷格兑付) - if not _hits_of(dr_hits, "reveal_tiles").is_empty(): - _fx_enqueue_reveal() - # 追加产出(深挖钻头 v2——本格双份制:grant_material 命中=配额内追加第二份;标签随行) - for h in dr_hits: - if String(h.get("kind", "")) == "grant_material": - var dd_snap = _reward_economy.grant(evt.cell) - if dd_snap is Dictionary: - _grant_routed(dd_snap, "positive", "safe_cell", evt.cell, ri, "", String(h.get("label", ""))) - # 奥术矢(魔能共鸣——每战首次开安全格追加魔法矢;任意真族单次生效) - var spell_hits: Array = _hits_of(dr_hits, "spawn_spell") - if not spell_hits.is_empty() and not _feat_first_dig_used: - _feat_first_dig_used = true - var spell_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell", - "奥术矢", "board", int((spell_hits[0] as Dictionary).get("value", 0)), - DamageController.TARGET_FRONT_ALIVE, ri, evt.cell, "magic") - _submit(spell_t, false) - elif evt.kind == "mine": - # 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴), - # 标中不发(旗已随挖掘擦除、不入轮末判定基线)。 - # mine_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价: - # 减半收集 → 负向兑付〔带减半系数〕→ 反打开单 → 连击清零计数器。 - # 迭代 20 T1——发射前移+payload 扩 dug_under_flag〔破封一击〕;新增施加面: - # damage_reduce〔安全帽耗层减伤——value_counter 层值,一次清零〕/ heal〔血蜜〕/ - # blast_scaled〔陷阱爆发——雷伤×倍率全体敌〕/ bonus_delta〔血税〕/ reveal_tiles〔见血开窍〕) - var mine_power := int((snap as Dictionary).get("power", 0)) - var mh_hits := _fx_emit("mine_hit", {"cell": evt.cell, "round": ri, - "damage": mine_power, "dug_under_flag": bool(evt.get("dug_under_flag", false)), - "hp_ratio": int(round(100.0 * float(int(_enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(_enemy_system.hero_unit().get("max_hp", 1))))))}) - var halve_pct := 0 - var mine_reduce := 0 - for h in mh_hits: - match String(h.get("kind", "")): - "damage_mult": - halve_pct += int(h.get("value", 0)) - "damage_reduce": - if h.has("value_counter") and String(h.get("value_counter", "")) != "": - mine_reduce += _fx.counter(String(h.get("value_counter", ""))) # 安全帽——层值减免 - else: - mine_reduce += int(h.get("value", 0)) # 背水——定值减免 - "heal": - var hu_heal: Dictionary = _enemy_system.hero_unit() - hu_heal["hp"] = mini(int(hu_heal.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_heal.get("max_hp", 1)))) - "bonus_delta": - var axis := String(h.get("axis", "")) - if _fx_bonus_delta.has(axis): - _fx_bonus_delta[axis] += int(h.get("delta", 0)) - "blast_scaled": - var blast_v := maxi(int(db.settlement.damage_floor), int(float(mine_power) * float(h.get("mult", 1)))) - var bs_t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_blast_scaled", "陷阱爆发", "board", - blast_v, DamageController.TARGET_ALL_ALIVE, ri, evt.cell, "", 0, "", {}) - _submit(bs_t, false) - if mine_reduce > 0: - snap["power"] = maxi(0, mine_power - mine_reduce) # 减伤落快照(负向兑付按减后值——安全帽每层减 1) - for h in mh_hits: - if String(h.get("kind", "")) == "counter_add": - var set_v = h.get("set", null) - _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) - if not _hits_of(mh_hits, "reveal_tiles").is_empty(): - _fx_enqueue_reveal() # 见血开窍——每颗雷一次揭示(授予段末统一兑付) - _grant_routed(_feat_yield_adjust(snap, false, 0, 0, -1), "negative", "mine_hit", evt.cell, ri, "", "", halve_pct) - for h in mh_hits: - if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "reflect": - var nct := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_counter", "惊巢反制·反弹", "board", - int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, evt.cell, "", 0, "", {}) - _submit(nct, false) - # 地形变异 兑付段(授予段尾——触发枚举 on_open:挖开即伤、chain 通道同口径;伤害单据案C 暂存到 - # 战斗段末兑付〔2026-10-06 用户裁〕;信息型 hide_number 不进结算——呈现层读取,见 board_view 数字遮蔽; - # 雷格恒无变异——布置段排除,双层叠罚不存在;插旗格直挖照吃变异伤害——地形后果同 mine_hit 口径, - # 非奖励不作废;吞噬流资源兑付〔荆棘编织/地脉补偿〕中伤害形态行同样经案C闸暂存,非伤害形态照常当场) - for evt in _facts_reveals: - var vids: Array = evt.get("variant_ids", []) - for vid in vids: - var vrow: Dictionary = db.variation.row_by_id(String(vid)) - if String(vrow.get("effect", "")) != "flat_damage": - continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面) - # 地形变异伤害单据(伤害管线单据化批 2026-09-24)——无类型必中,下限保底在控制器管线内 - # 迭代 20 T1——damage_taken 发射前移(单据开立前):damage_reduce〔背水——hp_ratio when - # 面〕先收集再按减后值开单;counter_add〔保险丝/石肤〕+ ac_bonus〔石肤〕照常施加; - # grant_material〔荆棘/地脉〕移到单据提交后(与迁移前兑付值序一致——收益不因减伤变化) - var dt_amount := int(vrow.get("effect_value", 0)) - var dt_hits := _fx_emit("damage_taken", {"kind": "terrain", "amount": dt_amount, - "source": String(vid), "cell": evt.cell, "round": ri, "terrain_kind": String(vid), - "hp_ratio": int(round(100.0 * float(int(_enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(_enemy_system.hero_unit().get("max_hp", 1))))))}) - for h in dt_hits: - match String(h.get("kind", "")): - "damage_reduce": - dt_amount = maxi(0, dt_amount - int(h.get("value", 0))) - "ac_bonus": - _apply_ac_bonus(h) - "counter_add": - var set_v = h.get("set", null) - _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) - var vt := _damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)), "board", \ - dt_amount, DamageController.TARGET_HERO, ri, evt.cell, "", 0, "", {"variant_id": String(vid)}) - _submit(vt, false) - for h in dt_hits: - if String(h.get("kind", "")) == "grant_material": - var tw_snap = _reward_economy.grant(evt.cell) - if tw_snap is Dictionary: - _grant_routed(tw_snap, "positive", "safe_cell", evt.cell, ri, "", String(h.get("label", ""))) - for evt in _facts_flags: - _judge_flag(evt, ri) - # 战旗军规开单段(迭代 12 T3 标旗流——flag_judged 批后统一开单:轮末每面标中旗对最前存活敌 - # 结算 board_typed 必中伤害〔复用零新配方;「旗所在巢穴敌」简化为 default_target 最前存活敌—— - # 台账披露待追认〕;完美布防=本轮旗批全中(hit>0 且 miss==0)时旗伤 ×mult——全对推理奖励)。 - # 伤害单据案C 暂存战斗段末兑付。 - # flag_settle 发射+施加(内容数据驱动扩展批 T3——战旗 open_ticket + 完美布防 modify_ticket - # 改写窗口:旗伤基值×完美倍率后逐旗开单,与迁移前算序一致) - var fs_hits := 0 - var fs_misses := 0 - for fevt in _facts_flags: - if bool(fevt.get("hit", false)): - fs_hits += 1 - else: - fs_misses += 1 - var fs_fx := _fx_emit("flag_settle", {"flags_hit_all": fs_hits > 0 and fs_misses == 0, - "flags_count": fs_hits, "round": ri}) - var banner_hits: Array = [] - for h in fs_fx: - if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "attack": - banner_hits.append(h) - if not banner_hits.is_empty(): - var strike_value := int((banner_hits[0] as Dictionary).get("value", 0)) - var flag_mult := 1 - for h in fs_fx: - if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "flag_mult": - flag_mult *= maxi(1, int(h.get("value", 1))) - if fs_misses == 0 and fs_hits > 0 and flag_mult > 1: - strike_value *= flag_mult # 完美布防:同轮全中旗伤倍增(全对推理奖励) - for fevt in _facts_flags: - if not bool(fevt.get("hit", false)): - continue - var st_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_flag_strike", - "战旗军规", "board", strike_value, DamageController.TARGET_FRONT_ALIVE, ri, - fevt.cell, "normal") - _submit(st_t, false) - # round_end 发射+施加(内容数据驱动扩展批 T3——每轮结算收口:熔火之躯反打单开立〔open_ticket - # 即时执行〕+积层清零〔counter_add set〕;整备转蓄力由 finish_round_stop 承载——停手侧 AP 已在 - # request_stop 记账,转存决策=grant_ap(mode:transfer) 订阅定夺,无订阅走 RAP 转存缺省) - var re_fx := _fx_emit("round_end", {"round": ri}) - for h in re_fx: - if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "blast": - var mg_layers := _fx.counter(String(h.get("per", ""))) - var mg_total := int(h.get("value", 0)) * mg_layers - if mg_total > 0: - # 迭代 20 T9——source_class 中性名(旧件名 feat_magma_body 随熔火之躯退役;per-counter 反打通道保留泛用面) - var mg_t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "fx_blast_counter", - "积层反噬 ×%d" % mg_layers, "board", mg_total, - DamageController.TARGET_ALL_ALIVE, ri, minesweeper.hero_pos(), "", 0, "", {}) - _submit(mg_t, false) - for h in re_fx: - if String(h.get("kind", "")) == "counter_add": - var set_v = h.get("set", null) - _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) - # 迭代 20 T1——round_end 新施加面:grant_gp〔爆破会计/收盘——escrow 战斗域缓冲,随 gp_dropped 上抛〕/ - # grant_ap mode:queue〔封印回响——返 1 AP 下轮,queue_ap_delta 通道与轮始加成同面〕 - for h in re_fx: - if String(h.get("kind", "")) == "grant_gp": - escrow += int(h.get("value", 0)) - _emit_row("effect", "fx_grant_gp", Vector2i(-1, -1), ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": escrow}, "") - elif String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue": - action_economy.queue_ap_delta(int(h.get("value", 0))) - # 转存率入参(迭代 22 秘宝批 T3——储备罐 rap_transfer_mult magnitude=目标转存率百分比 125=1.25:1; - # 未装配=100 与旧 1:1 逐位一致) - var rap_pct: int = attune_param_sum("rap_transfer_mult") - action_economy.finish_round_stop(_fx != null and _fx.has_effect_on("round_end", "grant_ap", "transfer"), - rap_pct if rap_pct > 0 else 100) - if _facts_path == "cleared": - _grant_clear_reward(ri) - _flush_pending_reveals(ri) # 迭代 20 T1——揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入) - _damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界) - # ② 增益/减益段(续存实例组合行;本结算新授予的已在授予段逐 grant 出行——增益状态 义务⑦) - _emit_composition_effects("hero", ri) - _emit_composition_effects("enemy", ri) - # ③ 战斗段(调用序:主角先手 → 逐敌还手 → 地图伤害账殿后〔案C 2026-10-06 用户裁〕;hero hp=0 短路) - _combat_segment(ri) - # ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减) - var k: int = _status_effects.decay_round() - _emit_row("decay", "", Vector2i(-1, -1), ri, "状态衰减", k, {}, "") - # ⑤ 判定段(防御性优先级 hero 先判——双死结构不可达的纵深防御) - var verdict := "continue" - if int(_enemy_system.hero_unit().get("hp", 0)) <= 0: - verdict = "defeat" - elif _enemy_system.query_alive("enemy").is_empty(): - verdict = "victory" - _emit_settlement_snapshot(verdict) # 全量状态快照(despawn 前组装——终局含 result) - if verdict == "defeat": - _emit_row("verdict", "", Vector2i(-1, -1), ri, "战斗失败", 0, {}, "") - _battle_over("defeat") - return - if verdict == "victory": - _emit_row("verdict", "", Vector2i(-1, -1), ri, "战斗胜利", 1, {}, "") - _battle_over("victory") - return - _emit_row("verdict", "", Vector2i(-1, -1), ri, "下一轮次", 0, {}, "") - _exit_distribution() + # 段序编排(迭代 27 T2——五段 Segment;段体归 game/core/settlement/,本类退为段序+ + # 行发射/开单原语单点。旧 259 行巨函数体已逐字迁五段类,git 史留痕) + _grant_seg.run(ri) # ① 授予段(案C 闸随段装载) + _aura_seg.run(ri) # ② 增益/减益段(续存实例组合行) + _combat_seg.run(ri) # ③ 战斗段(主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕) + _decay_seg.run(ri) # ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减) + var verdict: String = _verdict_seg.run(ri) # ⑤ 判定段(防御性优先级 hero 先判) + if verdict != "continue": + return # 终局:旧早退语义逐位一致(_settling 不回置——_battle_over 已接管) _settling = false @@ -1162,95 +938,6 @@ func _submit_flat_hero(source_class: String, display: String, power: int, cell: # 旗判定批:标中 = 该雷格资源的正向兑付(每雷实例一次;高光 = 强制检查点); # 标空 = "标旗未中"归因行 + 该安全格资源的负向兑付(同键去重——每格实例一次)。 -func _judge_flag(evt: Dictionary, ri: int) -> void: - var cell: Vector2i = evt.cell - if evt.hit: - if _flag_consumed.has(cell): - return # 去重跳过(资格以雷格实例为键——排雷玩法 诚实重复上报的对侧);零明细行 - _flag_consumed[cell] = true - _perfect_hit_true += 1 - # flag_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:殉爆〔T4 秘宝面〕→ - # 净化改写 → 图谱额外资源 → 基线兑付) - var fh_hits := _fx_emit("flag_hit", {"cell": cell, "round": ri, - "terrain_kind": _harmful_adjacent(cell)}) - # 标中殉爆(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:订阅施加——行 triggers open_ticket - # channel blast value_from magnitude;每标中一面旗全体存活敌吃 N 群伤,无类型走 blast 通道。 - # 迭代 20 T1——封印爆裂〔迁入专长〕同面消费:行 triggers open_ticket channel blast) - for h in _hits_of(fh_hits, "open_ticket"): - if String(h.get("channel", "")) != "blast": - continue - var fbt := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_flag_burst", "封印爆裂", "board", - int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, "highlight", {}) - _submit(fbt, false) - # 迭代 20 T1——标中新施加面:counter_add〔血税 标中+1 攻——bonus_delta atk 轴〕/ - # ac_bonus〔封印桩 本轮 AC+N〕/ preview_area〔示警烟火 3×3 资源类型预告〕/ - # remove_mines〔封印匠 每第 3 次该危险格中立移除——mod_eq 闸在行 when〕。 - # (净化工兵 rewrite_board 分支随老 27 件退役摘除——效果类型同批除名,T1 落法 commit 留痕) - for h in fh_hits: - match String(h.get("kind", "")): - "counter_add": - var set_v = h.get("set", null) - _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) - "ac_bonus": - _apply_ac_bonus(h) - "bonus_delta": - var axis_f := String(h.get("axis", "")) - if _fx_bonus_delta.has(axis_f): - _fx_bonus_delta[axis_f] += int(h.get("delta", 0)) - "remove_mines": - var removed: Array = minesweeper.remove_mines_in([cell]) - if not removed.is_empty(): - _emit_row("mine_neutralized", "fx_seal_smith", cell, ri, "封印匠·危险格中立移除", 0, {}, "") - "preview_area": - var board_p: BoardData = minesweeper.current_board() - var types: Array = [] - for n in board_p.neighbors8(cell): - var n_snap = _reward_economy.query_full_snapshot(n) - if n_snap is Dictionary and not types.has(String((n_snap as Dictionary).get("cell_type", ""))): - types.append(String((n_snap as Dictionary).get("cell_type", ""))) - if treasure_inventory != null and not types.is_empty(): - treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell)) - _emit_row("fx_preview_area", "", cell, ri, "示警烟火·周围资源类型", 0, {"types": types}, "") - # 雷区图谱(迭代 12 T3 标旗流——标中雷额外获得一份格面资源:插旗开始产出对标开格; - # 复用 grant 单点,额外兑付走同一 _grant_routed 正向路由) - for h in fh_hits: - if String(h.get("kind", "")) == "grant_material": - var extra_snap = _reward_economy.grant(cell) - if extra_snap is Dictionary: - _grant_routed(extra_snap, "positive", "flag_hit", cell, ri, "", String(h.get("label", ""))) - var snap = _reward_economy.grant(cell) - if not (snap is Dictionary): - return - _grant_routed(snap, "positive", "flag_hit", cell, ri, "flag_hit") - else: - _perfect_hit_false += 1 - if _flag_miss_seen.has(cell): - return # 每格实例一次去重(归因行与负向兑付同键——防逐轮惩罚轰炸) - _flag_miss_seen[cell] = true - # 完美保险(迭代 20 T4——本战下一次标错不破坏完美资格:_perfect_hit_false 回退=本次标错 - # 对完美谓词无痕;只保一次/件,节点级用完即止) - if _fx_insurance > 0: - _fx_insurance -= 1 - _perfect_hit_false -= 1 - _emit_row("insurance_used", "", cell, ri, "完美保险·标错豁免", 0, {"left": _fx_insurance}, "") - var hl := "" - if _perfect_hit_false == 1: - hl = "perfect_broken" # 本板完美谓词已破的归因锚 - _emit_row("flag_miss", "flag_miss", cell, ri, "标旗未中", 0, {}, hl) - # 危险直觉·守挂点(迭代 21 T3——flag_missed 发射:契约 12 触发主语=「当回合发生的标错 - # 动作」——去重闸=新旧标错判别器〔后续轮重判旧旗被闸挡回零触发〕;双条件②「结算时旗仍在 - # 场」由发射面既有过滤承载〔被挖开的旗已随挖掘擦除零兑付〕;grant_ap mode:queue 下一轮 - # 开轮兑现——末轮判出无下一轮=合法作废自动成立〔编排器每战新建,队列不跨战〕;限次=合成行 - # adventure 域计数器闸) - for h in _fx_emit("flag_missed", {"cell": cell, "round": ri}): - if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue": - action_economy.queue_ap_delta(int(h.get("value", 0))) - var snap = _reward_economy.grant(cell) - if not (snap is Dictionary): - return - _grant_routed(snap, "negative", "flag_miss", cell, ri, "") - - # 清图批:清图奖励对象恰好发一次;完美排雷追加第二份同身份兑付(grant_clear 本身恰一次)。 # 高光行 value = 实际有效兑付份数(禁记名义值;首份零兑付 → 降级不发)。 func _grant_clear_reward(ri: int) -> void: @@ -1336,93 +1023,6 @@ func _roll_round_actions() -> void: # 开单守卫归流程(用户裁决③——流程决定给谁开单):主角存活+目标存在才开先手单;hero hp=0 立即 # 停开(B 模式下若 flush 中主角倒下,由控制器死亡事件终止余单——两模式同一结果)。 -func _combat_segment(ri: int) -> void: - _defer_board_damage = false # 案C 闸复位——战斗段起开单即入队(主角/敌方/补提交的地图单按开单序统一结账) - # 本轮扫雷事实推导(迭代 12 T4——执拗 streak/连珠矢段数共用取数面):本轮 safe 事件数= - # streak;踩雷即清零(mine_hit 监听同口——执拗「踩雷清零」承载)。攻击在战斗段=授予段后 - # 同一次 _settle 内,facts 已定。 - var round_safe := 0 - var round_mine := false - for evt in _facts_reveals: - if evt.kind == "safe": - round_safe += 1 - elif evt.kind == "mine": - round_mine = true - _damage.dig_streak = 0 if round_mine else round_safe - # 蓄力层注入(内容数据驱动扩展批 T3——整备军备「每层下轮首段 +1」/厚积薄发满层 spike/铁壁整备 - # 减伤——三件 fact charge_layers 同读 action_economy;battle 域每轮刷新) - _damage.charge_layers = action_economy.charge_layers - # ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动。案C 后授予段不再产出伤害, - # 主角进入战斗段时恒存活——先手必兑,除非场上无敌)。 - # 单分量伤害构成(2026-09-19 充能退役批·用户拍板):主角攻击 = 唯一基础分量(atk,吃掷点含暴击; - # 类型 = 主角 attack_type 表值——2026-09-18 营地扩展前置批:量与类型抽象分离;查甲效矩阵对应行取克制修正)。 - # 原"基础 + 穿刺/魔法充能三分量"模型随充能系统整体退役——克制策略归冒险前配置(营地换型 / 未来冒险前参数面), - # 局内不再有挖掘攒弹药的第二伤害供给。attack_up 增益经 hero_offense 池乘攻击合计(见池乘区注记)。 - # 护甲等级掷点批(2026-09-16):基础分量吃掷点判定(含暴击);棋盘来源伤害不掷不折; - # 任何伤害落地 ≥ damage_floor(全局 max(1,·)——2026-09-20 用户拍板扩全来源,下限在控制器管线单点收口)。 - var hero_alive: bool = int(_enemy_system.hero_unit().get("hp", 0)) > 0 - var hero_target = _enemy_system.default_target() - if hero_alive and hero_target != null: - var ht := _damage.open_ticket(DamageController.KIND_HERO_ATTACK, "hero_attack", "主角攻击", "combat", _hero_atk, \ - DamageController.TARGET_FRONT_ALIVE, ri, Vector2i(-1, -1), String(_enemy_system.hero_unit().get("attack_type", "normal"))) - _damage.submit(ht) - # 连珠矢(内容数据驱动扩展批 T3——attack_commit 订阅施加面:开格节奏变攻击段数,本轮每开 - # ~3 安全格 +1 段穿刺附加(上限 cap;段值 provisional 固定值不乘 atk——台账披露)。施加位 - # 紧随主角攻击单开立=迁移前开单位(先于敌方行动单入队——flush 兑现序逐位一致) - for h in _hits_of(ht.context.get("fx_attack_hits", []) as Array, "attack_segments"): - var segments := mini(int(h.get("cap", 2)), _damage.dig_streak / maxi(1, int(h.get("divisor", 3)))) - for s in segments: - # 迭代 20 T9——source_class 中性名(旧件名 feat_chain_arrows 随连珠矢退役;attack_segments 兑付通道保留泛用面) - var ct := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "fx_streak_segments", - "连击分段", "board", int(h.get("value", 0)), DamageController.TARGET_FRONT_ALIVE, ri, - minesweeper.hero_pos(), "pierce") - _damage.submit(ct) - # 敌方行动(敌人技能池批——本拍动作开轮已摇定,此处按技能行兑现;行序 = 生成序)。 - # 死亡即不动手:摇定后、兑现前死亡的敌不在存活序中——本拍技能作废,不 commit、不进冷却。 - for u in _enemy_system.query_alive("enemy"): - if int(_enemy_system.hero_unit().hp) <= 0: - break # hero hp=0 立即短路(死尸挨打行是归因噪音——用户拍板) - var skill_id := _enemy_system.pending_action(u) - var srow: Dictionary = db.skill_row(skill_id) - if srow.is_empty(): - _fatal("敌实例待发动作悬空: %s" % skill_id) - continue - if String(srow.type) == "buff" or String(srow.type) == "debuff": - # 增益/妨害技(新效应词汇批 2026-09-19)= 0 伤害施放行:不掷不折;状态授予即数值兑现,技能冷却与增益持续两计时器分账。 - # (原"本轮喂的伏击逢施放拍不兑现"读怪奖励随伏击退役一并作废——2026-09-20 八型批) - # 落点由状态行 target_side 路由(buff → 敌方侧 / debuff → 主角侧——容器零改动,proposal-status-vocabulary §1)。 - var dur: int = int(srow.get("buff_duration", 1)) - _status_effects.apply_effect(String(srow.buff_ref), {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": ri}, dur) - _emit_row("cast", "enemy_action", Vector2i(-1, -1), ri, "%s 施放 %s" % [u.display_name, String(srow.display_name)], 0, {"skill_id": skill_id, "effect_id": String(srow.buff_ref), "duration": dur}) - _enemy_system.commit_action(u) - continue - # 伤害技(含普攻——倍率恒 1)= 敌方伤害技单据:基线 = round(atk×倍率)、rider 预计算归开单侧 - # (表值算术零随机),掷点/克制查表(第 9 参 = 敌方 attack_type——迭代 24 N3 轴开列前恒空串不查表)/ - # 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), String(u.get("attack_type", "normal")), rider) - et["source_unit"] = u - et["context"] = {"skill_id": skill_id, "baseline": baseline} - _damage.submit(et) - # 地图伤害兑付(案C 2026-10-06 用户裁——战斗段末:怪物还手后按授予段事件序补提交统一结账; - # 主角死于怪物 → 地图账不兑,逐单留「未生效」行不作废静默——没生效也要留痕口径; - # 补提交后若地图直伤反杀主角,控制器 _halted 作废余单——战斗已败,胜负归判定段) - if int(_enemy_system.hero_unit().get("hp", 0)) <= 0: - for t in _deferred_board: - _emit_row("reward_ineffective", String(t.get("source_class", "")), t.get("origin_cell", Vector2i(-1, -1)), ri, - "%s 未生效(主角已倒下)" % String(t.get("display", "")), 0, {"forfeit": "hero_down"}, "") - else: - for t in _deferred_board: - _damage.submit(t) - _deferred_board.clear() - _damage.flush() # 战斗段界结账(B 模式——主角单+敌方单+地图单按开单序统一兑现;A 模式地图单补提交时即结) - - # ---- 算术读数(伤害管线单据化批 2026-09-24 迁移注记)---- # 掷点/折扣/克制点数/池乘区/攻击行文本已全部迁入 伤害控制器(damage_controller.gd 管线单点); # 本类仅保留一处薄委托——_defender_ac(快照面消费)。_clamped_pool 委托壳已随 2026-10-02 修复批删除 diff --git a/game/core/damage_controller.gd b/game/core/damage_controller.gd index c175a501..77a375c3 100644 --- a/game/core/damage_controller.gd +++ b/game/core/damage_controller.gd @@ -268,12 +268,27 @@ func _resolve(t: Dictionary) -> void: # 主角先手攻击:掷点 → 折扣/暴击 → 克制点数 → hero_offense×enemy_defense 池乘区 → 下限 → 交付 # (内容数据驱动扩展批 T3——attack_commit/clamp_check/armor_roll 订阅施加面:命中=开单入口随单 -# context 携带,本函数按迁移前算序逐族施加——命中序=数据表声明序,确定性) +# context 携带,按迁移前算序逐族施加——命中序=数据表声明序,确定性)。 +# 迭代 27 T2 姊妹病灶拆段(P1-1):原 148 行单体拆四步——命中面(_hero_attack_ctx,目标/护甲/ +# 掷点/按序改写族四拍/克制点数) → 基值装配(_hero_attack_base_value,加法族/计数器施加/倍乘) → +# 掷点兑现(_hero_attack_redeem,rolled/池乘/下限/rider/落地/击杀发射) → 交付(_hero_attack_deliver, +# 状态施加/标签/明细行)。步骤间经 ctx/redemption 字典穿线——算序/取值逐字保留(🔴 逐位一致契约)。 func _resolve_hero_attack(t: Dictionary) -> void: + var ctx := _hero_attack_ctx(t) + if ctx.is_empty(): + return # 不可达(流程开单前已有存活守卫)——防御位不发行(outcome 已置 zero) + var base_value: float = _hero_attack_base_value(t, ctx) + var redemption := _hero_attack_redeem(t, ctx, base_value) + _hero_attack_deliver(t, ctx, redemption) + + +# 命中面(原 :273-328 逐字):目标/护甲克制 matchup/armor_roll 发射/按序改写族四拍(矿工重锤命中 +# 交换→专注免掷→厚积薄发必中→猎弱折扣移档)/克制点数读值+订阅改写。返回穿线 ctx(空=目标不可达)。 +func _hero_attack_ctx(t: Dictionary) -> Dictionary: var target = _target_for(String(t.target_rule)) if target == null: _outcomes[int(t.ticket_id)] = "zero" # 不可达(流程开单前已有存活守卫)——防御位不发行 - return + return {} var armor: String = String(target.get("armor_type", "light")) var atk_type: String = String(t.attack_type) var matchup := (atk_type + "_vs_" + armor) if atk_type != "" else "" @@ -326,10 +341,20 @@ func _resolve_hero_attack(t: Dictionary) -> void: # 淬毒刃纹(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:特判拆除——行 triggers clamp_check # modify(mode:clamp_bonus) 订阅命中已并入上方 delta 合计;mode 路由=主角攻击面专享, # 棋盘克制直伤窗只消费无模式 delta——棋盘不吃毒面与迁移前一致) + return {"target": target, "armor": armor, "atk_type": atk_type, "ac_hits": ac_hits, + "ac_payload": ac_payload, "roll_read": roll_read, "threshold": threshold, + "crit": crit, "discount": discount, "spike_ready": spike_ready, "cdelta": cdelta} + + +# 基值装配(原 :329-355 逐字):基值+单位标记加伤+矿工重锤增伤面+加法族(attack_bonus/ +# value_counter 层值)+计数器施加(attack_commit 用后清零行——命中序=声明序)+spike 倍乘。 +func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float: + var ac_hits: Array = ctx["ac_hits"] + var ac_payload: Dictionary = ctx["ac_payload"] var base_value := float(t.base_value) # 单位标记加伤(迭代 20 T1——处刑台 target_attack_bonus 挂单位字典 fx_atk_bonus 键; # 阈值沿发射点已写入,本处通用消费——对该敌伤害加算) - base_value += float(int(target.get("fx_atk_bonus", 0))) + base_value += float(int((ctx["target"] as Dictionary).get("fx_atk_bonus", 0))) # 按序改写族第五拍:矿工重锤增伤面(交换的正向侧) for h in ac_hits: if String(h.get("kind", "")) == "modify_ticket" and int(h.get("trade_atk", 0)) > 0: @@ -349,17 +374,27 @@ func _resolve_hero_attack(t: Dictionary) -> void: var set_v = h.get("set", null) _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) # 任意真族:厚积薄发满层 spike 倍乘(≥3 层——强制必中已在折扣拍承载) - if spike_ready: + if ctx["spike_ready"]: for h in ac_hits: if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "spike": base_value *= float(int(h.get("value", 1))) - var rolled: int = DamageMath.rolled_component(base_value, discount, crit, int(db.settlement.crit_multiplier), cdelta) + return base_value + + +# 掷点兑现(原 :356-402 逐字):rolled 分量/payload 归因/池乘区/下限/英雄侧 rider 族/落地扣血/ +# 击杀标记/kill 发射+贯穿弹射。返回 {landed, payload, hero_rider}(交付步消费)。 +func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> Dictionary: + var target = ctx["target"] + var ac_hits: Array = ctx["ac_hits"] + var roll_read: Dictionary = ctx["roll_read"] + var atk_type: String = ctx["atk_type"] + var rolled: int = DamageMath.rolled_component(base_value, int(ctx["discount"]), bool(ctx["crit"]), int(db.settlement.crit_multiplier), int(ctx["cdelta"])) var payload: Dictionary = {"target": String(target.display_name)} payload[atk_type] = rolled # 分量键 = 实际攻击类型(归因字段,单分量模型) payload["roll"] = int(roll_read.roll) - payload["ac"] = threshold - payload["tier"] = discount - if crit: + payload["ac"] = int(ctx["threshold"]) + payload["tier"] = int(ctx["discount"]) + if bool(ctx["crit"]): payload["crit"] = true var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_offense"), clamped_pool("enemy_defense")) var landed: int = maxi(int(db.settlement.damage_floor), pooled) @@ -370,12 +405,12 @@ func _resolve_hero_attack(t: Dictionary) -> void: var hero_rider := rider_tier_bonus for h in ac_hits: if String(h.get("kind", "")) == "add_ticket_rider": - var contrib := int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ac_payload) + var contrib := int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ctx["ac_payload"]) if h.has("cap"): contrib = mini(int(h.cap), contrib) hero_rider += contrib if hero_rider > 0: - if crit and rider_tier_crit: + if bool(ctx["crit"]) and rider_tier_crit: hero_rider = int(round(float(hero_rider) * float(db.settlement.crit_multiplier))) landed += hero_rider payload["hero_rider"] = hero_rider @@ -400,17 +435,23 @@ func _resolve_hero_attack(t: Dictionary) -> void: _queue.append(of_t) else: _resolve(of_t) + return {"landed": landed, "payload": payload, "hero_rider": hero_rider} + + +# 交付(原 :403-414 逐字):即时执行族(碎甲震击护甲裂解)/标签装配/明细行发射/兑现登记。 +func _hero_attack_deliver(t: Dictionary, ctx: Dictionary, redemption: Dictionary) -> void: + var payload: Dictionary = redemption["payload"] # 即时执行族:碎甲震击打重甲附加「护甲裂解」(复用 off_guard——零新状态行;apply_effect 域闭/ # 重复授予由状态容器自行守卫) - for h in ac_hits: + for h in (ctx["ac_hits"] as Array): if String(h.get("kind", "")) == "apply_status": # 迭代 20 T9——source_class 中性名(旧件名 feat_armor_crack 随碎甲震击退役;apply_status 通道保留泛用面) effects.apply_effect(String(h.get("status_id", "")), {"source_class": "fx_apply_status"}, -1) payload["cracked"] = true - var label := _attack_label(String(t.display), crit, discount) - if hero_rider > 0: - label += "+奥术%d(必中)" % hero_rider - emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), label, landed, payload, String(t.highlight)) + var label := _attack_label(String(t.display), bool(ctx["crit"]), int(ctx["discount"])) + if int(redemption["hero_rider"]) > 0: + label += "+奥术%d(必中)" % int(redemption["hero_rider"]) + emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), label, int(redemption["landed"]), payload, String(t.highlight)) _outcomes[int(t.ticket_id)] = "redeemed" diff --git a/game/core/settlement/aura_segment.gd b/game/core/settlement/aura_segment.gd new file mode 100644 index 00000000..add4aca7 --- /dev/null +++ b/game/core/settlement/aura_segment.gd @@ -0,0 +1,16 @@ +class_name AuraSegment +extends RefCounted +# ② 增益/减益段 Segment(迭代 27 T2——P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。 +# 职责:续存实例组合行(本结算新授予的已在授予段逐 grant 出行——增益状态 义务⑦)。 +# 行发射归宿主编排器(oc)——零算法改动(🔴 零行为变更契约)。 + +var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) 宿主 + + +func _init(host) -> void: + oc = host + + +func run(ri: int) -> void: + oc._emit_composition_effects("hero", ri) + oc._emit_composition_effects("enemy", ri) diff --git a/game/core/settlement/aura_segment.gd.uid b/game/core/settlement/aura_segment.gd.uid new file mode 100644 index 00000000..05197d02 --- /dev/null +++ b/game/core/settlement/aura_segment.gd.uid @@ -0,0 +1 @@ +uid://buwfoafhp2wvk diff --git a/game/core/settlement/combat_segment.gd b/game/core/settlement/combat_segment.gd new file mode 100644 index 00000000..6e9da4a8 --- /dev/null +++ b/game/core/settlement/combat_segment.gd @@ -0,0 +1,101 @@ +class_name CombatSegment +extends RefCounted +# ③ 战斗段 Segment(迭代 27 T2——P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。 +# 职责:案C 闸复位 + 本轮扫雷事实推导(执拗 streak/连珠矢段数)+ 蓄力层注入 + 主角先手 +# (含连珠矢分段开单)+ 敌方行动兑现 + 地图伤害账殿后(补提交统一结账)。 +# 调用序:主角先手 → 逐敌还手 → 地图伤害账殿后〔案C 2026-10-06 用户裁〕;hero hp=0 短路。 +# 行发射/开单/兑付原语仍归宿主编排器(oc)——本函数体为原编排器 _combat_segment 逐字搬运 +# (🔴 零行为变更契约)。 + +var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) 宿主 + + +func _init(host) -> void: + oc = host + + +func run(ri: int) -> void: + oc._defer_board_damage = false # 案C 闸复位——战斗段起开单即入队(主角/敌方/补提交的地图单按开单序统一结账) + # 本轮扫雷事实推导(迭代 12 T4——执拗 streak/连珠矢段数共用取数面):本轮 safe 事件数= + # streak;踩雷即清零(mine_hit 监听同口——执拗「踩雷清零」承载)。攻击在战斗段=授予段后 + # 同一次 _settle 内,facts 已定。 + var round_safe := 0 + var round_mine := false + for evt in oc._facts_reveals: + if evt.kind == "safe": + round_safe += 1 + elif evt.kind == "mine": + round_mine = true + oc._damage.dig_streak = 0 if round_mine else round_safe + # 蓄力层注入(内容数据驱动扩展批 T3——整备军备「每层下轮首段 +1」/厚积薄发满层 spike/铁壁整备 + # 减伤——三件 fact charge_layers 同读 action_economy;battle 域每轮刷新) + oc._damage.charge_layers = oc.action_economy.charge_layers + # ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动。案C 后授予段不再产出伤害, + # 主角进入战斗段时恒存活——先手必兑,除非场上无敌)。 + # 单分量伤害构成(2026-09-19 充能退役批·用户拍板):主角攻击 = 唯一基础分量(atk,吃掷点含暴击; + # 类型 = 主角 attack_type 表值——2026-09-18 营地扩展前置批:量与类型抽象分离;查甲效矩阵对应行取克制修正)。 + # 原"基础 + 穿刺/魔法充能三分量"模型随充能系统整体退役——克制策略归冒险前配置(营地换型 / 未来冒险前参数面), + # 局内不再有挖掘攒弹药的第二伤害供给。attack_up 增益经 hero_offense 池乘攻击合计(见池乘区注记)。 + # 护甲等级掷点批(2026-09-16):基础分量吃掷点判定(含暴击);棋盘来源伤害不掷不折; + # 任何伤害落地 ≥ damage_floor(全局 max(1,·)——2026-09-20 用户拍板扩全来源,下限在控制器管线单点收口)。 + var hero_alive: bool = int(oc._enemy_system.hero_unit().get("hp", 0)) > 0 + var hero_target = oc._enemy_system.default_target() + if hero_alive and hero_target != null: + var ht := oc._damage.open_ticket(DamageController.KIND_HERO_ATTACK, "hero_attack", "主角攻击", "combat", oc._hero_atk, \ + DamageController.TARGET_FRONT_ALIVE, ri, Vector2i(-1, -1), String(oc._enemy_system.hero_unit().get("attack_type", "normal"))) + oc._damage.submit(ht) + # 连珠矢(内容数据驱动扩展批 T3——attack_commit 订阅施加面:开格节奏变攻击段数,本轮每开 + # ~3 安全格 +1 段穿刺附加(上限 cap;段值 provisional 固定值不乘 atk——台账披露)。施加位 + # 紧随主角攻击单开立=迁移前开单位(先于敌方行动单入队——flush 兑现序逐位一致) + for h in oc._hits_of(ht.context.get("fx_attack_hits", []) as Array, "attack_segments"): + var segments := mini(int(h.get("cap", 2)), oc._damage.dig_streak / maxi(1, int(h.get("divisor", 3)))) + for s in segments: + # 迭代 20 T9——source_class 中性名(旧件名 feat_chain_arrows 随连珠矢退役;attack_segments 兑付通道保留泛用面) + var ct := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "fx_streak_segments", + "连击分段", "board", int(h.get("value", 0)), DamageController.TARGET_FRONT_ALIVE, ri, + oc.minesweeper.hero_pos(), "pierce") + oc._damage.submit(ct) + # 敌方行动(敌人技能池批——本拍动作开轮已摇定,此处按技能行兑现;行序 = 生成序)。 + # 死亡即不动手:摇定后、兑现前死亡的敌不在存活序中——本拍技能作废,不 commit、不进冷却。 + for u in oc._enemy_system.query_alive("enemy"): + if int(oc._enemy_system.hero_unit().hp) <= 0: + break # hero hp=0 立即短路(死尸挨打行是归因噪音——用户拍板) + var skill_id := oc._enemy_system.pending_action(u) + var srow: Dictionary = oc.db.skill_row(skill_id) + if srow.is_empty(): + oc._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)) + oc._status_effects.apply_effect(String(srow.buff_ref), {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": ri}, dur) + oc._emit_row("cast", "enemy_action", Vector2i(-1, -1), ri, "%s 施放 %s" % [u.display_name, String(srow.display_name)], 0, {"skill_id": skill_id, "effect_id": String(srow.buff_ref), "duration": dur}) + oc._enemy_system.commit_action(u) + continue + # 伤害技(含普攻——倍率恒 1)= 敌方伤害技单据:基线 = round(atk×倍率)、rider 预计算归开单侧 + # (表值算术零随机),掷点/克制查表(第 9 参 = 敌方 attack_type——迭代 24 N3 轴开列前恒空串不查表)/ + # 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(oc.db.settlement.damage_floor), int(floor(float(u.atk) * float(srow.rider_mult)))) + var et := oc._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), String(u.get("attack_type", "normal")), rider) + et["source_unit"] = u + et["context"] = {"skill_id": skill_id, "baseline": baseline} + oc._damage.submit(et) + # 地图伤害兑付(案C 2026-10-06 用户裁——战斗段末:怪物还手后按授予段事件序补提交统一结账; + # 主角死于怪物 → 地图账不兑,逐单留「未生效」行不作废静默——没生效也要留痕口径; + # 补提交后若地图直伤反杀主角,控制器 _halted 作废余单——战斗已败,胜负归判定段) + if int(oc._enemy_system.hero_unit().get("hp", 0)) <= 0: + for t in oc._deferred_board: + oc._emit_row("reward_ineffective", String(t.get("source_class", "")), t.get("origin_cell", Vector2i(-1, -1)), ri, + "%s 未生效(主角已倒下)" % String(t.get("display", "")), 0, {"forfeit": "hero_down"}, "") + else: + for t in oc._deferred_board: + oc._damage.submit(t) + oc._deferred_board.clear() + oc._damage.flush() # 战斗段界结账(B 模式——主角单+敌方单+地图单按开单序统一兑现;A 模式地图单补提交时即结) diff --git a/game/core/settlement/combat_segment.gd.uid b/game/core/settlement/combat_segment.gd.uid new file mode 100644 index 00000000..b37f260a --- /dev/null +++ b/game/core/settlement/combat_segment.gd.uid @@ -0,0 +1 @@ +uid://cmr602bpitrxd diff --git a/game/core/settlement/decay_segment.gd b/game/core/settlement/decay_segment.gd new file mode 100644 index 00000000..c54e13d2 --- /dev/null +++ b/game/core/settlement/decay_segment.gd @@ -0,0 +1,16 @@ +class_name DecaySegment +extends RefCounted +# ④ 衰减段 Segment(迭代 27 T2——P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。 +# 职责:状态衰减恰一次恒执行含末轮——先兑付后衰减。行发射归宿主编排器(oc)—— +# 零算法改动(🔴 零行为变更契约)。 + +var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) 宿主 + + +func _init(host) -> void: + oc = host + + +func run(ri: int) -> void: + var k: int = oc._status_effects.decay_round() + oc._emit_row("decay", "", Vector2i(-1, -1), ri, "状态衰减", k, {}, "") diff --git a/game/core/settlement/decay_segment.gd.uid b/game/core/settlement/decay_segment.gd.uid new file mode 100644 index 00000000..8cc85091 --- /dev/null +++ b/game/core/settlement/decay_segment.gd.uid @@ -0,0 +1 @@ +uid://c4oht2fyigigk diff --git a/game/core/settlement/grant_segment.gd b/game/core/settlement/grant_segment.gd new file mode 100644 index 00000000..bb1911b9 --- /dev/null +++ b/game/core/settlement/grant_segment.gd @@ -0,0 +1,333 @@ +class_name GrantSegment +extends RefCounted +# ① 授予段 Segment(迭代 27 T2——P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。 +# 职责:结算队列(开格/旗判定/清图)→ 极性路由逐批兑付 + 旗判/军规开单 + round_end 施加 + +# 转存收口 + 揭示兑付 + 段界结账。行发射/开单/兑付原语仍归宿主编排器(oc)—— +# 本段只搬运时序,零算法改动(🔴 零行为变更契约:同种子完整局输出逐位一致)。 +# kind 分派单点化(P1-1 同批):counter_add 五位三行组 / grant_material 三位兑付块 +# 收 _apply_counter_hit / _apply_material_hit 两助手(原 :683/:742/:777/:839/:1193 与 +# :690/:782/:1217 八处——分支本体逐字保留在助手内)。 + +var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) 宿主(状态/行发射/开单单点仍归其身——段只搬运时序) + + +func _init(host) -> void: + oc = host + + +func run(ri: int) -> void: + # ① 授予段:结算队列(开格/旗判定/清图)→ 极性路由逐批兑付(伤害形态单据暂存——见 _submit 案C 闸)。 + # 判定基线 = 轮末在场旗(发射面已过滤——被挖开的旗已随挖掘擦除零兑付,插旗不可逆批 2026-09-19; + # 开格事实不可撤)。 + oc._defer_board_damage = true # 案C:授予段伤害单据只暂存不结账(2026-10-06 用户裁——兑付时点=战斗段末) + for evt in oc._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 用户拍板):插安全格再挖开 = 零惩罚零奖励——正向兑付 + # 作废(归因行在案);标空判定已随旗擦除自然缺席(被挖的旗不在轮末在场基线)。 + oc._emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(插旗格直接挖开——零惩罚零奖励)", 0, {}, "") + continue + # dig_reveal 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价: + # 乘区收集 → 正向兑付 → 计数器 → 追加产出(深挖)→ 奥术矢。 + # 迭代 20 T1——payload 扩三事实:is_edge_cell〔边陲矿工〕/is_blank_zero〔跳格采掘〕/ + # new_resource_type〔地质学家——快照 cell_type 首见判定,见后集登记〕) + var board_b: BoardData = oc.minesweeper.current_board() + var snap_pre = evt.get("reward_snapshot") + var ctype := String((snap_pre as Dictionary).get("cell_type", "")) if snap_pre is Dictionary else "" + var is_new_type := ctype != "" and not oc._fx_new_resource_types.has(ctype) + if is_new_type: + oc._fx_new_resource_types[ctype] = true + var dr_hits := oc._fx_emit("dig_reveal", {"cell": evt.cell, "round": ri, + "is_first_dig_of_battle": not oc._feat_first_dig_used, + "terrain_kind": oc._harmful_variants(evt.get("variant_ids", []) as Array), + "is_edge_cell": evt.cell.x == 0 or evt.cell.y == 0 + or evt.cell.x == board_b.width - 1 or evt.cell.y == board_b.height - 1, + "is_blank_zero": board_b.digit_of(evt.cell) == 0, + "new_resource_type": is_new_type}) + var pos_m := 0 + var neg_m := 0 + var combo_cap := -1 + for h in dr_hits: + if String(h.get("kind", "")) != "yield_mult": + continue + pos_m += int(h.get("mult", 0)) + neg_m += int(h.get("cost_mult", 0)) + if int(h.get("cap", 0)) > 0: + combo_cap = int(h.cap) # 矿脉直觉连击上限 + oc._grant_routed(oc._feat_yield_adjust(snap, true, pos_m, neg_m, combo_cap), "positive", "safe_cell", evt.cell, ri, "") + # 计数器执行(连击计数——加法族,乘区收集后执行=本格兑付用旧值,与迁移前逐位一致) + for h in dr_hits: + if String(h.get("kind", "")) == "counter_add": + _apply_counter_hit(h) + # 揭示效果入队(迭代 20 T1——地质学家/跳格采掘 reveal_tiles 订阅面;授予段末 + # _flush_pending_reveals 统一掷格兑付) + if not oc._hits_of(dr_hits, "reveal_tiles").is_empty(): + oc._fx_enqueue_reveal() + # 追加产出(深挖钻头 v2——本格双份制:grant_material 命中=配额内追加第二份;标签随行) + for h in dr_hits: + if String(h.get("kind", "")) == "grant_material": + _apply_material_hit(h, evt.cell, ri) + # 奥术矢(魔能共鸣——每战首次开安全格追加魔法矢;任意真族单次生效) + var spell_hits: Array = oc._hits_of(dr_hits, "spawn_spell") + if not spell_hits.is_empty() and not oc._feat_first_dig_used: + oc._feat_first_dig_used = true + var spell_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell", + "奥术矢", "board", int((spell_hits[0] as Dictionary).get("value", 0)), + DamageController.TARGET_FRONT_ALIVE, ri, evt.cell, "magic") + oc._submit(spell_t, false) + elif evt.kind == "mine": + # 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴), + # 标中不发(旗已随挖掘擦除、不入轮末判定基线)。 + # mine_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价: + # 减半收集 → 负向兑付〔带减半系数〕→ 反打开单 → 连击清零计数器。 + # 迭代 20 T1——发射前移+payload 扩 dug_under_flag〔破封一击〕;新增施加面: + # damage_reduce〔安全帽耗层减伤——value_counter 层值,一次清零〕/ heal〔血蜜〕/ + # blast_scaled〔陷阱爆发——雷伤×倍率全体敌〕/ bonus_delta〔血税〕/ reveal_tiles〔见血开窍〕) + var mine_power := int((snap as Dictionary).get("power", 0)) + var mh_hits := oc._fx_emit("mine_hit", {"cell": evt.cell, "round": ri, + "damage": mine_power, "dug_under_flag": bool(evt.get("dug_under_flag", false)), + "hp_ratio": int(round(100.0 * float(int(oc._enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(oc._enemy_system.hero_unit().get("max_hp", 1))))))}) + var halve_pct := 0 + var mine_reduce := 0 + for h in mh_hits: + match String(h.get("kind", "")): + "damage_mult": + halve_pct += int(h.get("value", 0)) + "damage_reduce": + if h.has("value_counter") and String(h.get("value_counter", "")) != "": + mine_reduce += oc._fx.counter(String(h.get("value_counter", ""))) # 安全帽——层值减免 + else: + mine_reduce += int(h.get("value", 0)) # 背水——定值减免 + "heal": + var hu_heal: Dictionary = oc._enemy_system.hero_unit() + hu_heal["hp"] = mini(int(hu_heal.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_heal.get("max_hp", 1)))) + "bonus_delta": + var axis := String(h.get("axis", "")) + if oc._fx_bonus_delta.has(axis): + oc._fx_bonus_delta[axis] += int(h.get("delta", 0)) + "blast_scaled": + var blast_v := maxi(int(oc.db.settlement.damage_floor), int(float(mine_power) * float(h.get("mult", 1)))) + var bs_t := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_blast_scaled", "陷阱爆发", "board", + blast_v, DamageController.TARGET_ALL_ALIVE, ri, evt.cell, "", 0, "", {}) + oc._submit(bs_t, false) + if mine_reduce > 0: + snap["power"] = maxi(0, mine_power - mine_reduce) # 减伤落快照(负向兑付按减后值——安全帽每层减 1) + for h in mh_hits: + if String(h.get("kind", "")) == "counter_add": + _apply_counter_hit(h) + if not oc._hits_of(mh_hits, "reveal_tiles").is_empty(): + oc._fx_enqueue_reveal() # 见血开窍——每颗雷一次揭示(授予段末统一兑付) + oc._grant_routed(oc._feat_yield_adjust(snap, false, 0, 0, -1), "negative", "mine_hit", evt.cell, ri, "", "", halve_pct) + for h in mh_hits: + if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "reflect": + var nct := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_counter", "惊巢反制·反弹", "board", + int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, evt.cell, "", 0, "", {}) + oc._submit(nct, false) + # 地形变异 兑付段(授予段尾——触发枚举 on_open:挖开即伤、chain 通道同口径;伤害单据案C 暂存到 + # 战斗段末兑付〔2026-10-06 用户裁〕;信息型 hide_number 不进结算——呈现层读取,见 board_view 数字遮蔽; + # 雷格恒无变异——布置段排除,双层叠罚不存在;插旗格直挖照吃变异伤害——地形后果同 mine_hit 口径, + # 非奖励不作废;吞噬流资源兑付〔荆棘编织/地脉补偿〕中伤害形态行同样经案C闸暂存,非伤害形态照常当场) + for evt in oc._facts_reveals: + var vids: Array = evt.get("variant_ids", []) + for vid in vids: + var vrow: Dictionary = oc.db.variation.row_by_id(String(vid)) + if String(vrow.get("effect", "")) != "flat_damage": + continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面) + # 地形变异伤害单据(伤害管线单据化批 2026-09-24)——无类型必中,下限保底在控制器管线内 + # 迭代 20 T1——damage_taken 发射前移(单据开立前):damage_reduce〔背水——hp_ratio when + # 面〕先收集再按减后值开单;counter_add〔保险丝/石肤〕+ ac_bonus〔石肤〕照常施加; + # grant_material〔荆棘/地脉〕移到单据提交后(与迁移前兑付值序一致——收益不因减伤变化) + var dt_amount := int(vrow.get("effect_value", 0)) + var dt_hits := oc._fx_emit("damage_taken", {"kind": "terrain", "amount": dt_amount, + "source": String(vid), "cell": evt.cell, "round": ri, "terrain_kind": String(vid), + "hp_ratio": int(round(100.0 * float(int(oc._enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(oc._enemy_system.hero_unit().get("max_hp", 1))))))}) + for h in dt_hits: + match String(h.get("kind", "")): + "damage_reduce": + dt_amount = maxi(0, dt_amount - int(h.get("value", 0))) + "ac_bonus": + oc._apply_ac_bonus(h) + "counter_add": + _apply_counter_hit(h) + var vt := oc._damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)), "board", \ + dt_amount, DamageController.TARGET_HERO, ri, evt.cell, "", 0, "", {"variant_id": String(vid)}) + oc._submit(vt, false) + for h in dt_hits: + if String(h.get("kind", "")) == "grant_material": + _apply_material_hit(h, evt.cell, ri) + for evt in oc._facts_flags: + _judge_flag(evt, ri) + # 战旗军规开单段(迭代 12 T3 标旗流——flag_judged 批后统一开单:轮末每面标中旗对最前存活敌 + # 结算 board_typed 必中伤害〔复用零新配方;「旗所在巢穴敌」简化为 default_target 最前存活敌—— + # 台账披露待追认〕;完美布防=本轮旗批全中(hit>0 且 miss==0)时旗伤 ×mult——全对推理奖励)。 + # 伤害单据案C 暂存战斗段末兑付。 + # flag_settle 发射+施加(内容数据驱动扩展批 T3——战旗 open_ticket + 完美布防 modify_ticket + # 改写窗口:旗伤基值×完美倍率后逐旗开单,与迁移前算序一致) + var fs_hits := 0 + var fs_misses := 0 + for fevt in oc._facts_flags: + if bool(fevt.get("hit", false)): + fs_hits += 1 + else: + fs_misses += 1 + var fs_fx := oc._fx_emit("flag_settle", {"flags_hit_all": fs_hits > 0 and fs_misses == 0, + "flags_count": fs_hits, "round": ri}) + var banner_hits: Array = [] + for h in fs_fx: + if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "attack": + banner_hits.append(h) + if not banner_hits.is_empty(): + var strike_value := int((banner_hits[0] as Dictionary).get("value", 0)) + var flag_mult := 1 + for h in fs_fx: + if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "flag_mult": + flag_mult *= maxi(1, int(h.get("value", 1))) + if fs_misses == 0 and fs_hits > 0 and flag_mult > 1: + strike_value *= flag_mult # 完美布防:同轮全中旗伤倍增(全对推理奖励) + for fevt in oc._facts_flags: + if not bool(fevt.get("hit", false)): + continue + var st_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_flag_strike", + "战旗军规", "board", strike_value, DamageController.TARGET_FRONT_ALIVE, ri, + fevt.cell, "normal") + oc._submit(st_t, false) + # round_end 发射+施加(内容数据驱动扩展批 T3——每轮结算收口:熔火之躯反打单开立〔open_ticket + # 即时执行〕+积层清零〔counter_add set〕;整备转蓄力由 finish_round_stop 承载——停手侧 AP 已在 + # request_stop 记账,转存决策=grant_ap(mode:transfer) 订阅定夺,无订阅走 RAP 转存缺省) + var re_fx := oc._fx_emit("round_end", {"round": ri}) + for h in re_fx: + if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "blast": + var mg_layers := oc._fx.counter(String(h.get("per", ""))) + var mg_total := int(h.get("value", 0)) * mg_layers + if mg_total > 0: + # 迭代 20 T9——source_class 中性名(旧件名 feat_magma_body 随熔火之躯退役;per-counter 反打通道保留泛用面) + var mg_t := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "fx_blast_counter", + "积层反噬 ×%d" % mg_layers, "board", mg_total, + DamageController.TARGET_ALL_ALIVE, ri, oc.minesweeper.hero_pos(), "", 0, "", {}) + oc._submit(mg_t, false) + for h in re_fx: + if String(h.get("kind", "")) == "counter_add": + _apply_counter_hit(h) + # 迭代 20 T1——round_end 新施加面:grant_gp〔爆破会计/收盘——escrow 战斗域缓冲,随 gp_dropped 上抛〕/ + # grant_ap mode:queue〔封印回响——返 1 AP 下轮,queue_ap_delta 通道与轮始加成同面〕 + for h in re_fx: + if String(h.get("kind", "")) == "grant_gp": + oc.escrow += int(h.get("value", 0)) + oc._emit_row("effect", "fx_grant_gp", Vector2i(-1, -1), ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": oc.escrow}, "") + elif String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue": + oc.action_economy.queue_ap_delta(int(h.get("value", 0))) + # 转存率入参(迭代 22 秘宝批 T3——储备罐 rap_transfer_mult magnitude=目标转存率百分比 125=1.25:1; + # 未装配=100 与旧 1:1 逐位一致) + var rap_pct: int = oc.attune_param_sum("rap_transfer_mult") + oc.action_economy.finish_round_stop(oc._fx != null and oc._fx.has_effect_on("round_end", "grant_ap", "transfer"), + rap_pct if rap_pct > 0 else 100) + if oc._facts_path == "cleared": + oc._grant_clear_reward(ri) + oc._flush_pending_reveals(ri) # 迭代 20 T1——揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入) + oc._damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界) + + +# 旗判定(迭代 12 T3 标旗流本体随段迁入——原编排器 _judge_flag 逐字搬运,仅宿主引用加前缀; +# 唯一调用面=本段 run 的 facts_flags 循环)。 +func _judge_flag(evt: Dictionary, ri: int) -> void: + var cell: Vector2i = evt.cell + if evt.hit: + if oc._flag_consumed.has(cell): + return # 去重跳过(资格以雷格实例为键——排雷玩法 诚实重复上报的对侧);零明细行 + oc._flag_consumed[cell] = true + oc._perfect_hit_true += 1 + # flag_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:殉爆〔T4 秘宝面〕→ + # 净化改写 → 图谱额外资源 → 基线兑付) + var fh_hits := oc._fx_emit("flag_hit", {"cell": cell, "round": ri, + "terrain_kind": oc._harmful_adjacent(cell)}) + # 标中殉爆(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:订阅施加——行 triggers open_ticket + # channel blast value_from magnitude;每标中一面旗全体存活敌吃 N 群伤,无类型走 blast 通道。 + # 迭代 20 T1——封印爆裂〔迁入专长〕同面消费:行 triggers open_ticket channel blast) + for h in oc._hits_of(fh_hits, "open_ticket"): + if String(h.get("channel", "")) != "blast": + continue + var fbt := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_flag_burst", "封印爆裂", "board", + int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, "highlight", {}) + oc._submit(fbt, false) + # 迭代 20 T1——标中新施加面:counter_add〔血税 标中+1 攻——bonus_delta atk 轴〕/ + # ac_bonus〔封印桩 本轮 AC+N〕/ preview_area〔示警烟火 3×3 资源类型预告〕/ + # remove_mines〔封印匠 每第 3 次该危险格中立移除——mod_eq 闸在行 when〕。 + # (净化工兵 rewrite_board 分支随老 27 件退役摘除——效果类型同批除名,T1 落法 commit 留痕) + for h in fh_hits: + match String(h.get("kind", "")): + "counter_add": + _apply_counter_hit(h) + "ac_bonus": + oc._apply_ac_bonus(h) + "bonus_delta": + var axis_f := String(h.get("axis", "")) + if oc._fx_bonus_delta.has(axis_f): + oc._fx_bonus_delta[axis_f] += int(h.get("delta", 0)) + "remove_mines": + var removed: Array = oc.minesweeper.remove_mines_in([cell]) + if not removed.is_empty(): + oc._emit_row("mine_neutralized", "fx_seal_smith", cell, ri, "封印匠·危险格中立移除", 0, {}, "") + "preview_area": + var board_p: BoardData = oc.minesweeper.current_board() + var types: Array = [] + for n in board_p.neighbors8(cell): + var n_snap = oc._reward_economy.query_full_snapshot(n) + if n_snap is Dictionary and not types.has(String((n_snap as Dictionary).get("cell_type", ""))): + types.append(String((n_snap as Dictionary).get("cell_type", ""))) + if oc.treasure_inventory != null and not types.is_empty(): + oc.treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell)) + oc._emit_row("fx_preview_area", "", cell, ri, "示警烟火·周围资源类型", 0, {"types": types}, "") + # 雷区图谱(迭代 12 T3 标旗流——标中雷额外获得一份格面资源:插旗开始产出对标开格; + # 复用 grant 单点,额外兑付走同一 _grant_routed 正向路由) + for h in fh_hits: + if String(h.get("kind", "")) == "grant_material": + _apply_material_hit(h, cell, ri, "flag_hit") + var snap = oc._reward_economy.grant(cell) + if not (snap is Dictionary): + return + oc._grant_routed(snap, "positive", "flag_hit", cell, ri, "flag_hit") + else: + oc._perfect_hit_false += 1 + if oc._flag_miss_seen.has(cell): + return # 每格实例一次去重(归因行与负向兑付同键——防逐轮惩罚轰炸) + oc._flag_miss_seen[cell] = true + # 完美保险(迭代 20 T4——本战下一次标错不破坏完美资格:_perfect_hit_false 回退=本次标错 + # 对完美谓词无痕;只保一次/件,节点级用完即止) + if oc._fx_insurance > 0: + oc._fx_insurance -= 1 + oc._perfect_hit_false -= 1 + oc._emit_row("insurance_used", "", cell, ri, "完美保险·标错豁免", 0, {"left": oc._fx_insurance}, "") + var hl := "" + if oc._perfect_hit_false == 1: + hl = "perfect_broken" # 本板完美谓词已破的归因锚 + oc._emit_row("flag_miss", "flag_miss", cell, ri, "标旗未中", 0, {}, hl) + # 危险直觉·守挂点(迭代 21 T3——flag_missed 发射:契约 12 触发主语=「当回合发生的标错 + # 动作」——去重闸=新旧标错判别器〔后续轮重判旧旗被闸挡回零触发〕;双条件②「结算时旗仍在 + # 场」由发射面既有过滤承载〔被挖开的旗已随挖掘擦除零兑付〕;grant_ap mode:queue 下一轮 + # 开轮兑现——末轮判出无下一轮=合法作废自动成立〔编排器每战新建,队列不跨战〕;限次=合成行 + # adventure 域计数器闸) + for h in oc._fx_emit("flag_missed", {"cell": cell, "round": ri}): + if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue": + oc.action_economy.queue_ap_delta(int(h.get("value", 0))) + var snap = oc._reward_economy.grant(cell) + if not (snap is Dictionary): + return + oc._grant_routed(snap, "negative", "flag_miss", cell, ri, "") + + +# counter_add 三行组单点(原五位分支本体逐字收拢——kind 分派随段归位 P1-1) +func _apply_counter_hit(h: Dictionary) -> void: + var set_v = h.get("set", null) + oc._fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) + + +# grant_material 兑付块单点(原三位分支本体逐字收拢——source_class 两态 safe_cell/flag_hit 由参承载) +func _apply_material_hit(h: Dictionary, cell: Vector2i, ri: int, source_class: String = "safe_cell") -> void: + var g_snap = oc._reward_economy.grant(cell) + if g_snap is Dictionary: + oc._grant_routed(g_snap, "positive", source_class, cell, ri, "", String(h.get("label", ""))) diff --git a/game/core/settlement/grant_segment.gd.uid b/game/core/settlement/grant_segment.gd.uid new file mode 100644 index 00000000..2e82a6bd --- /dev/null +++ b/game/core/settlement/grant_segment.gd.uid @@ -0,0 +1 @@ +uid://b0f3h2g13d04g diff --git a/game/core/settlement/verdict_segment.gd b/game/core/settlement/verdict_segment.gd new file mode 100644 index 00000000..5c5bd287 --- /dev/null +++ b/game/core/settlement/verdict_segment.gd @@ -0,0 +1,33 @@ +class_name VerdictSegment +extends RefCounted +# ⑤ 判定段 Segment(迭代 27 T2——P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。 +# 职责:防御性优先级 hero 先判(双死结构不可达的纵深防御)→ 全量状态快照(despawn 前 +# 组装——终局含 result)→ 终局行+收束分派 / 下一轮出口分发。 +# 返回值 = 裁定("victory"/"defeat"/"continue")——宿主 _settle 据此保持旧早退语义 +#(终局不回置 _settling,_battle_over 接管);本段零算法改动(🔴 零行为变更契约)。 + +var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) 宿主 + + +func _init(host) -> void: + oc = host + + +func run(ri: int) -> String: + var verdict := "continue" + if int(oc._enemy_system.hero_unit().get("hp", 0)) <= 0: + verdict = "defeat" + elif oc._enemy_system.query_alive("enemy").is_empty(): + verdict = "victory" + oc._emit_settlement_snapshot(verdict) # 全量状态快照(despawn 前组装——终局含 result) + if verdict == "defeat": + oc._emit_row("verdict", "", Vector2i(-1, -1), ri, "战斗失败", 0, {}, "") + oc._battle_over("defeat") + return verdict + if verdict == "victory": + oc._emit_row("verdict", "", Vector2i(-1, -1), ri, "战斗胜利", 1, {}, "") + oc._battle_over("victory") + return verdict + oc._emit_row("verdict", "", Vector2i(-1, -1), ri, "下一轮次", 0, {}, "") + oc._exit_distribution() + return verdict diff --git a/game/core/settlement/verdict_segment.gd.uid b/game/core/settlement/verdict_segment.gd.uid new file mode 100644 index 00000000..e44c4f30 --- /dev/null +++ b/game/core/settlement/verdict_segment.gd.uid @@ -0,0 +1 @@ +uid://i0dhvekibaam diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index c0aa8aaa..d68b8282 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -5329,7 +5329,7 @@ func _smoke_tree() -> void: # ---- 天赋树六键行为冒烟(迭代 21 T3——V10:开局姿态窗/危险直觉限次/财富之道聚散/守侧失误补偿; -# 引擎直构 + 编排授予段直注(_judge_flag=结算授予段旗判定单口))---- +# 引擎直构 + 编排授予段直注(_grant_seg._judge_flag=结算授予段旗判定单口——迭代27 T2 随段迁址))---- func _smoke_tree_rules() -> void: print("--- 天赋树六键行为(V10:窗/限次/聚散/守侧)---") # 开局姿态组:每关首轮窗(battle_index 事实——引擎直构) @@ -5362,7 +5362,7 @@ func _smoke_tree_rules() -> void: fz2.counters_seed(exported) check((fz2.dispatch("mine_hit", {}, Callable()) as Array).is_empty(), "锋:限次跨战持久(adventure 域 counters_export/seed——断点保留同源,不新增存档域不升 v7)") - # 守侧五场景(契约 12):编排 rig——begin_battle 携 sense_guard,授予段 _judge_flag 直注 + # 守侧五场景(契约 12):编排 rig——begin_battle 携 sense_guard,授予段 _grant_seg._judge_flag 直注 var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777001)) check(orch.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, @@ -5379,16 +5379,16 @@ func _smoke_tree_rules() -> void: check(safe.x >= 0, "守侧 rig:安全格定位(非雷格=标错负样本)") check(int(orch.action_economy.pending_ap_delta()) == 0, "守侧 rig:首轮后 queue 干净(开局 posture queue 已兑现)") # 场景①:当回合新插旗、结算判标错且旗仍在场 → queue +1(本轮 AP 零变化=queue 承载、下轮 notify_round_start 兑现) - orch._judge_flag({"cell": safe, "hit": false}, 1) + orch._grant_seg._judge_flag({"cell": safe, "hit": false}, 1) check(int(orch.action_economy.pending_ap_delta()) == 1, "守①:当回合标错且旗在场 → 下轮 AP+1(queue 排队——本轮 AP 逐位零变化)") # 场景③④:之前回合标错重判(旗在场状态)与同格重复——去重闸挡回,最多返还一次 - orch._judge_flag({"cell": safe, "hit": false}, 2) - orch._judge_flag({"cell": safe, "hit": false}, 3) + orch._grant_seg._judge_flag({"cell": safe, "hit": false}, 2) + orch._grant_seg._judge_flag({"cell": safe, "hit": false}, 3) check(int(orch.action_economy.pending_ap_delta()) == 1, "守③④:旧旗重判零触发+同格最多一次(_flag_miss_seen 去重闸=「当回合标错动作」判别器)") # 场景②负样本:标中旗(hit=true 正向兑付路径)零触发 - orch._judge_flag({"cell": safe, "hit": true}, 4) + orch._grant_seg._judge_flag({"cell": safe, "hit": true}, 4) check(int(orch.action_economy.pending_ap_delta()) == 1, "守②负样本:标中/被挖旗零触发(标错事件只在 miss 分支发射——发射面过滤承载)") # 场景⑤:末轮判出→无下一轮=queue 不兑现(编排器每战新建——队列实例随战亡,合法作废态) var orch2 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777002)) diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 38a09c7e..16ad2cef 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -52,6 +52,7 @@ func _suites() -> Array: {"path": "res://tests/unit/core/tree_requires_level_test.gd", "expect": 0}, # R1 机制位批 2026-10-13——requires_level 等级阈值前置(schema 四拒一过+判定三态+投影两态) {"path": "res://tests/unit/core/pool_case_d_test.gd", "expect": 0}, # 迭代 20 T2——池结构案D 行为(V6 令牌三态/V7 曝光槽 2+1 回退/V8 概率上限解耦提前退役) {"path": "res://tests/unit/core/feat_vouchers_test.gd", "expect": 0}, # 迭代 20 T4——凭证五件+留任二件(V9 路由逐件/F-1 存量账/V10 留任路由+AP 属性轴) + {"path": "res://tests/unit/core/settle_segments_test.gd", "expect": 0}, # 迭代 27 T2——结算五段 Segment(V4:直接 new/编排器接线/真实一轮走段序) ] diff --git a/game/tests/unit/core/settle_segments_test.gd b/game/tests/unit/core/settle_segments_test.gd new file mode 100644 index 00000000..52bb3cbc --- /dev/null +++ b/game/tests/unit/core/settle_segments_test.gd @@ -0,0 +1,55 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# 结算五段 Segment 单测(迭代 27 T2——需求卡片 V4 验收条目:Segment 类直接 new() + 编排器段序 +# 接线 + 真实一轮结算走段序出裁定/行)。段体逐位一致性由 parity_anchor 三变体对拍承载(V1 核心), +# 本测兜段类可独立实例化与编排器装配面。 +# 运行:godot --headless --path game --script res://tests/unit/core/settle_segments_test.gd(expect 0) +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") + + +func _init() -> void: + var db = load("res://core/config_db.gd").new() + if not db.load_tables("res://data/tables", false): + print("[settle_segments_test] FAIL — 数值表加载失败") + quit(1) + return + + # V4-1 段类直接 new()(构造收宿主引用——五类同构) + var oc = load("res://core/battle_orchestrator.gd").new(db, load("res://core/rng_service.gd").new()) + Kit.check_quiet(load("res://core/settlement/grant_segment.gd").new(oc) != null, "V4 GrantSegment 直接 new()〔迭代27 T2〕") + Kit.check_quiet(load("res://core/settlement/aura_segment.gd").new(oc) != null, "V4 AuraSegment 直接 new()〔迭代27 T2〕") + Kit.check_quiet(load("res://core/settlement/combat_segment.gd").new(oc) != null, "V4 CombatSegment 直接 new()〔迭代27 T2〕") + Kit.check_quiet(load("res://core/settlement/decay_segment.gd").new(oc) != null, "V4 DecaySegment 直接 new()〔迭代27 T2〕") + Kit.check_quiet(load("res://core/settlement/verdict_segment.gd").new(oc) != null, "V4 VerdictSegment 直接 new()〔迭代27 T2〕") + + # V4-2 编排器 _init 接线(段持宿主引用;段序编排面在位) + Kit.check_quiet(oc._grant_seg != null and oc._grant_seg.oc == oc, "V4 编排器接线·授予段(宿主回指)〔迭代27 T2〕") + Kit.check_quiet(oc._aura_seg != null and oc._aura_seg.oc == oc, "V4 编排器接线·增益/减益段〔迭代27 T2〕") + Kit.check_quiet(oc._combat_seg != null and oc._combat_seg.oc == oc, "V4 编排器接线·战斗段〔迭代27 T2〕") + Kit.check_quiet(oc._decay_seg != null and oc._decay_seg.oc == oc, "V4 编排器接线·衰减段〔迭代27 T2〕") + Kit.check_quiet(oc._verdict_seg != null and oc._verdict_seg.oc == oc, "V4 编排器接线·判定段〔迭代27 T2〕") + Kit.check_quiet(oc._grant_seg._apply_counter_hit({"counter_id": "dig_combo", "value": 1}) == null + and oc._fx.counter("dig_combo") == 1, "V4 counter_add 助手单点(原五位三行组收拢后行为等价)〔迭代27 T2〕") + + # V4-3 真实一轮结算走段序(begin_battle→停手→_maybe_settle→五段→裁定行) + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(4242) + var settlement = load("res://core/battle_orchestrator.gd").new(db, rng) + var rows: Array = [] + settlement.step_row.connect(func(row): rows.append(row)) + var result := {"r": ""} + settlement.battle_finished.connect(func(payload): result.r = payload.get("result", "")) + var enc := {"level_index": 1, "node_depth": 1, "hero_hp": int(db.hero.max_hp)} + Kit.check_quiet(settlement.begin_battle(enc), "V4 前置:begin_battle 受理") + # 停手触发轮末结算(stop 经批次提交——_maybe_settle 在 WAITING 相位放行 _settle 段序) + var outcomes: Array = settlement.minesweeper.settle_batch([{"action": "stop"}]) + Kit.check_quiet(not outcomes.is_empty(), "V4 前置:停手受理") + Kit.check_quiet(rows.any(func(row): return String(row.get("type", "")) == "verdict"), + "V4 段序结算出判定行(stop→WAITING→五段→verdict)〔迭代27 T2〕") + Kit.check_quiet(rows.any(func(row): return String(row.get("type", "")) == "decay"), + "V4 衰减段行在流(段序④恒执行)〔迭代27 T2〕") + Kit.check_quiet(String(result.r) == "" or String(result.r) == "victory" or String(result.r) == "defeat", + "V4 裁定面合法(continue 静默/终局经 battle_finished)〔迭代27 T2〕") + + quit(Kit.finish("settle_segments")) diff --git a/game/tests/unit/core/settle_segments_test.gd.uid b/game/tests/unit/core/settle_segments_test.gd.uid new file mode 100644 index 00000000..afc0b06b --- /dev/null +++ b/game/tests/unit/core/settle_segments_test.gd.uid @@ -0,0 +1 @@ +uid://blthtn45ikah6