Files
gplan-2/game/core/action_economy.gd
T
DKChan 96d31705b3 feat(卡② T3): 7 施加点 ctx 化 + dig_combo 死路删除 + run_ledger 搬运链方法删除
- action_economy/damage_controller/reward_economy: dispatch 加 _ctx、写口改 EffectEngine.counter_add/counter(_ctx,...)
- battle_orchestrator: _fx_emit→dispatch(counters_ctx)、counter_spend 静态化、删 counters_seed 回灌与 counters_export 战末导出、_feat_yield_adjust 删 dig_combo 死读数与 combo_cap 第五参(零行为差)
- grant_segment: counter 读/value_counter/per ctx 化、_apply_counter_hit 写口静态化、combo_cap 收集删除
- run_ledger: 删 counters_seed_snapshot/counters_merge_export + 头注/字段注改 ctx 直写口径
数据行 domain/reset_at 删除与 feats.tres id 改名见 T6;核心层调用面已对齐新 API。
2026-10-20 23:02:13 +08:00

288 lines
12 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## VERTICAL SLICE - NOT FOR PRODUCTION
## 行动经济被动账本:AP 统一价目扣费、轮末三路径仲裁(cleared > stopped > ap_exhausted)与战斗域 RAP 池(垂直切片验证:一场完整战斗逐行归因)。
## 相位/AP/RAP 只读经 getter,写入只经 request_*/notify_*/try_spend 通道——相位守卫拒绝零副作用。
## 相位词表 "Idle"/"Active"/"Waiting" 是 presentation_snapshot 外部契约,禁改拼写。
## round_end 双发射源:stopped 路径由本类 round_end_stopped 直发,棋盘路径由编排器组包 round_end_board——订阅方两源并听。
class_name ActionEconomy
extends RefCounted
signal round_end_stopped(payload: Dictionary) # 双发射源之停手路径(行动经济 直发;payload 与 排雷玩法 棋盘路径同构)
const PHASE_IDLE := "Idle"
const PHASE_ACTIVE := "Active"
const PHASE_WAITING := "Waiting"
# 相位轴:内部 enum + match,外部 phase getter 保持
# String 面(presentation_snapshot 与冒烟链的 "Idle"/"Active"/"Waiting" 对照契约)。
enum Phase { IDLE, ACTIVE, WAITING }
var db: ConfigDb
# 相位与账本只读出口:外部读取经 getter-only 属性(架构文档
# "ap/rap/phase 只读" 的机制化),写入只经 notify_* / request_* 方法通道。
var _phase: int = Phase.IDLE
var phase: String:
get: return _phase_text(_phase)
var _ap := 0
var ap: int:
get: return _ap
var _rap := 0
var rap: int:
get: return _rap
var _last_round_end: Dictionary[String, String] = {} # {path_tag, continuation_hint}——排雷玩法 组包 round_end 的合法数据源
var last_round_end: Dictionary[Variant, Variant]:
get: return _last_round_end
var _settlement_consumed := false # AC-14 门:notify_settlement_done 消费后才受理 round_start
var _next_round_mode := "" # 调试观测量(同板续轮 / 换新图)
var _pending_ap_delta := 0 # 下轮行动点修正排队(action 型 ±1 排队,轮开始兑现后清零)
# 蓄力流运行态:转蓄力=round_end grant_ap(mode:transfer) 订阅(finish_round_stop 定夺)、
# 深挖对价=dig_paid counter_spend 订阅(request_dig 施加面);charge_layers=蓄力层账本
var _charge_layers := 0
var charge_layers: int:
get: return _charge_layers
# 效果引擎(dig_paid/charge_full 发射面;round_end 转存决策面)
var _fx: EffectEngine = null
var _ctx: EffectContext = null
var _last_stop_ap := 0 # 停手侧剩余 AP 记账(转存决策在 round_end 发射点——grant_ap(transfer) 订阅定夺)
var _ap_exhausted_last := false # 上轮 AP 耗尽封印(一滴不剩 round_start when 面)
func set_fx(fx: EffectEngine, ctx: EffectContext) -> void:
_fx = fx
_ctx = ctx
func _init(db_ref: ConfigDb) -> void:
db = db_ref
func _phase_text(p: int) -> String:
match p:
Phase.ACTIVE:
return PHASE_ACTIVE
Phase.WAITING:
return PHASE_WAITING
return PHASE_IDLE
func _cost(kind: String) -> int:
var ae = db.action_economy
match kind:
"dig":
return ae.dig_ap_cost # 基础挖格成本恒表值(钻头对价挂付费开掘侧)
"seal_place":
return ae.seal_place_ap_cost
"move":
return ae.move_ap_cost
"chain":
return ae.chain_ap_cost
assert(false, "未知动作价目: " + kind)
return 0
# 通用扣减(排雷玩法 用于封印动作;挖握走 request_dig——cleared 仲裁需要 clears_map 参数)
func try_spend(action_kind: String) -> bool:
if _phase != Phase.ACTIVE:
_reject("phase")
return false
return _spend_and_arbitrate(action_kind, false)
# 排雷玩法 挖掘专用:chain_size = 连锁附加格数(不含首格);clears_map = 本次动作后全图安全格开完。
# 深挖钻头付费开掘(dig_paid counter_spend 订阅施加面):按序消耗族——AP 富余(≥基础+对价)时本动作付全价并积本轮付费计数(效果引擎
# 计数器=授予段追加产出配额取数面);AP 不足全价时 fallback 降级只付基础 1 照常开格、不积计数
# ——持有钻头永不锁死普通挖格(散件保底 B-2)。
func request_dig(chain_size: int, clears_map: bool) -> bool:
if _phase != Phase.ACTIVE:
_reject("phase")
return false
var cost := _cost("dig")
# dig_paid 发射+施加(付费开掘发起、动作经济闸收对价前:
# counter_spend 命中=尝试加收对价并积计数;资源不足走 fallback/耗尽即止=只付常规基础对价。
# counter_add〔双管镐 付费挖格计数〕/ grant_ap〔每第 2 次返 1 AP——对价减免,
# mod_eq 闸在行 when〕同面施加)
var dig_refund := 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":
var extra := int(h.get("value", 1))
if _ap >= cost + extra:
cost += extra
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), 1)
elif String(h.get("kind", "")) == "counter_add":
var set_v = h.get("set", null)
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)
func _spend_cost_and_arbitrate(cost: int, clears_map: bool) -> bool:
if _ap < cost:
_reject("resource_face")
return false
_ap -= cost
if clears_map:
# cleared 胜出(同帧仲裁 cleared > ap_exhausted);剩余 AP 当场清零作废、不进 RAP(EC7/EC10)
_ap = 0
_finish_round("cleared", "new_board")
elif _ap == 0:
# AP 归 0 瞬间轮次结束——不存在 0-AP 操作窗口
_finish_round("ap_exhausted", "same_board")
return true
func _spend_and_arbitrate(action_kind: String, clears_map: bool) -> bool:
var cost := _cost(action_kind)
if _ap < cost:
_reject("resource_face")
return false
_ap -= cost
if clears_map:
# cleared 胜出(同帧仲裁 cleared > ap_exhausted);剩余 AP 当场清零作废、不进 RAP(EC7/EC10)
_ap = 0
_finish_round("cleared", "new_board")
elif _ap == 0:
# AP 归 0 瞬间轮次结束——不存在 0-AP 操作窗口
_finish_round("ap_exhausted", "same_board")
return true
# 停手:剩余 AP 1:1 转存 RAP;行动经济 直发 stopped 路径 round_end(双发射源)。
# 转蓄力层〔上限 3〕替代 RAP 转存——「不动也是行动」;cleared 分支 AP 恒清零维持作废语义。
# 本方法只记账——剩余 AP 存 _last_stop_ap 后清零,转存决策在 round_end 发射点
# finish_round_stop(grant_ap(mode:transfer) 订阅定夺)。
func request_stop() -> bool:
if _phase != Phase.ACTIVE:
_reject("phase")
return false
# dig_stop 发射+施加(停手挂点〔弃图兑换〕:剩余 AP 随载荷外发,counter_add
# value_event="ap_left" 取值面〔值=停手时刻真实余量〕;发射先于记账清零)
if _fx != null:
for h in _fx.dispatch(_ctx, "dig_stop", {"ap_left": _ap}):
if String(h.get("kind", "")) == "counter_add":
var val := int(h.get("value", 0))
if String(h.get("value_event", "")) == "ap_left":
val = _ap
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), val, h.has("set"))
_last_stop_ap = _ap
_ap = 0
_finish_round("stopped", "same_board")
round_end_stopped.emit({"path_tag": "stopped", "continuation_hint": "same_board"})
return true
# 储备池注入(储备金袋凭证:战首 RAP +N,节点级一次性;任意相位受理=战首注入时 Idle/Active 均合法)
func grant_reserve_ap(n: int) -> void:
_rap += maxi(0, n)
# 轮末停手转存决策(round_end 发射点调用——编排器授予段尾):transfer=true=剩余 AP 转蓄力层
# (上限 3,整备军备面);false=按转存率转存 RAP(transfer_pct 百分比口径缺省 100=1:1;
# 储备罐 rap_transfer_mult magnitude 125=1.25:1,整数除法向下取整=每 4 AP 多转 1)。
# 非停手轮(cleared/耗尽)_last_stop_ap=0 两分支均零操作。转蓄后达上限的沿发 charge_full(R6 框架先行——现无订阅者)。
func finish_round_stop(transfer: bool, transfer_pct: int = 100) -> void:
if transfer:
var prev_layers := _charge_layers
_charge_layers = mini(3, _charge_layers + _last_stop_ap)
if _fx != null and _charge_layers >= 3 and prev_layers < 3:
_fx.dispatch(_ctx, "charge_full", {"layers": _charge_layers})
else:
_rap += _last_stop_ap * maxi(1, transfer_pct) / 100
_last_stop_ap = 0
# 轮内刷新:唯一入口 =
# Active 相位——仅扣 RAP,相位与 AP 均不动。刷新不打断轮次、不触发结算;换图编排归 战斗结算
# (retire + install 立即执行,玩家同轮继续挖新图)。决策主体 = 玩家(经 HUD框架发起)纪律不变。
func request_refresh() -> bool:
if _phase != Phase.ACTIVE:
_reject("phase")
return false
if _rap < db.action_economy.rap_refresh_cost:
return false # EC13 资源不足拒绝——合法相位内资源拒绝,非编排错误
_rap -= db.action_economy.rap_refresh_cost
return true
# 战斗结算 回发:结算完成(仅记录编排意图;phase 停留 Waiting——AC-14 门不被绕过)
func notify_settlement_done(next_round_mode: String) -> bool:
if _phase != Phase.WAITING:
_reject("phase")
return false
_settlement_consumed = true
_next_round_mode = next_round_mode
return true
# 下轮行动点修正排队:action 型正负行结算期调用,同轮多次授予累加;
# 相位无关(授予发生在结算段,此时本类可能处于 ACTIVE/WAITING 任一相位)
# 第 9 API(registry 行动经济×① 接口漂移预授权):ap_grant 道具的
# 当轮行动点加量(GDD「加行动点效果给当轮」——区别于 queue_ap_delta 的下轮排队语义)。Active 受理、
# 无上限域、零随机;使用道具本身零扣费(成本在获取时已付——捡道具格不出战斗奖励)。
func grant_ap(amount: int) -> bool:
if amount <= 0 or _phase != Phase.ACTIVE:
_reject("grant_ap")
return false
_ap += amount
return true
func queue_ap_delta(delta: int) -> void:
_pending_ap_delta += delta
func pending_ap_delta() -> int:
return _pending_ap_delta
func notify_round_start() -> bool:
if _phase == Phase.IDLE:
pass # 战斗首轮唯一出口
elif _phase == Phase.WAITING and _settlement_consumed:
pass
else:
_reject("phase")
return false
_ap = maxi(0, db.action_economy.ap_per_round + _pending_ap_delta) # 每轮基线 + 排队修正(下限 0)
_pending_ap_delta = 0
_phase = Phase.ACTIVE
_settlement_consumed = false
return true
func notify_combat_end() -> bool:
if _phase != Phase.WAITING:
_reject("phase")
return false
_phase = Phase.IDLE
_rap = 0 # RAP 战斗域清零(HUD框架可见反馈义务)
_ap = 0
return true
func _finish_round(path_tag: String, continuation_hint: String) -> void:
# 实现层断言兜底(行动经济 轮次结束仲裁表):耗尽轮剩余恒为 0;清图轮剩余已当场清零(不进储备池)。
# 两断言守在唯一仲裁出口,未来若在扣费与仲裁之间挪动 AP 写入在此红灯。
assert(path_tag != "ap_exhausted" or _ap == 0, "耗尽轮剩余行动点恒为 0(实现层断言兜底)")
assert(path_tag != "cleared" or _ap == 0, "清图轮剩余行动点当场清零(不进储备池)")
_last_round_end = {"path_tag": path_tag, "continuation_hint": continuation_hint}
_ap_exhausted_last = path_tag == "ap_exhausted" # 一滴不剩 when 面(轮始发射读数)
_phase = Phase.WAITING
# 上轮 AP 是否耗尽(round_start 发射 payload 读数面;轮始兑现后保持到本轮末)
func ap_exhausted_last_round() -> bool:
return _ap_exhausted_last
func _reject(source: String) -> void:
if source == "phase":
push_warning("[行动经济] 动作被拒(%s 相位守卫/资源)" % source)
else:
push_warning("[行动经济] 资源不足拒绝")