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

424 lines
22 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.
## 三选一停点件:停点编排唯一所有者(架构原则 9 本域承载)。
## 停点三态(关卡完成三选一在途/已兑付待推进/奖励节点三选一在途);刷新账三源(X 刷新件每碰三选一
## 重给/优先消耗不结转、Y 营地升级行冒险周期池/用完即无、刷新券存量账/冒险级跨停点持久——账面归
## RefreshBank 件、编排留本件);兑付方法族(pick/refresh/redeem/skip);凭证出箱(RAP/时沙待注入
## ——战首取走清零);三选一信号面自带。
## 零自动推进停点语义承载:兑付只经 pick/apply 显式入口,停点在途时推进被拒(宿主关序受理读放行谓词)。
## 生命周期=每冒险一次(AdventureRunner 冒险开始建;关卡换实例不换件——每关注入时点经 rebind_level_runner)。
## 随机只经注入 CurioDeck 的 reward_pick 封闭流;兑付落账经注入主角账件(HeroStats/GainJournal)
## 单写原语;落盘经注入方法 Callable(core↔core 接线禁 lambda 捕获 self)。Core 层零 UI。
class_name CurioStop
extends RefCounted
signal curio_choice_presented(payload: Dictionary) ## 专长三选一停点(非末关的关卡完成恰一次;通道白名单后候选恒专长+回血在载荷)——{level_index, candidates, heal_gained, hp_before, hp_after, keystone_gained, keystone_total, gp, aux_cleared}
signal curio_resolved(result: Dictionary) ## 三选一兑付完成(pick_curio/替换受理后恰一次)——{picked, value, hp_before, hp_after, level_index}
var db: ConfigDb
var _deck: CurioDeck # 令牌池件(抽签/摘牌/cap/凭证判定——纯池)
var _stats: HeroStats # 主角数值账(生命/特性——兑付回血与 relic 入集单写原语)
var _journal: GainJournal # 加算日记件(兑付落账单写原语——加算区/钥石读面)
var _snapshot_call := Callable() # 落盘回调(宿主断点快照受理——发射点后触发)
var _grant_gp := Callable() # 当前关卡对象 grant_curio_gp 方法 Callable(GP 件入账维持现路径与 wallet_changed 发射序——每关注入时点更新,防调死对象)
# ---- 停点三态 ----
var _pending := {} # 关卡完成三选一在途:{"candidates": Array, "payload": Dictionary}——空 = 无停点
var _resolved := {} # 已兑付待推进态(curio_resolved 载荷副本——中断续玩不重兑付)
var _node_pending := {} # 奖励节点三选一停点态(共享池;断点不载:续玩重建到选路停点重走,恢复的计数器保证同候选重抽)
# ---- 刷新账(三源账与付费计数归 RefreshBank 件——编排留本件:查停点态→bank 支出→重抽重发)----
var _bank: RefreshBank # 刷新账件(构造注入——零停点态依赖)
# ---- 凭证出箱(奇物.md 凭证件表——节点级/回合级一次性,战首取走清零)----
var _pending_rap := 0 # 储备金袋待注入(下一战 RAP +N,战首注入后清零)
var _time_sand_queue := 0 # 时沙待注入(下一战首轮 AP +N,战首入队后清零)
var _level_index := 0 # 兑付载荷关卡序号(宿主每关注入——与 AdventureRunner.level_index 同源单写)
func _init(db_ref: ConfigDb, deps: Dictionary, snapshot_call: Callable) -> void:
db = db_ref
_deck = deps.get("deck", null) as CurioDeck
_stats = deps.get("stats", null) as HeroStats
_journal = deps.get("journal", null) as GainJournal
_bank = deps.get("bank", null) as RefreshBank
_snapshot_call = snapshot_call
# 每关注入时点(AdventureRunner._start_current_level 建件接线处调用):关卡序号快照+
# 当前关卡对象 GP 入账方法 Callable(第二关起绑新 runner——防跨关调死对象)
func rebind_level_runner(runner: LevelRunner, level_index: int) -> void:
_level_index = level_index
if runner != null:
_grant_gp = runner.grant_curio_gp
# ---- 五组入口之一:关卡完成开停点(宿主结算受理调用——回血先行、抽签、发信号、落盘)----
# 关卡完成兑付(非末关恰一次;三层奖励框架):部分回血先行 → 加权洗牌掷 3 → 三选一停点
#(顺序即契约——回血按当刻账本与上限结算,选择停点零自动推进)。调用方(宿主关序)须先置
# 「已兑付待推进」关序位(信号同步派发——处理器内立即确认时推进位必须已就位)。
func open_level_clear(payload_base: Dictionary) -> void:
var hp_before := _stats.hero_hp
var max_eff := _stats.effective_max_hp() # 回血封顶口径含调试浮窗 pct(同战斗内上限)
var heal := mini(int(floor(float(max_eff) * float(int(db.run.adventure.clear_heal_pct)) / 100.0)), max_eff - hp_before)
_stats.hero_hp = hp_before + heal
_bank.reset_x() # 碰三选一重置 X 余量(不结转)
_bank.reset_paid() # 付费刷计数随停点清零(对齐商店进店清零)
var pick_payload: Dictionary[Variant, Variant] = {
"level_index": _level_index,
"candidates": _deck.draw_candidates("level_clear"), # T4 渠道白名单——关底 Boss 击败=关卡完成渠道只出专长
"heal_gained": heal,
"hp_before": hp_before,
"hp_after": _stats.hero_hp,
"keystone_gained": int(_journal.keystone_drop_log().back().get("amount", 0)) if not _journal.keystone_drop_log().is_empty() else 0,
"keystone_total": _journal.keystone_gained(),
"gp": int(payload_base.get("gp", 0)), # 钱包读数由宿主随载荷_base 注入(停点件构造参只收 stats+journal——零钱包依赖)
"aux_cleared": int(payload_base.get("aux_cleared", 0)), # additive 扩签(关卡末清空反馈取数)
}
_pending = {"candidates": (pick_payload["candidates"] as Array).duplicate(true),
"payload": pick_payload.duplicate(true)}
curio_choice_presented.emit(pick_payload)
if _snapshot_call.is_valid():
_snapshot_call.call() # 选择停点可中断(契约 5——续玩不重抽不静默丢失)
# ---- 五组入口之二:奖励节点三选一 ----
# 候选呈现(节点到达时调用——恰一 reward_pick scope 洗牌 + 道具件掷定 scope;本停点断点不载,
# 续玩重建到选路停点后重走重抽,恢复的全局计数器保证同候选逐位复现)。
func node_curio_candidates() -> Dictionary:
var cands := _deck.draw_candidates("reward_node")
return {"candidates": cands}
func present_node_curio(candidates: Array) -> void:
_bank.reset_x() # 碰三选一重置 X 余量(不结转)
_bank.reset_paid() # 付费刷计数随停点清零(对齐商店进店清零)
_node_pending = {"candidates": candidates.duplicate(true)}
# ---- 五组入口之三:兑付族(选择停点零自动推进、重复兑付被拒;兑付恰一件、未选项零生效)----
# 玩家点选兑付(呈现层/冒烟/自动模拟确认入口):令牌 ∈ 停点候选(身份键精确匹配——relic 件
# 另须 feat_id)。属性件写加算区、AP 件写第四轴、GP 件走钱包「奖励入账」入口、恢复件当即回血、
# relic 件入特性持有集(🔴 层级纪律:只走局内随机渠道)、道具件受同调位约束。
# 消费=选中令牌踢池(未选回池由「抽签不摘牌、兑付才摘牌」语义承载——TP0 v2.2)。
func pick_curio(kind: String, tier: String = "", feat_id: String = "") -> bool:
if _pending.is_empty():
push_error("[停点件] pick_curio 拒绝——无待选停点")
return false
var candidates: Array[Variant] = _pending.get("candidates", []) as Array
if _deck.find_token(candidates, kind, tier, feat_id).is_empty():
push_error("[停点件] pick_curio 拒绝——令牌不在候选: %s|%s|%s" % [kind, tier, feat_id])
return false
if kind == "feat_ticket":
return _redeem_feat_ticket() # T5 拿到即开——停点候选替换为专长三选一(零摘牌/不进持有集)
var value: int = int(db.run.adventure.curio_value(kind, tier))
var hp_before := _stats.hero_hp
if not _apply_curio_pick(kind, tier, value, feat_id):
push_error("[停点件] pick_curio 拒绝——未知件: %s|%s|%s" % [kind, tier, feat_id])
return false
_finish_curio_pick(kind, value, hp_before, tier, feat_id)
return true
# 兑付载荷 relic 伴随键(反馈行取数面;非 relic 件零新增键)
func _feat_payload_keys(feat_id: String) -> Dictionary:
if feat_id == "":
return {}
var row: Dictionary[Variant, Variant] = db.crossrun.feats.row_by_id(feat_id)
return {"feat_id": feat_id, "feat_name": String(row.get("display_name", "")), "feat_archetype": String(row.get("archetype", ""))}
# 三选一件兑付共享体(TP0——令牌消费、摘牌单点,含 relic 分支;领域拆分批拆三私有段:
# 资源件段/路由加算段/副作用通道段,兑付语义与顺序逐字不变):
# 属性件写加算区 + 件获取计数、AP 件写第四轴、GP 件走钱包、恢复件当即回血(封顶生效上限——
# 与账本置满/关卡完成回血同式 effective_max_hp)、relic 件入持有集(散件单体保底——效果接线面
# 按挂点消费,档位质变由 FeatsEngine 求值)。兑付即从牌库摘除该令牌(选中踢池;
# 未选候选从未摘牌=自动回池)。道具件不入此体——未知件返回 false 由调用方按渠道名报错。
func _apply_curio_pick(kind: String, tier: String, value: int, feat_id: String = "") -> bool:
if kind == "relic" and db.crossrun.feats.row_by_id(feat_id).is_empty():
push_error("[停点件] relic 兑付失败——表无此件: %s" % feat_id)
return false # 先校验后摘牌(令牌零丢失)
if not _deck.consume_deck_token(kind, tier, feat_id):
push_error("[停点件] 令牌消费失败——牌库无此令牌: %s|%s|%s" % [kind, tier, feat_id])
return false
if _apply_resource_pick(kind, value, feat_id): # 资源件段(refresh/rap/time_sand/relic)
return true
if _apply_route_pick(kind, value): # 路由加算段(CURIO_APPLY_ROUTES 声明路由)
return true
return _apply_channel_pick(kind, value) # 副作用通道段(gp/heal 显式分支——未知件 false)
# 资源件段(refresh/rap/time_sand/relic——冒险级资源与凭证账面;非资源件 false 交回主路由)
func _apply_resource_pick(kind: String, value: int, feat_id: String) -> bool:
if kind == "refresh":
_bank.add_refresh_stock(value) # T4 F-1——刷新券入刷新存量账(冒险级;**不写 X 临时账**:X 每停点 reset_x 重置会吞存量;摘牌先于落账=选中即消尽)
_bump_taken("refresh")
return true
if kind == "rap":
_pending_rap += value # T4——储备金袋:下一战 RAP +N(节点级,战首注入后清零)
_bump_taken("rap")
return true
if kind == "time_sand":
_time_sand_queue += value # T4——时沙:下一战首轮 AP +N(回合级池增量,不改上限)
_bump_taken("time_sand")
return true
if kind == "relic":
if not _stats.feats.has(feat_id):
_stats.feats.append(feat_id)
_bump_taken("relic")
_bump_taken("relic:" + feat_id) # 身份维已拿计数(cap 消费面)
return true
return false
# 路由加算段(match 收敛进效果类型:属性值提升四类走 adventure_table.CURIO_APPLY_ROUTES 声明
# 路由(bonus 落点键),加新加算类令牌零代码)
func _apply_route_pick(kind: String, value: int) -> bool:
var sink := db.run.adventure.curio_apply_route(kind)
if sink == "":
return false
_journal.gain_bonus(sink, value, "curio_pick", kind) # 加算区单写原语(获得日记明细+汇总同拍)
_bump_taken(kind)
return true
# 副作用通道段(副作用形态——显式分支:GP 件走关卡对象入账回调、恢复件当即回血)
func _apply_channel_pick(kind: String, value: int) -> bool:
match kind:
"gp":
if _grant_gp.is_valid():
_grant_gp.call(value)
_bump_taken("gp")
"heal":
_stats.hero_hp = mini(_stats.hero_hp + value, _stats.effective_max_hp())
_bump_taken("heal")
_:
return false
return true
# 件获取次数计数递增(kind 维度派生账本——池件侧承载)
func _bump_taken(kind: String) -> void:
_deck.taken[kind] = int(_deck.taken.get(kind, 0)) + 1
# 兑付收口(恰一次):清停点态 → 记已兑付态 → emit → 停点快照(等继续确认——中断续玩不重兑付)。
# picked = kind;tier 随载荷(TP0——大小档位读数面);relic 伴随键随载荷(反馈行取数)
func _finish_curio_pick(picked: String, value: int, hp_before: int, tier: String = "", feat_id: String = "") -> void:
_pending = {}
_resolved = {
"picked": picked,
"tier": tier,
"value": value,
"hp_before": hp_before,
"hp_after": _stats.hero_hp,
"level_index": _level_index,
}
_resolved.merge(_feat_payload_keys(feat_id))
curio_resolved.emit(_resolved.duplicate(true))
if _snapshot_call.is_valid():
_snapshot_call.call()
# 点选兑付(节点域——无部分回血无钥石;令牌消费与牌库摘牌与关卡完成三选一同库;relic 同权入持有集)
func apply_node_curio(kind: String, tier: String = "", feat_id: String = "") -> bool:
if kind == "feat_ticket":
return _redeem_feat_ticket() # T5 拿到即开——节点停点候选替换为专长三选一(呈现层本地重绘)
if _node_pending.is_empty():
push_error("[停点件] apply_node_curio 拒绝——无待选停点")
return false
var candidates: Array[Variant] = _node_pending.get("candidates", []) as Array
if _deck.find_token(candidates, kind, tier, feat_id).is_empty():
push_error("[停点件] apply_node_curio 拒绝——令牌不在候选: %s|%s|%s" % [kind, tier, feat_id])
return false
var value: int = int(db.run.adventure.curio_value(kind, tier))
if not _apply_curio_pick(kind, tier, value, feat_id):
push_error("[停点件] apply_node_curio 拒绝——未知件: %s|%s|%s" % [kind, tier, feat_id])
return false
_finish_node_bonus(kind, value, tier, feat_id)
return true
# 停点空候选消化(池空=三选一环节整体跳过:候选 0 件时停点视同已处理,
# 呈现层跳开选择面直接走后续推进。仅空候选合法;非空停点拒绝——零自动推进契约不破,正常
# 消化仍只走 pick_curio/apply_node_curio 兑付。触发源=渠道白名单过滤后池空(关卡 Boss 击破=关卡完成
# 渠道只出专长,专长零配置/旧档 deck 无 relic 余库即空集)。)
func skip_empty_curio() -> bool:
if _pending.is_empty():
return false
if not (_pending.get("candidates", []) as Array).is_empty():
return false
_pending = {}
return true
# 专长凭证兑付(拿到即开——停点候选替换为专长三选一;不进持有集/零摘牌;限购计数 +1):
# 节点停点=本地替换(呈现层读在途态重绘);店内无停点=临时停点 + payload.context 标记(呈现层
# 重 present 无胜利总结面,确认后只关面回店)。
func _redeem_feat_ticket() -> bool:
_deck.ticket_sold += 1 # 限购计数(本体在池件——原宿主转发属性迁点)
_deck.ticket_pity = 0
var cands := _deck.draw_candidates("level_clear") # 专长池(与关卡完成渠道同白名单)
if cands.is_empty():
push_error("[停点件] 专长凭证兑付失败——专长池空集")
_deck.ticket_sold -= 1
return false
if not _node_pending.is_empty():
_node_pending["candidates"] = cands.duplicate(true)
return true
var payload: Dictionary[Variant, Variant] = {"context": "feat_ticket", "candidates": cands.duplicate(true)}
_pending = {"candidates": cands.duplicate(true), "payload": payload.duplicate(true)}
curio_choice_presented.emit(payload)
if _snapshot_call.is_valid():
_snapshot_call.call()
return true
# ---- 五组入口之四:刷新编排(三源账与报价归 RefreshBank 件——本件只做停点态校验、
# 支出分派与重抽重发;重抽消费 reward_pick 既有洗牌流,池规则全保留——不放回/
# 达上限剔牌/掷定前置直显;停点可中断)----
# 节点停点在途态读回(组合根重 present 用——candidates 值拷贝)
func node_curio_pending_state() -> Dictionary:
return {
"candidates": (_node_pending.get("candidates", []) as Array).duplicate(true),
}
# 三选一刷新(免费位 X/Y——组合根唯一调用口;无停点/无余量 false 零副作用)。关卡完成停点重抽后重发
# curio_choice_presented(呈现层重建卡片+刷新余量);节点停点由组合根读 node_curio_pending_state 重 present。
func refresh_curio() -> bool:
return _refresh_curio_common(false)
# 三选一 GP 付费刷新(对齐商店 MC-1——免费位 X/Y 用尽后转付费,同旋钮同曲线同钱包;
# 组合根先经 curio_stop_pending+钱包支出再调,本口只做计数与重抽)。池规则全保留——不放回/达上限剔牌/掷定前置直显。
func refresh_curio_paid() -> bool:
return _refresh_curio_common(true)
# 三选一停点在途谓词(付费路径扣费前置校验——无停点禁扣钱,防空转烧钱包)
func curio_stop_pending() -> bool:
return not _pending.is_empty() or not _node_pending.is_empty()
# 放行谓词(宿主关序受理——已兑付待推进且无在途停点)
func stop_resolved() -> bool:
return not _resolved.is_empty() and _pending.is_empty()
func _refresh_curio_common(paid: bool) -> bool:
if not curio_stop_pending():
return false
if paid:
_bank.charge_paid() # 付费刷计数递增(报价曲线输入——账面归刷新账件)
else:
if not _bank.spend_free(): # 免费位消耗序 X→Y→存量(三源全空 false 零副作用——序与语义逐字归账件)
return false
if not _pending.is_empty():
var cands := _deck.draw_candidates("level_clear")
_pending["candidates"] = cands.duplicate(true)
var payload: Dictionary[Variant, Variant] = (_pending.get("payload", {}) as Dictionary).duplicate(true)
payload["candidates"] = cands
payload["refreshed"] = true # 重发载荷带标——呈现层直落选择面,不再重开胜利总结面
curio_choice_presented.emit(payload) # 同信号重 present(零自动推进契约不破——停点仍在途)
if _snapshot_call.is_valid():
_snapshot_call.call()
return true
if not _node_pending.is_empty():
var cands2 := _deck.draw_candidates("reward_node")
_node_pending["candidates"] = cands2.duplicate(true)
return true
return false
func _finish_node_bonus(picked: String, value: int, tier: String = "", feat_id: String = "") -> void:
_node_pending = {}
var payload := {
"context": "reward_node",
"picked": picked,
"tier": tier,
"value": value,
"level_index": _level_index,
}
payload.merge(_feat_payload_keys(feat_id))
curio_resolved.emit(payload)
# 商店共享池消费单点(兑付单点——ShopSession.buy_pool_item 经注入 Callable 调用,回 _apply_curio_pick
# 同口;专长凭证买下即开零摘牌)。未知 kind 拒绝(货架词表封闭的前置防御)。
func consume_pool_purchase(kind: String, tier: String) -> bool:
if kind == "feat_ticket":
return _redeem_feat_ticket()
if not db.run.adventure.CURIO_KINDS.has(kind):
push_error("[停点件] 商店购买消费拒绝——件不在池词表: %s" % kind)
return false
return _apply_curio_pick(kind, tier, int(db.run.adventure.curio_value(kind, tier)))
# ---- 冒险级复位(宿主 _reset_curio_state 调用——停点半:池归 deck.reset、停点三态与刷新账清零)----
func reset_adventure_state() -> void:
_pending = {}
_resolved = {}
_node_pending = {}
_bank.reset_all() # 刷新三源与付费计数清零(账面归刷新账件)
_pending_rap = 0
_time_sand_queue = 0
# 续玩选择停点回填(token_migration 类:旧版断点候选为 kind 字符串,按普通档令牌防御归一;
# 发射回宿主信号面由调用方承接——本件只载态)
func restore_pending(bp_pending: Dictionary) -> void:
var migrated: Array[Variant] = []
for c in (bp_pending.get("candidates", []) as Array):
if c is Dictionary:
# T4——feat_id 随迁移(关卡完成渠道候选=纯专长令牌,身份键 relic|tier|feat_id 三段全要)
var mig := {"kind": String((c as Dictionary).get("kind", "")), "tier": String((c as Dictionary).get("tier", ""))}
if String(mig["kind"]) == "relic":
mig["feat_id"] = String((c as Dictionary).get("feat_id", ""))
migrated.append(mig)
else:
migrated.append({"kind": String(c), "tier": db.run.adventure.TIER_COMMON})
_pending = {"candidates": migrated}
var payload: Dictionary[Variant, Variant] = (bp_pending.get("payload", {}) as Dictionary).duplicate(true)
curio_choice_presented.emit(payload)
# ---- 凭证出箱(战首取走清零——LevelRunner.set_voucher_grants 注入面同语义)----
func voucher_outbox() -> Dictionary:
var out := {"rap": _pending_rap, "time_sand": _time_sand_queue}
_pending_rap = 0
_time_sand_queue = 0
return out
# 已兑付态清除(宿主 continue_to_next_level 推进受理时调用——重兑付防御复位面)
func clear_resolved() -> void:
_resolved = {}
# 已兑付态注入+重发(宿主 resume 已兑付分支调用——置态后重发载荷,信号面归停点件:
# 续玩不重兑付只重发;原 restore_resolved/reemit_resolved 两步收编一步,时序逐位同)
func restore_resolved(payload: Dictionary) -> void:
_resolved = payload.duplicate(true)
curio_resolved.emit(_resolved.duplicate(true))
# ---- 断点自交片段(应用层拼盘口收件)----
# 片段键:bonus_pending/bonus_result/feat_ticket_pity/feat_ticket_sold
#(feat_ticket_* 现役为宿主字段——随停点件迁移同片段承载;refresh_stock/attune_refresh_y 两键
# 归 RefreshBank 片段,宿主拼盘同一并集)
func breakpoint_fragment() -> Dictionary[Variant, Variant]:
return {
"bonus_pending": _pending.duplicate(true),
"bonus_result": _resolved.duplicate(true),
"feat_ticket_pity": _deck.ticket_pity,
"feat_ticket_sold": _deck.ticket_sold,
}
# 片段回填(凭证计数;停点在途态与已兑付态归宿主主分支
# restore_pending/restore_resolved 承接——token_migration 与信号发射时序在彼处)
func restore_fragment(frag: Dictionary, _params: Dictionary) -> void:
_deck.ticket_pity = int(frag.get("feat_ticket_pity", 0)) # T5 扩列——旧档缺键缺省 0(存档兼容)
_deck.ticket_sold = int(frag.get("feat_ticket_sold", 0))