Files
gplan-2/game/presentation/curio_interact_controller.gd

229 lines
12 KiB
GDScript
Raw Permalink 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
## 三选一交互控制器:三选一停点编排(presented→pick→confirm/refresh→resolved→continue)+ TP0 令牌池卡片文案装配 + 秘宝显示名投影。
## 信号连接只归组合根(curio_overlay/adventure 信号接至本类 on_* 入口);模态栈/演播后排窗/呈现刷新/fatal 经组合根回调注入,adventure/runner 现值注入。
## 兑付写调用唯一经 adventure.stop().pick_curio/apply_node_curio——本类零直写。
## 同调装配不可变(冒险中零更换途径)——无替换子交互。
extends RefCounted
const Narrator := preload("res://presentation/battle_narrator.gd")
var db
var ctx: CurioFlowContext # u13 引用束注入:构造注入 context 活引用(views/getters/flow 三束键名)
# _pending_curio/_curio_context 纯内部态保持宿主
var _pending_curio := {} # 三选一停点载荷暂存(战斗总结结算确认后开三选一面;续玩停点复用)
var _curio_context := "level_clear" # 三选一面上下文(level_clear=关卡完成停点→推进下一关 / reward_node=奖励节点→回图选路)
func _init(ctx_ref: CurioFlowContext) -> void:
ctx = ctx_ref
db = ctx.db
# 停点在途判据(组合根 _on_battle_end_continue 分派面——战斗总结确认后开三选一)
# 续玩停点恢复入口(true=已接手呈现,调用方中止后续分派)。空候选降级:
# 候选 0 件不开选择面(空面无卡无钮=死面)——消化停点后按上下文直落后续推进(战斗总结面已
# 给过玩家确认拍,三选一环节整体跳过)
func resume_if_pending() -> bool:
if _pending_curio.is_empty():
return false
if (_pending_curio.get("candidates", []) as Array).is_empty():
return _skip_empty_stop()
show_pick()
return true
# 停点空候选跳过(「选出来 0 个则直接跳过该环节」):分派与 on_continue
# 同构(level_clear=推进下一关 / reward_node=兑付确认回图选路 / feat_ticket=只关面回店),
# 差异仅零面可收——curio_overlay 未开不 close。core 停点经 skip_empty_curio 消化后推进
#(continue_to_next_level 的「停点未兑付拒推进」守卫因此放行)。
func _skip_empty_stop() -> bool:
ctx.views.toast.show_toast("可用战利品为空——本次三选一跳过", "info")
var adventure = ctx.getters.adventure_get.call()
if adventure != null:
adventure.stop().skip_empty_curio() # core 停点消化(其守卫=仅空候选放行)
if _curio_context == "reward_node":
var runner = ctx.getters.runner_get.call()
if runner != null:
runner.continue_after_node()
elif _curio_context == "level_clear" and bool(ctx.getters.adventure_mode_get.call()):
if not adventure.continue_to_next_level():
ctx.flow.fatal_screen.call("关卡推进失败——见输出日志")
# feat_ticket = 只消化停点回店(零推进——与 on_continue 同构)
_pending_curio = {}
return true
# 三选一停点(非末关的关卡完成恰一次):Boss 胜利先经战斗总结结算面(settlement-summary v1.1——钥石
# 增量行在其上),确认后进三选一面(level-clear.md v2);续玩停点恢复同入口(零自动推进契约)
func on_presented(payload: Dictionary) -> void:
if String(payload.get("context", "")) == "feat_ticket":
# T5 店内专长凭证兑付——临时停点开专长三选一面(零胜利总结面;确认后只关面回店)
_pending_curio = payload.duplicate(true)
_curio_context = "feat_ticket"
if (_pending_curio.get("candidates", []) as Array).is_empty():
# 空候选防御(零卡不开死面;core _redeem_feat_ticket 守卫外的末道)
_skip_empty_stop()
return
ctx.flow.open_modal.call(ctx.views.curio_overlay)
ctx.views.curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(payload.get("candidates", []) as Array))
return
if int(payload.get("aux_cleared", 0)) > 0:
ctx.views.toast.show_toast("上关剩余 %d 件秘宝已随关卡结束清空" % int(payload.get("aux_cleared", 0)), "info") # 清空反馈
_pending_curio = payload.duplicate(true)
_curio_context = "level_clear"
if bool(payload.get("refreshed", false)):
# 刷新重发:refreshed 标直落选择面——不再重开胜利总结面
show_pick()
return
ctx.views.end_overlay.open_victory(Narrator.battle_end_text("victory", false), "继续",
ctx.views.board_view.get_global_rect(),
{"gp_summary": "%s\n%s" % [String(ctx.flow.gp_summary.call()), Narrator.bonus_keystone_line(payload)]})
ctx.flow.open_modal_after_playback.call(ctx.views.end_overlay)
ctx.flow.refresh_presentations.call()
# 奖励节点三选一(与关卡完成三选一共享同池同权重同上限;CurioPickOverlay 共用面)。
# 无替换子交互(同调装配不可变,见类头)。
func present_node_curio() -> void:
var adventure = ctx.getters.adventure_get.call()
_curio_context = "reward_node"
var node_draw: Dictionary[Variant, Variant] = adventure.stop().node_curio_candidates()
var node_cands: Array[Variant] = node_draw.get("candidates", []) as Array
if node_cands.is_empty():
# 空池降级——不开面、不挂节点停点(present_node_curio 未调即未挂),
# 兑付确认直落回图选路(auto-sim「node_entered 回调内立即 continue_after_node」先例)
ctx.views.toast.show_toast("可用战利品为空——本次三选一跳过", "info")
var runner = ctx.getters.runner_get.call()
if runner != null:
runner.continue_after_node()
return
ctx.flow.open_modal.call(ctx.views.curio_overlay)
ctx.views.curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(node_cands))
adventure.stop().present_node_curio(node_cands)
_feed_refresh_display(adventure) # 刷新面投喂单点
# 三选一面开出(战斗总结结算确认 / 续玩停点恢复)
func show_pick() -> void:
var payload: Dictionary[Variant, Variant] = _pending_curio
ctx.flow.open_modal.call(ctx.views.curio_overlay)
ctx.views.curio_overlay.open_pick(Narrator.curio_pick_header(payload), _curio_card_texts(payload.get("candidates", []) as Array))
var adventure = ctx.getters.adventure_get.call()
if adventure != null:
_feed_refresh_display(adventure) # 刷新面投喂单点
ctx.flow.refresh_presentations.call()
# 刷新呈现投喂单点(免费余量 X/Y + GP 付费价 + 冒险钱包可付——读数与商店 set_refresh_display 同构)
func _feed_refresh_display(adventure: AdventureRunner) -> void:
var runner = ctx.getters.runner_get.call()
var gp := int(runner.gp) if runner != null else 0
var cost: int = adventure.refresh_bank().gp_cost()
ctx.views.curio_overlay.set_refresh_display(adventure.refresh_bank().free_left(), cost, gp >= cost)
func _curio_card_texts(candidates: Array) -> Array:
# TP0 令牌池——候选 = {"kind","tier"} 令牌字典;卡片 id = "kind|tier|feat_id" 复合键(同 kind
# 大小两档/同档多 relic 可同屏,kind 单键不构成唯一身份),确认回程拆键(on_pick_confirmed)
var out: Array[Variant] = []
for tok in candidates:
var kind := String((tok as Dictionary).get("kind", ""))
var tier := String((tok as Dictionary).get("tier", ""))
var rid := String((tok as Dictionary).get("feat_id", ""))
var cid := "%s|%s|%s" % [kind, tier, rid]
var text := ""
if kind == "feat_ticket":
text = "专长获取
选中或买下后立刻开一次专长三选一"
elif kind == "relic":
text = Narrator.feat_card_text(db.crossrun.feats.row_by_id(rid))
else:
text = Narrator.bonus_card_text(kind, tier, int(db.run.adventure.curio_value(kind, tier)))
out.append({"id": cid, "text": text})
return out
# 道具件卡片行(掷定前置——具体道具名 + 效果摘要;空 iid = 无预掷回退态显示通用文案)。
# 文案表值化:效果摘要=数据行 desc 字段直读(零 kind 硬编码映射——
# 新件写行即有名有姓)
# 三选一刷新(X/Y 双源 + 对齐商店 MC-1——免费位用尽转 GP 付费档:
# 同 shop_refresh_cost_base/curve 旋钮、同冒险钱包 runner.gp;关卡完成停点经重发 curio_choice_presented 直落
# 选择面,节点停点在此读在途态重 present——零自动推进契约不破,停点仍在途)
func on_refresh() -> void:
var adventure = ctx.getters.adventure_get.call()
if adventure == null or not adventure.stop().curio_stop_pending():
return
var runner = ctx.getters.runner_get.call()
var refreshed: bool = adventure.stop().refresh_curio()
if not refreshed:
# 免费位(X/Y)已穷——GP 付费档(先扣钱包后重抽,余额不足拒绝零副作用)
var cost: int = adventure.refresh_bank().gp_cost()
if runner == null or not runner.shop.spend_gp(cost):
ctx.views.toast.show_toast("GP不足——刷新失败", "warn")
return
refreshed = adventure.stop().refresh_curio_paid()
ctx.flow.refresh_presentations.call() # 钱包变化上屏(对齐商店购买后刷新面)
if not refreshed:
return
if _curio_context == "reward_node":
var st: Dictionary[Variant, Variant] = adventure.stop().node_curio_pending_state()
ctx.views.curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st.get("candidates", []) as Array))
_feed_refresh_display(adventure)
func on_pick_confirmed(item_id: String) -> void:
var adventure = ctx.getters.adventure_get.call()
if adventure == null:
return
# TP0 令牌池——卡片 id "kind|tier|feat_id" 拆键(缺 | = 旧载荷防御归普通档;relic 三段)
var parts := item_id.split("|")
var kind := parts[0]
var tier := parts[1] if parts.size() > 1 else ""
var rid := parts[2] if parts.size() > 2 else ""
var ok: bool = adventure.stop().apply_node_curio(kind, tier, rid) if _curio_context == "reward_node" else adventure.stop().pick_curio(kind, tier, rid)
if not ok:
push_error("[bonus_interact] 三选一兑付被拒——见输出日志")
return
# T5——节点渠道专长凭证拿到即开:候选已被替换为专长面,本地读在途态重绘(零 emit——context 保持 reward_node,
# 后续兑付/确认仍走节点路径回图选路)
if _curio_context == "reward_node" and kind == "feat_ticket":
var st2: Dictionary[Variant, Variant] = adventure.stop().node_curio_pending_state()
ctx.views.curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st2.get("candidates", []) as Array))
# 兑付反馈(curio_resolved 到达——结果行上屏,唯一出口 = 继续)
func on_resolved(result: Dictionary) -> void:
_pending_curio = {}
ctx.views.curio_overlay.show_result(Narrator.bonus_result_text(result))
ctx.flow.refresh_presentations.call()
# 继续钮:收三选一面 → 上下文分派(关卡完成停点=推进下一关 / 奖励节点=兑付确认回图选路——零自动推进)
func on_continue() -> void:
ctx.flow.close_modal.call(ctx.views.curio_overlay)
ctx.views.curio_overlay.close_pick()
var adventure = ctx.getters.adventure_get.call()
if _curio_context == "feat_ticket":
return # T5 店内专长凭证面——专长兑付完成后关面回店(无推进/无回图)
if _curio_context == "reward_node":
var runner = ctx.getters.runner_get.call()
if runner != null:
runner.continue_after_node()
return
if bool(ctx.getters.adventure_mode_get.call()) and adventure != null:
if not adventure.continue_to_next_level():
ctx.flow.fatal_screen.call("关卡推进失败——见输出日志")
# 秘宝 item_id → 显示名投影(keystone_settle_text/替换子命名文案数据源——拼句归 Narrator);
# 数据源=treasure 装配词表 12 件
func item_name_lookup() -> Dictionary:
var lookup := {}
var meta = ctx.getters.meta_get.call()
if meta != null:
for row in db.crossrun.treasure_items.attunement_rows():
lookup[String(row.get("item_id", ""))] = String(row.get("display_name", ""))
return lookup