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

238 lines
9.5 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
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# 行动经济:被动账本(AP 硬重置 / 统一价目 / 三路径结束仲裁 cleared > stopped > ap_exhausted /
# RAP 池战斗域 / 双发射源之 stopped 直发)。相位域 = {Idle, Active, Waiting}。
# 实现 GDD:design/archive/gdd-2026-09-14/a3-action-economy.md(系统 ID A3·行动经济——registry 追溯锚,命名规范见 CLAUDE.md)
class_name ActionEconomy
extends RefCounted
signal round_end_stopped(payload: Dictionary) # 双发射源之停手路径(行动经济 直发;payload 与 排雷玩法 棋盘路径同构)
const PHASE_IDLE := "Idle"
const PHASE_ACTIVE := "Active"
const PHASE_WAITING := "Waiting"
# 相位轴(P6 状态机——code-review 2026-09-29 P2-2):内部 enum + match,外部 phase getter 保持
# String 面(presentation_snapshot 与冒烟链的 "Idle"/"Active"/"Waiting" 对照契约零变化)。
enum Phase { IDLE, ACTIVE, WAITING }
var db: ConfigDb
# 相位与账本只读出口(E5 封装收口 2026-09-13):外部读取经 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:
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 # 下轮行动点修正排队(格面资源八型批 2026-09-20——action 型 ±1 排队,轮开始兑现后清零)
# 蓄力流运行态(迭代 12 T5——relic 件注入面;false=关着零扰动):
# relic_ap_carry=整备军备(轮末剩余 AP 转蓄力层替代 RAP 转存——cleared 作废口径改写的停手侧承载);
# relic_dig_extra=深挖钻头付费开掘对价(R4 甲非信息税形态;2026-10-04 试玩缺陷修复——对价挂付费
# 开掘侧:AP 富余时挖掘付全价 1+extra 积付费计数、AP 不足时降级只付基础 1 照常开格不积计数——
# 原「持有期所有挖掘无条件 +1」实现把对价错挂成普通挖掘税,AP=1 锁死开格);charge_layers=蓄力层账本。
var relic_ap_carry := false
var relic_dig_extra := 0
var _charge_layers := 0
var charge_layers: int:
get: return _charge_layers
var _deep_paid_count := 0
var deep_paid_count: int:
get: return _deep_paid_count
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 # 基础挖格成本恒表值(钻头对价挂付费开掘侧——2026-10-04 缺陷修复)
"flag_place":
return ae.flag_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 = 本次动作后全图安全格开完。
# 深挖钻头付费开掘(2026-10-04 缺陷修复——对价挂付费侧):AP 富余(≥基础+extra)时本动作付全价
# 并积付费开掘计数(orchestrator 深层产出配额取数面);AP 不足全价时**降级只付基础 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")
if relic_dig_extra > 0 and _ap >= cost + relic_dig_extra:
cost += relic_dig_extra
_deep_paid_count += 1
if _ap < cost:
_reject("resource_face")
return false
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(双发射源)。
# 整备军备改写(迭代 12 T5——relic_ap_carry=true 时剩余 AP 转蓄力层〔上限 3〕替代 RAP 转存:
# 「不动也是行动」——cleared 作废口径改写的停手侧承载;cleared 分支 AP 恒清零维持既有作废语义)
func request_stop() -> bool:
if _phase != Phase.ACTIVE:
_reject("phase")
return false
if relic_ap_carry:
_charge_layers = mini(3, _charge_layers + _ap)
else:
_rap += _ap
_ap = 0
_finish_round("stopped", "same_board")
round_end_stopped.emit({"path_tag": "stopped", "continuation_hint": "same_board"})
return true
# 轮内刷新(2026-09-12 方案 A 用户拍板;等待期通道 2026-09-15 用户拍板整体移除):唯一入口 =
# 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
# 下轮行动点修正排队(格面资源八型批 2026-09-20):action 型正负行结算期调用,同轮多次授予累加;
# 相位无关(授予发生在结算段,此时本类可能处于 ACTIVE/WAITING 任一相位)
# 第 9 API(registry 行动经济×① 接口漂移预授权——迭代 7 T2 秘宝内容批落位):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}
_phase = Phase.WAITING
func _reject(source: String) -> void:
if source == "phase":
push_warning("[行动经济] 动作被拒(%s 相位守卫/资源)" % source)
else:
push_warning("[行动经济] 资源不足拒绝")