## 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("[行动经济] 资源不足拒绝")