diff --git a/game/core/action_economy.gd b/game/core/action_economy.gd index 5a49f05b..8fc13919 100644 --- a/game/core/action_economy.gd +++ b/game/core/action_economy.gd @@ -93,11 +93,29 @@ func request_dig(chain_size: int, clears_map: bool) -> bool: _reject("phase") return false var cost := _cost("dig") - # dig_paid 发射+施加(付费开掘发起、动作经济闸收对价前: + # dig_paid 发射+施加(付费开掘发起、动作经济闸收对价前——施加单点 _apply_dig_paid_hits): # counter_spend 命中=尝试加收对价并积计数;资源不足走 fallback/耗尽即止=只付常规基础对价。 - # counter_add〔双管镐 付费挖格计数〕/ grant_ap〔每第 2 次返 1 AP——对价减免, - # mod_eq 闸在行 when〕同面施加) + var apply_res := _apply_dig_paid_hits(cost) + cost = int(apply_res["cost"]) + if _ap < cost: + _reject("resource_face") + return false # 拒绝轮零入池(矿脉储能只在扣费受理后兑付——C2 成功才登记同款纪律) + if int(apply_res["refund"]) > 0: + cost = maxi(0, cost - int(apply_res["refund"])) # 返还=对价减免(净费不低于 0——免费开格不产生负对价) + var accepted := _spend_cost_and_arbitrate(cost, clears_map) + if accepted and int(apply_res["rap_gain"]) > 0: + grant_reserve_ap(int(apply_res["rap_gain"])) # 每次受理的付费挖格产 RAP(一次性入池——每动作恰一挖,连锁不二次) + return accepted + + +# dig_paid 命中施加单点(迭代 46 卡A T3 自 request_dig 迁出——分支本体逐字保留: +# counter_spend 加收对价并积计数/counter_add 施加〔双管镐 付费挖格计数〕/ +# grant_ap 对价减免〔每第 2 次返 1 AP——mod_eq 闸在行 when〕/grant_reserve_ap 矿脉储能累计; +# 命中序=声明序不变。返回 {cost, refund, rap_gain} 交受理面消费) +func _apply_dig_paid_hits(base_cost: int) -> Dictionary: + var cost := base_cost var dig_refund := 0 + var rap_gain := 0 if _fx != null: for h in _fx.dispatch(_ctx, "dig_paid", {"ap_cost": cost, "ap_surplus": _ap - cost}): if String(h.get("kind", "")) == "counter_spend": @@ -110,12 +128,9 @@ func request_dig(chain_size: int, clears_map: bool) -> bool: EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) elif String(h.get("kind", "")) == "grant_ap": dig_refund += int(h.get("value", 0)) - if _ap < cost: - _reject("resource_face") - return false - if dig_refund > 0: - cost = maxi(0, cost - dig_refund) # 返还=对价减免(净费不低于 0——免费开格不产生负对价) - return _spend_cost_and_arbitrate(cost, clears_map) + elif String(h.get("kind", "")) == "grant_reserve_ap": + rap_gain += int(h.get("value", 0)) + return {"cost": cost, "refund": dig_refund, "rap_gain": rap_gain} func _spend_cost_and_arbitrate(cost: int, clears_map: bool) -> bool: @@ -196,13 +211,15 @@ func finish_round_stop(transfer: bool, transfer_pct: int = 100) -> void: # 轮内刷新:唯一入口 = # Active 相位——仅扣 RAP,相位与 AP 均不动。刷新不打断轮次、不触发结算;换图编排归 战斗结算 # (retire + install 立即执行,玩家同轮继续挖新图)。决策主体 = 玩家(经 HUD框架发起)纪律不变。 -func request_refresh() -> bool: +# cost_override=折后价入参(迭代 46 卡A T3——BattleRefreshFlow 折价读数传入;缺省 -1=表基线逐位不变) +func request_refresh(cost_override: int = -1) -> bool: if _phase != Phase.ACTIVE: _reject("phase") return false - if _rap < db.run.action_economy.rap_refresh_cost: + var cost := int(db.run.action_economy.rap_refresh_cost) if cost_override < 0 else cost_override + if _rap < cost: return false # EC13 资源不足拒绝——合法相位内资源拒绝,非编排错误 - _rap -= db.run.action_economy.rap_refresh_cost + _rap -= cost return true diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 94b29869..026066a9 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -31,8 +31,9 @@ signal settlement_snapshot(snap: Dictionary) ## 每结算末尾的全量状态 const SOURCE_EXEMPT_TYPES := ["decay", "verdict"] # 行来源断言豁免集合(来源类别层)——事实来源是单位 / 清图而非棋盘格; # fx_reveal=专长揭示掷格(落空行无真实格); -# fx_grant_gp=专长·GP 入账(钱包面而非棋盘面,行无格子可指) -const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "fx_reveal", "fx_grant_gp"] +# fx_grant_gp=专长·GP 入账(钱包面而非棋盘面,行无格子可指); +# fx_board_refresh=成功刷新事实族(迭代 46 卡A——崩壁冲击群伤行源=换图动作而非棋盘格) +const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "fx_reveal", "fx_grant_gp", "fx_board_refresh"] var _facts: BattleFacts # 战斗事实件(battle_facts.gd——事实族 15 成员单点) var _rounds: RoundCycle # 轮循环驱动件(settlement/round_cycle.gd——五段持有/段序/重入闸/摇号) @@ -596,26 +597,17 @@ func _exit_distribution() -> void: round_started.emit(round_index) -# 呈现层查询面(HUD 刷新按钮可用性)——挖掘期唯一通道 +# 呈现层查询面(HUD 刷新按钮可用性)——挖掘期唯一通道(折价成本同源:判据归 BattleRefreshFlow) func can_refresh() -> bool: - return battle_result == "" \ - and _facts.refresh_used < int(db.run.action_economy.refresh_cap_per_round) \ - and action_economy.phase == action_economy.PHASE_ACTIVE \ - and action_economy.rap >= int(db.run.action_economy.rap_refresh_cost) + return BattleRefreshFlow.can_refresh(self) # 玩家经 HUD框架发起、战斗结算 代理调用(禁止 RAP 满自动刷)。 -# 挖掘期唯一通道:Active 相位立即换图 -# 继续挖(RAP -8;AP 保留、相位不动、不触发结算);非挖掘期请求一律拒绝——非清图轮恒同图。 +# 挖掘期唯一通道:Active 相位立即换图继续挖(RAP 按折后价扣;AP 保留、相位不动、不触发结算); +# 非挖掘期请求一律拒绝——非清图轮恒同图。刷新门/折价扣费/换图/成功事实归 BattleRefreshFlow +# (迭代 46 卡A T3 拆出——编排器入口与唯一通道不变,C10 结构预算只降不升)。 func player_request_refresh() -> bool: - if not can_refresh(): - return false - if not action_economy.request_refresh(): - return false - _facts.refresh_used += 1 - _reward_economy.retire_mapping() # 旧图批次退役(地图战斗资源 Ready→retired;E6 新批重置前提) - minesweeper.retire_board(true) # 旧板退役(轮内通道) - return _install_and_reset(true) # 当场换图——失败即编排违例 fail-fast + return BattleRefreshFlow.execute(self) func _battle_over(result: String) -> void: diff --git a/game/core/reward_grant.gd b/game/core/reward_grant.gd index 3267c2ac..ae265754 100644 --- a/game/core/reward_grant.gd +++ b/game/core/reward_grant.gd @@ -365,6 +365,12 @@ func outcome_of(ticket_id: int) -> String: return "redeemed" if ticket_id < 0 else damage.outcome(ticket_id) +# 延迟单据入队(迭代 46 卡A T4——成功刷新的崩壁冲击群伤单走案C 同队列:授予段闸外唯一入口, +# 战斗段末 _board_deferred_settle 按开单序补提交统一结账;类变量零增员——复用 defer.queue) +func defer_ticket(t: Dictionary) -> void: + (defer.queue as Array).append(t) + + # 主角直伤单据(反噬/治疗包负向——无类型必中,下限保底在控制器管线内) func _submit_flat_hero(display: String, power: int, att: Dictionary, sync: bool) -> String: var t := damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, String(att.get("source_class", "")), display, diff --git a/game/core/settlement/battle_refresh_flow.gd b/game/core/settlement/battle_refresh_flow.gd new file mode 100644 index 00000000..0f6a30e8 --- /dev/null +++ b/game/core/settlement/battle_refresh_flow.gd @@ -0,0 +1,43 @@ +class_name BattleRefreshFlow +extends RefCounted +## 换图刷新流程件(迭代 46 卡A T3/T4 自编排器拆出):刷新可用性门、折价扣费、换图执行与成功事实登记。 +## 拆分边界(需求卡片 C13):本件只负责可用性、扣费、换图与成功事实登记——七件效果的算法留在 +## 效果引擎、授予段与伤害控制器。调用路径=呈现层→编排器单行转发→本件→行动经济/装板壳/授予段。 +## 反自动刷新(C11):只经玩家入口发起,禁 RAP 满自动刷——编排器公开面单行转发,无自动调用点。 +## 成功序钉死(T4):折价扣 RAP→refresh_used+1→旧资源退役→旧板退役→装新板→授予段成功事实发射; +## 装板失败返回 false 且零效果——成功事实只在新板就位后发射。 + + +## 折价成本读数(refresh_request 命中省力周转 apply_discount——只认 kind==apply_discount 且 +## mode==refresh,取声明序第一命中,floor(base×cost_mult/100);无命中=表基线逐位不变。 +## can_refresh 与 execute 同源取数〔V3——can 与执行同源判断〕;折扣只改变门槛与扣费, +## 不改变拒绝判据〔C2——每轮上限与相位判据照旧〕) +static func _effective_refresh_cost(oc: BattleOrchestrator) -> int: + var base := int(oc.db.run.action_economy.rap_refresh_cost) + for h in oc.fx_host().fx_emit("refresh_request", {"mode": "refresh", "base": base}): + if String(h.get("kind", "")) == "apply_discount" and String(h.get("mode", "")) == "refresh": + return int(floor(float(base) * float(int(h.get("cost_mult", 100))) / 100.0)) + return base + + +## 可用性门(HUD 刷新按钮同源判据):战斗未终局、每轮上限未满、Active 相位、RAP 抵折后价。 +static func can_refresh(oc: BattleOrchestrator) -> bool: + return oc.battle_result == "" \ + and oc._facts.refresh_used < int(oc.db.run.action_economy.refresh_cap_per_round) \ + and oc.action_economy.phase == oc.action_economy.PHASE_ACTIVE \ + and oc.action_economy.rap >= _effective_refresh_cost(oc) + + +## 执行(玩家请求刷新唯一通道体):扣费受理后继续;扣费拒绝/门拒零效果零事实。 +static func execute(oc: BattleOrchestrator) -> bool: + if not can_refresh(oc): + return false + if not oc.action_economy.request_refresh(_effective_refresh_cost(oc)): + return false + oc._facts.refresh_used += 1 + oc._reward_economy.retire_mapping() # 旧图批次退役(地图战斗资源 Ready→retired;E6 新批重置前提) + oc.minesweeper.retire_board(true) # 旧板退役(轮内通道) + if not oc._install_and_reset(true): # 当场换图——失败即编排违例 fail-fast(零成功事实) + return false + oc._rounds._grant_seg.apply_board_refreshed(oc.round_index) # 成功事实:新板就位后一次发射 + return true diff --git a/game/core/settlement/grant_segment.gd b/game/core/settlement/grant_segment.gd index 88df43de..3b5f43bb 100644 --- a/game/core/settlement/grant_segment.gd +++ b/game/core/settlement/grant_segment.gd @@ -33,6 +33,27 @@ func run(ri: int) -> void: _round_end_apply(ri) +# 成功刷新事实发射+施加(迭代 46 卡A T4——BattleRefreshFlow 装新板后单点调用,每成功刷新恰一次; +# 拒绝刷新零事实〔C2〕。命中消费三面:counter_add→既有单点落账〔蓄力周转/石层缓冲/坑道小憩 计数〕; +# grant_gp→战斗内缓冲 escrow 入账+fx_grant_gp 行〔矿道红利——不绑清图奖励,C7〕; +# open_ticket channel=blast→崩壁冲击群伤单延迟入队〔案C 同队列,战斗段末按存活目标结账〕) +func apply_board_refreshed(ri: int) -> void: + var br_hits := fx_host.fx_emit("board_refreshed", {"round": ri}) + for h in br_hits: + if String(h.get("kind", "")) == "counter_add": + _apply_counter_hit(h) + for h in br_hits: + if String(h.get("kind", "")) == "grant_gp": + reward_grant.escrow += int(h.get("value", 0)) + oc._emit_row({"type": "effect", "source_class": "fx_grant_gp", "origin_cell": Vector2i(-1, -1)}, ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": reward_grant.escrow}) + for h in br_hits: + if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "blast": + var bb_t := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "fx_board_refresh", "崩壁冲击", + {"channel": "board", "base_value": int(h.get("value", 0)), "target_rule": DamageController.TARGET_ALL_ALIVE, + "ri": ri, "origin_cell": Vector2i(-1, -1)}) + reward_grant.defer_ticket(bb_t) + + # 开格/踩雷兑付批(轮 facts_reveals 资源事件序:safe 正向乘区兑付 / mine_hit 减半负向兑付) func _reveal_batch(ri: int) -> void: for evt in oc._facts.reveals: diff --git a/game/data/tables/feats.tres b/game/data/tables/feats.tres index 4cb5cd5c..eb4cebaf 100644 --- a/game/data/tables/feats.tres +++ b/game/data/tables/feats.tres @@ -869,5 +869,38 @@ rows = [ } } ] + }, + { + "feat_id": "vein_reserve", + "display_name": "矿脉储能", + "rarity": "excellent", + "cap": 1, + "desc": "挖格储能:每次受理的付费挖格 +1 储备点(RAP)", + "triggers": [ + { + "on": "dig_paid", + "do": { + "kind": "grant_reserve_ap", + "value": 1 + } + } + ] + }, + { + "feat_id": "easy_swap", + "display_name": "省力周转", + "rarity": "excellent", + "cap": 1, + "desc": "刷新半价:弃图刷新成本为基线 50%", + "triggers": [ + { + "on": "refresh_request", + "do": { + "kind": "apply_discount", + "mode": "refresh", + "cost_mult": 50 + } + } + ] } ] diff --git a/game/tests/unit/battle_settlement/refresh_feats_test.gd b/game/tests/unit/battle_settlement/refresh_feats_test.gd index f746a4a5..aa313d22 100644 --- a/game/tests/unit/battle_settlement/refresh_feats_test.gd +++ b/game/tests/unit/battle_settlement/refresh_feats_test.gd @@ -28,6 +28,10 @@ func _init() -> void: _install_dt_probe() test_all_hero_damage_paths_emit_once() test_no_duplicate_damage_taken_event() + test_reserve_on_accepted_dig() + test_refresh_discount_gate_and_charge() + test_double_base_stack() + test_no_feats_baseline() quit(Kit.finish("refresh_feats_test")) @@ -232,3 +236,113 @@ func test_no_duplicate_damage_taken_event() -> void: orch2.minesweeper.settle_batch([{"action": "stop"}]) Kit.check("ND 地形路径零重复(行 %d=计数 %d——context 标记闸失效即计数翻倍)〔换图系批 V5〕" % [rows2.size(), _probe_count(orch2)], rows2.size() >= 1 and _probe_count(orch2) == rows2.size()) + + +# ---- V2/V3 两件基座(迭代 46 卡A T3)---- + +# 表驱动停手攒 RAP(每轮 1:1 转存——无转存率订阅缺省) +func _bank_rap(orch, rounds_n: int) -> void: + for _i in rounds_n: + orch.minesweeper.settle_batch([{"action": "stop"}]) + + +# AutoPlayer 单次付费挖格(真实挖掘入口——移位+dig 一动作) +func _auto_dig(orch) -> void: + var plan = AutoPlayer.choose_dig(orch.minesweeper) + if not plan.is_empty(): + if plan.hero_cell != orch.minesweeper.hero_pos(): + orch.minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}]) + orch.minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) + + +# V2 矿脉储能:受理的付费挖格 RAP 恰 +1、AP 按表扣、板资源不变 +func test_reserve_on_accepted_dig() -> void: + print("--- V2 矿脉储能:付费挖格产 RAP ---") + var orch = _orch(46021) + Kit.check("V2 前置:战斗受理", orch.begin_battle(_enc(["vein_reserve"]))) + _bank_rap(orch, 1) + var rap_before: int = orch.action_economy.rap + var ap_before: int = orch.action_economy.ap + _auto_dig(orch) + var dig_cost: int = int(_db.run.action_economy.dig_ap_cost) + Kit.check("V2 受理付费挖格 RAP 恰 +1(%d→%d)〔换图系批 V2〕" % [rap_before, orch.action_economy.rap], + orch.action_economy.rap == rap_before + 1) + Kit.check("V2 AP 按表扣(%d→%d,dig 价 %d)〔换图系批 V2〕" % [ap_before, orch.action_economy.ap, dig_cost], + orch.action_economy.ap == ap_before - dig_cost) + Kit.check("V2 受理记账闭合:AP 耗尽轮 RAP 增量恰等于受理挖数(AP 消耗数——每受理恰 +1,未受理零入池)〔换图系批 V2〕", + _drain_round_rap_accounting(orch)) + + +# V2 辅证:挖到 AP 耗尽——RAP 增量 == 受理挖数(每受理恰 +1;移位耗 AP 不入账故按挖计数) +func _drain_round_rap_accounting(orch) -> bool: + var rap0: int = orch.action_economy.rap + var digs := 0 + var guard := 24 + while orch.action_economy.phase == "Active" and guard > 0: + guard -= 1 + var plan = AutoPlayer.choose_dig(orch.minesweeper) + if plan.is_empty(): + break + if plan.hero_cell != orch.minesweeper.hero_pos(): + orch.minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}]) + if orch.action_economy.phase != "Active": + break # 移位耗尽 AP 收轮——本轮不再有受理挖 + orch.minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) + digs += 1 + return digs > 0 and orch.action_economy.rap - rap0 == digs + + +# V3 省力周转:折价门槛与扣费(can 与执行同源;base 8→扣 4) +func test_refresh_discount_gate_and_charge() -> void: + print("--- V3 省力周转:折价门槛与扣费 ---") + var orch = _orch(46022) + Kit.check("V3 前置:战斗受理", orch.begin_battle(_enc(["easy_swap"]))) + _bank_rap(orch, 1) + var base_cost: int = int(_db.run.action_economy.rap_refresh_cost) + var discounted: int = int(floor(float(base_cost) * 50.0 / 100.0)) + var rap0: int = orch.action_economy.rap + Kit.check("V3 门同源:can_refresh 与折后价同源(RAP %d ≥ 折后 %d)〔换图系批 V3〕" % [rap0, discounted], + orch.can_refresh()) + Kit.check("V3 第 1 次刷新受理", orch.player_request_refresh()) + Kit.check("V3 折后单次扣费(%d→%d,−%d)〔换图系批 V3〕" % [rap0, orch.action_economy.rap, discounted], + orch.action_economy.rap == rap0 - discounted) + Kit.check("V3 第 2 次刷新受理(RAP=%d)" % orch.action_economy.rap, orch.player_request_refresh()) + Kit.check("V3 两次折后扣费累计(RAP=%d=%d−2×%d)〔换图系批 V3〕" % [orch.action_economy.rap, rap0, discounted], + orch.action_economy.rap == rap0 - 2 * discounted) + Kit.check("V3 门槛拒:RAP 不足折后价零受理(can=false 且执行拒)〔换图系批 V3〕", + orch.action_economy.rap < discounted and not orch.can_refresh() and not orch.player_request_refresh()) + + +# V3 双基座叠加:挖格产 RAP 与刷新折价同场生效 +func test_double_base_stack() -> void: + print("--- V3 双基座叠加 ---") + var orch = _orch(46023) + Kit.check("V3 前置:战斗受理", orch.begin_battle(_enc(["vein_reserve", "easy_swap"]))) + _bank_rap(orch, 1) + var rap_before: int = orch.action_economy.rap + var base_cost: int = int(_db.run.action_economy.rap_refresh_cost) + _auto_dig(orch) + Kit.check("V3 双基座:挖格产 RAP +1(%d→%d)〔换图系批 V3〕" % [rap_before, orch.action_economy.rap], + orch.action_economy.rap == rap_before + 1) + var rap_after_dig: int = orch.action_economy.rap + Kit.check("V3 双基座:刷新按折后价扣(−%d)〔换图系批 V3〕" % int(base_cost / 2), + orch.player_request_refresh() and orch.action_economy.rap == rap_after_dig - int(base_cost / 2)) + + +# V12(refresh_feats 侧):无专长基线——挖格零产 RAP、刷新按表基线扣、AP 保留、上限照旧 +func test_no_feats_baseline() -> void: + print("--- V12 无专长基线(refresh_feats 侧) ---") + var orch = _orch(46024) + Kit.check("V12 前置:战斗受理", orch.begin_battle(_enc([]))) + _bank_rap(orch, 3) + var rap_before: int = orch.action_economy.rap + var ap_before: int = orch.action_economy.ap + var base_cost: int = int(_db.run.action_economy.rap_refresh_cost) + _auto_dig(orch) + Kit.check("V12 无基座:挖格零产 RAP(%d 不变)〔换图系批 V12〕" % rap_before, orch.action_economy.rap == rap_before) + Kit.check("V12 刷新受理并按表基线扣(−%d)〔换图系批 V12〕" % base_cost, + orch.player_request_refresh() and orch.action_economy.rap == rap_before - base_cost) + Kit.check("V12 AP 保留、相位 Active(基线刷新不重置)〔换图系批 V12〕", + orch.action_economy.ap == ap_before - int(_db.run.action_economy.dig_ap_cost) and orch.action_economy.phase == "Active") + Kit.check("V12 每轮上限照旧:第 3 次拒(RAP 足额仍拒)〔换图系批 V12〕", + orch.player_request_refresh() and not orch.player_request_refresh())