## 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 # u13 引用束注入:16 个 _init 抄写成员删除——正文读点改址 ctx 束(u20 三束键名; # _pending_curio/_curio_context 纯内部态保持宿主) var _pending_curio := {} # 三选一停点载荷暂存(战斗总结结算确认后开三选一面;续玩停点复用) var _curio_context := "level_clear" # 三选一面上下文(level_clear=关卡完成停点→推进下一关 / reward_node=奖励节点→回图选路) func _init(ctx: CurioFlowContext) -> void: # 构造参数收拢 context 对象(16 参长签名收一处命名面) db = ctx.db ctx.flow.fatal_screen = ctx.fatal_screen ctx.flow.gp_summary = ctx.gp_summary # 停点在途判据(组合根 _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 = _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(), "%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 = _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 = _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 = _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 = _adventure_get.call() if adventure == null or not adventure.stop().curio_stop_pending(): return var runner = _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.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 = _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 = _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 = _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