Files
gplan-2/game/core/action_economy.gd
T

294 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 _settlement_consumed := false # AC-14 门:notify_settlement_done 消费后才受理 round_start
var _pending_ap_delta := 0 # 下轮行动点修正排队(action 型 ±1 排队,轮开始兑现后清零)
# 效果端口件(dig_paid/dig_stop 发射面经端口路由宿主;组装期单点注入——
# host _init bind_port;缺省 null=未绑零扰动,与原 _fx==null 守卫同语义)
var _port: EffectPort = null
var _last_stop_ap := 0 # 停手侧剩余 AP 记账(转存决策在 round_end 发射点)
# 效果端口件注入(host _init 组装期单点——先于 begin_battle 任何发射)
func bind_port(port: EffectPort) -> void:
_port = port
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.run.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 发射+施加(付费开掘发起、动作经济闸收对价前——施加单点 _apply_dig_paid_hits):
# counter_spend 命中=尝试加收对价并积计数;资源不足走 fallback/耗尽即止=只付常规基础对价。
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 _port != null:
for h in _port.emit("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
_port.counter_add(String(h.get("counter_id", "")), 1, false)
elif String(h.get("kind", "")) == "counter_add":
var set_v = h.get("set", null)
_port.counter_add(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))
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:
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(双发射源)。
# cleared 分支 AP 恒清零维持作废语义。
# 本方法只记账——剩余 AP 存 _last_stop_ap 后清零,转存决策在 round_end 发射点
# finish_round_stop(转存率定夺)。
func request_stop() -> bool:
if _phase != Phase.ACTIVE:
_reject("phase")
return false
# dig_stop 发射+施加(停手挂点〔弃图兑换〕:剩余 AP 随载荷外发,counter_add
# value_event="ap_left" 取值面〔值=停手时刻真实余量〕;发射先于记账清零)
if _port != null:
for h in _port.emit("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
_port.counter_add(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)
# 轮末停手 RAP 转存(round_end 发射点调用——编排器授予段尾):按转存率转存 RAP
# (transfer_pct 百分比口径缺省 100=1:1;储备罐 rap_transfer_mult magnitude 125=1.25:1,
# 整数除法向下取整=每 4 AP 多转 1)。非停手轮(cleared/耗尽)_last_stop_ap=0 零操作。
func finish_round_stop(transfer_pct: int = 100) -> void:
_rap += _last_stop_ap * maxi(1, transfer_pct) / 100
_last_stop_ap = 0
# 轮内刷新:唯一入口 =
# Active 相位——仅扣 RAP,相位与 AP 均不动。刷新不打断轮次、不触发结算;换图编排归 战斗结算
# (retire + install 立即执行,玩家同轮继续挖新图)。决策主体 = 玩家(经 HUD框架发起)纪律不变。
# cost_override=折后价入参(迭代 46 卡A T3——BattleRefreshFlow 折价读数传入;缺省 -1=表基线逐位不变)
func request_refresh(cost_override: int = -1) -> bool:
if _phase != Phase.ACTIVE:
_reject("phase")
return false
var cost := int(db.run.action_economy.rap_refresh_cost) if cost_override < 0 else cost_override
if _rap < cost:
return false # EC13 资源不足拒绝——合法相位内资源拒绝,非编排错误
_rap -= 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
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.run.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}
_phase = Phase.WAITING
# 上轮组包读数({path_tag, continuation_hint}——排雷玩法 组包 round_end 的合法数据源;
# 属性→方法 K5 换形,消费方 minesweeper_rules 单点改址)
func last_round_end() -> Dictionary[String, String]:
return _last_round_end
# 上轮 AP 是否耗尽(round_start 发射 payload 读数面;轮始兑现后保持到本轮末)——
# 派生读(同源同刻:_last_round_end 组包点即派生原点,K5 派生合并——存储成员退役)
func ap_exhausted_last_round() -> bool:
return _last_round_end.get("path_tag", "") == "ap_exhausted"
func _reject(source: String) -> void:
if source == "phase":
push_warning("[行动经济] 动作被拒(%s 相位守卫/资源)" % source)
else:
push_warning("[行动经济] 资源不足拒绝")