Files
gplan-2/game/presentation/curio_interact_controller.gd
T
DKChan 9a72394652 chore(代码卫生): 头注存量清偿批——82 文件按两类白名单改写为 ## 文档注释载体
按 code-conventions §1.4.1 第三轮定稿执行:core 22+schemas 18+presentation 根 14+views 28,全部头注 #→##、删日期/变更叙事/GDD 与架构反向链接/字段与域清单/机制描述,保留职责摘要+真实使用约束(防事故禁令/单一入口/信号契约每信号一行)。main.gd 31→5 行、save_manager 45→5 行、adventure_runner 37→5 行为最大单件。子代理分模块执行(波一 core+schemas、波二 presentation+views,并发≤2)。验收=tools/verify_comment_only_change.py 逐字节校验(82 文件头注区外逐行一致=零行为变更)+audit_file_headers 全绿;纯注释批免冒烟(先例 5843270)
2026-10-16 23:28:07 +08:00

257 lines
13 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
## 三选一交互控制器:三选一停点编排(presented→pick→confirm/refresh→resolved→continue)+ TP0 令牌池卡片文案装配 + 秘宝显示名投影。
## 信号连接只归组合根(curio_overlay/adventure 信号接至本类 on_* 入口);模态栈/演播后排窗/呈现刷新/fatal 经组合根回调注入,adventure/runner 现值注入。
## 兑付写调用唯一经 adventure.pick_curio/apply_node_curio——本类零直写。
## 同调装配不可变(冒险中零更换途径)——无替换子交互。
extends RefCounted
const Narrator := preload("res://presentation/battle_narrator.gd")
var db
var _meta_get: Callable # () → 元进度账本现值(组合根 lambda——探针 isolate_save 换注面,迭代13 T4 实证)
var curio_overlay: CurioPickOverlay
var end_overlay: BattleEndOverlay
var board_view: BoardView
var toast: ToastStack
var _save_get: Callable # () → 存档承载现值(同上——Attunement.slots 读数面)
var _adventure_get: Callable # () → 冒险结构现值(组合根 lambda——随局更替)
var _runner_get: Callable # () → 关卡推进器现值(组合根 lambda——reward_node 继续推进)
var _adventure_mode_get: Callable # () → 冒险模式标记现值(组合根 lambda)
var _open_modal: Callable # 模态栈单口(组合根)
var _open_modal_after_playback: Callable # 演播后排窗单口(T17——组合根)
var _close_modal: Callable
var _refresh_presentations: Callable
var _fatal_screen: Callable
var _gp_summary: Callable # () → 战斗总结 GP 行文案(组合根 lambda——_last_gain_amount 取数面在根)
var _pending_curio := {} # 三选一停点载荷暂存(迭代 10——战斗总结结算确认后开三选一面;续玩停点复用)
var _curio_context := "level_clear" # 三选一面上下文(level_clear=通关停点→推进下一关 / reward_node=奖励节点→回图选路)
func _init(ctx: CurioFlowContext) -> void:
# 迭代 27 T4——构造参数收拢 context 对象(P1-5:16 参长签名收一处命名面;字段逐一同名迁移)
db = ctx.db
_meta_get = ctx.meta_get
curio_overlay = ctx.curio_overlay
end_overlay = ctx.end_overlay
board_view = ctx.board_view
toast = ctx.toast
_save_get = ctx.save_get
_adventure_get = ctx.adventure_get
_runner_get = ctx.runner_get
_adventure_mode_get = ctx.adventure_mode_get
_open_modal = ctx.open_modal
_open_modal_after_playback = ctx.open_modal_after_playback
_close_modal = ctx.close_modal
_refresh_presentations = ctx.refresh_presentations
_fatal_screen = ctx.fatal_screen
_gp_summary = ctx.gp_summary
# 停点在途判据(组合根 _on_battle_end_continue 分派面——战斗总结确认后开三选一)
# 续玩停点恢复入口(true=已接手呈现,调用方中止后续分派)。空候选降级(2026-10-06 用户令):
# 候选 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
# 停点空候选跳过(2026-10-06 用户令——「选出来 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:
toast.show_toast("可用战利品为空——本次三选一跳过", "info")
var adventure = _adventure_get.call()
if adventure != null:
adventure.skip_empty_curio() # core 停点消化(其守卫=仅空候选放行)
if _curio_context == "reward_node":
var runner = _runner_get.call()
if runner != null:
runner.continue_after_node()
elif _curio_context == "level_clear" and bool(_adventure_mode_get.call()):
if not adventure.continue_to_next_level():
_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():
# 空候选防御(2026-10-06 用户令——零卡不开死面;core _redeem_feat_ticket 守卫外的末道)
_skip_empty_stop()
return
_open_modal.call(curio_overlay)
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:
toast.show_toast("上关剩余 %d 件秘宝已随关卡结束清空" % int(payload.get("aux_cleared", 0)), "info") # 迭代 7 T4 清空反馈
_pending_curio = payload.duplicate(true)
_curio_context = "level_clear"
if bool(payload.get("refreshed", false)):
# 刷新重发(2026-10-06):refreshed 标直落选择面——不再重开胜利总结面
show_pick()
return
end_overlay.open_victory(Narrator.battle_end_text("victory", false), "继续",
board_view.get_global_rect(),
"%s\n%s" % [String(_gp_summary.call()), Narrator.bonus_keystone_line(payload)])
_open_modal_after_playback.call(end_overlay)
_refresh_presentations.call()
# 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;CurioPickOverlay 共用面)。
# 原「秘宝投放渠道」三选一(迭代 7 过渡形态)随批退役;替换子交互随迭代 22 T5 拆除。
func present_node_curio() -> void:
var adventure = _adventure_get.call()
_curio_context = "reward_node"
var node_draw: Dictionary[Variant, Variant] = adventure.node_curio_candidates()
var node_cands: Array[Variant] = node_draw.get("candidates", []) as Array
if node_cands.is_empty():
# 空池降级(2026-10-06 用户令)——不开面、不挂节点停点(present_node_curio 未调即未挂),
# 兑付确认直落回图选路(auto-sim「node_entered 回调内立即 continue_after_node」先例)
toast.show_toast("可用战利品为空——本次三选一跳过", "info")
var runner = _runner_get.call()
if runner != null:
runner.continue_after_node()
return
_open_modal.call(curio_overlay)
curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(node_cands))
adventure.present_node_curio(node_cands)
_feed_refresh_display(adventure) # 迭代 11 第①批 T5b + 2026-10-06 对齐商店——刷新面投喂单点
# 三选一面开出(战斗总结结算确认 / 续玩停点恢复)
func show_pick() -> void:
var payload: Dictionary[Variant, Variant] = _pending_curio
_open_modal.call(curio_overlay)
curio_overlay.open_pick(Narrator.curio_pick_header(payload), _curio_card_texts(payload.get("candidates", []) as Array))
var adventure = _adventure_get.call()
if adventure != null:
_feed_refresh_display(adventure) # 迭代 11 第①批 T5b + 2026-10-06 对齐商店——刷新面投喂单点
_refresh_presentations.call()
# 刷新呈现投喂单点(免费余量 X/Y + GP 付费价 + 冒险钱包可付——读数与商店 set_refresh_display 同构)
func _feed_refresh_display(adventure: AdventureRunner) -> void:
var runner = _runner_get.call()
var gp := int(runner.gp) if runner != null else 0
var cost: int = adventure.curio_refresh_gp_cost()
curio_overlay.set_refresh_display(adventure.attune_refresh_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 = "奇物门票
选定即开一次奇物三选一(T5 门票——拿到即开不进持有集)"
elif kind == "relic":
text = Narrator.feat_card_text(db.feat_row(rid))
else:
text = Narrator.bonus_card_text(kind, tier, int(db.adventure.curio_value(kind, tier)))
out.append({"id": cid, "text": text})
return out
# 道具件卡片行(掷定前置——具体道具名 + 效果摘要;空 iid = 无预掷回退态显示通用文案)。
# 内容数据驱动扩展批 T4 文案表值化:效果摘要=数据行 desc 字段直读(零 kind 硬编码映射——
# 新件写行即有名有姓)
# 三选一刷新(迭代 11 第①批 T5b X/Y 双源 + 2026-10-06 用户令对齐商店 MC-1——免费位用尽转 GP 付费档:
# 同 shop_refresh_cost_base/curve 旋钮、同冒险钱包 runner.gp;通关停点经重发 curio_choice_presented 直落
# 选择面,节点停点在此读在途态重 present——零自动推进契约不破,停点仍在途)
func on_refresh() -> void:
var adventure = _adventure_get.call()
if adventure == null or not adventure.curio_stop_pending():
return
var runner = _runner_get.call()
var refreshed: bool = adventure.refresh_curio()
if not refreshed:
# 免费位(X/Y)已穷——GP 付费档(先扣钱包后重抽,余额不足拒绝零副作用)
var cost: int = adventure.curio_refresh_gp_cost()
if runner == null or not runner.spend_gp(cost):
toast.show_toast("金币不足——刷新失败", "warn")
return
refreshed = adventure.refresh_curio_paid()
_refresh_presentations.call() # 钱包变化上屏(对齐商店购买后刷新面)
if not refreshed:
return
if _curio_context == "reward_node":
var st: Dictionary[Variant, Variant] = adventure.node_curio_pending_state()
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 = _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.apply_node_curio(kind, tier, rid) if _curio_context == "reward_node" else adventure.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.node_curio_pending_state()
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 = {}
curio_overlay.show_result(Narrator.bonus_result_text(result))
_refresh_presentations.call()
# 继续钮:收三选一面 → 上下文分派(通关停点=推进下一关 / 奖励节点=兑付确认回图选路——零自动推进)
func on_continue() -> void:
_close_modal.call(curio_overlay)
curio_overlay.close_pick()
var adventure = _adventure_get.call()
if _curio_context == "feat_ticket":
return # T5 店内门票面——专长兑付完成后关面回店(无推进/无回图)
if _curio_context == "reward_node":
var runner = _runner_get.call()
if runner != null:
runner.continue_after_node()
return
if bool(_adventure_mode_get.call()) and adventure != null:
if not adventure.continue_to_next_level():
_fatal_screen.call("关卡推进失败——见输出日志")
# 秘宝 item_id → 显示名投影(keystone_settle_text/替换子命名文案数据源——拼句归 Narrator);
# 迭代 22 T2 数据源换代:旧工坊行族退役,改读 treasure 装配词表 12 件
func item_name_lookup() -> Dictionary:
var lookup := {}
var meta = _meta_get.call()
if meta != null:
for row in db.treasure_attunement_rows():
lookup[String(row.get("item_id", ""))] = String(row.get("display_name", ""))
return lookup