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