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

242 lines
12 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
# 提升三选一与替换子交互控制器(迭代 13 T4/D4——二期家族① 自 main.gd 迁出;规范 1.1 单一职责):
# 三选一停点编排(presented→pick→confirm/refresh→resolved→continue)+ 同调位替换子态
#(presented→chosen)+ TP0 令牌池卡片文案装配("kind|tier|feat_id" 复合键)+ 工坊显示名投影。
# 装配纪律(ADR-0001,仿 treasure_interact_controller 先例):信号连接不在此发生——curio_overlay
# 四信号与 adventure 两信号由组合根接至本类 on_* 入口;模态栈/演播后排窗/呈现刷新/终局面/
# 推进失败 fatal 经组合根回调注入;adventure/runner 随局更替——现值回调注入;兑付写调用唯一
# 经 adventure.pick_curio/apply_node_curio/resolve_curio_replace(core 侧单口,本类零直写)。
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=奖励节点→回图选路)
var _last_replace_items: Array = [] # 替换子态续玩重建缓存(treasure_replace_presented 载荷)
var _last_replace_incoming := {} # 同上——incoming 记录
func _init(db_ref, meta_get: Callable, curio_overlay_ref: CurioPickOverlay, end_overlay_ref: BattleEndOverlay,
board_view_ref: BoardView, toast_ref: ToastStack, save_get: Callable,
adventure_get: Callable, runner_get: Callable, adventure_mode_get: Callable,
open_modal: Callable, open_modal_after_playback: Callable, close_modal: Callable,
refresh_presentations: Callable, fatal_screen: Callable, gp_summary: Callable) -> void:
db = db_ref
_meta_get = meta_get
curio_overlay = curio_overlay_ref
end_overlay = end_overlay_ref
board_view = board_view_ref
toast = toast_ref
_save_get = save_get
_adventure_get = adventure_get
_runner_get = runner_get
_adventure_mode_get = adventure_mode_get
_open_modal = open_modal
_open_modal_after_playback = open_modal_after_playback
_close_modal = close_modal
_refresh_presentations = refresh_presentations
_fatal_screen = fatal_screen
_gp_summary = gp_summary
# 停点在途判据(组合根 _on_battle_end_continue 分派面——战斗总结确认后开三选一)
func has_pending() -> bool:
return not _pending_curio.is_empty()
# 续玩停点恢复入口(true=已接手呈现,调用方中止后续分派)
func resume_if_pending() -> bool:
if _pending_curio.is_empty():
return false
show_pick()
return true
# 三选一停点(非末关通关恰一次):Boss 胜利先经战斗总结结算面(settlement-summary v1.1——钥石
# 增量行在其上),确认后进三选一面(level-clear.md v2);续玩停点恢复同入口(零自动推进契约)
func on_presented(payload: Dictionary) -> void:
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("replace_pending", false)):
# 续玩:替换子交互在途中断——重建三选一面后直落替换子态(候选不重抽)
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()
# 三选一面开出(战斗总结结算确认 / 续玩停点恢复;替换在途由 treasure_replace_presented 续发直落子态)
# 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;CurioPickOverlay 共用面,
# 道具件满位替换子交互同构)。原「秘宝投放渠道」三选一(迭代 7 过渡形态)随批退役。
func present_node_curio() -> void:
var adventure = _adventure_get.call()
_curio_context = "reward_node"
var node_draw: Dictionary = adventure.node_curio_candidates()
var node_cands: Array = node_draw.get("candidates", []) as Array
_open_modal.call(curio_overlay)
curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(node_cands, node_draw.get("aux_detail", []) as Array))
adventure.present_node_curio(node_cands, node_draw.get("aux_detail", []) as Array)
curio_overlay.set_refresh_info(adventure.attune_refresh_left()) # 迭代 11 第①批 T5b——刷新余量投喂
# 三选一面开出(战斗总结结算确认 / 续玩停点恢复;替换在途由 treasure_replace_presented 续发直落子态)
func show_pick() -> void:
var payload: Dictionary = _pending_curio
_open_modal.call(curio_overlay)
curio_overlay.open_pick(Narrator.curio_pick_header(payload), _curio_card_texts(payload.get("candidates", []) as Array, payload.get("aux_detail", []) as Array))
var adventure = _adventure_get.call()
curio_overlay.set_refresh_info(adventure.attune_refresh_left() if adventure != null else 0) # 迭代 11 第①批 T5b——刷新余量投喂
_refresh_presentations.call()
func _curio_card_texts(candidates: Array, treasure_detail: Array = []) -> Array:
# TP0 令牌池——候选 = {"kind","tier"} 令牌字典;卡片 id = "kind|tier|feat_id" 复合键(同 kind
# 大小两档/同档多 relic 可同屏,kind 单键不构成唯一身份),确认回程拆键(on_pick_confirmed)
var out: Array = []
var treasure_i := 0
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 == "relic":
text = Narrator.feat_card_text(db.feat_row(rid))
elif kind == "treasure_item":
var iid := String(treasure_detail[treasure_i]) if treasure_i < treasure_detail.size() else ""
treasure_i += 1
text = Narrator.bonus_card_text(kind, tier, int(db.adventure.curio_value(kind, tier)), _treasure_card_line(iid))
else:
text = Narrator.bonus_card_text(kind, tier, int(db.adventure.curio_value(kind, tier)))
out.append({"id": cid, "text": text})
return out
# 道具件卡片行(掷定前置——具体道具名 + 效果摘要;空 iid = 无预掷回退态显示通用文案)
func _treasure_card_line(iid: String) -> String:
if iid == "":
return ""
var row: Dictionary = db.treasure_item_row(iid)
if row.is_empty():
return ""
var parts: Array[String] = []
for k in (row.get("effect_kinds", []) as Array):
if String(k) == "mine_reveal":
parts.append("真视")
elif String(k) == "reward_preview":
parts.append("预读")
elif String(k) == "ap_grant":
parts.append("AP +%d" % int(row.get("magnitude", 0)))
var tgt := String(row.get("target", "none"))
if tgt == "area_3x3":
parts.append("3×3")
elif tgt == "board_wide":
parts.append("全图")
return "%s · %s" % [String(row.get("display_name", iid)), " ".join(parts)]
# 三选一刷新(迭代 11 第①批 T5b——P1 甲 X/Y 双源;通关停点经重发 curio_choice_presented 自然重建,
# 节点停点在此读在途态重 present——零自动推进契约不破,停点仍在途)
func on_refresh() -> void:
var adventure = _adventure_get.call()
if adventure == null:
return
if not adventure.refresh_curio():
return
if _curio_context == "reward_node":
var st: Dictionary = adventure.node_curio_pending_state()
curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st.get("candidates", []) as Array, st.get("aux_detail", []) as Array))
curio_overlay.set_refresh_info(adventure.attune_refresh_left())
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] 三选一兑付被拒——见输出日志")
func on_treasure_replace_presented(items: Array, incoming: Dictionary) -> void:
_last_replace_items = items.duplicate(true)
_last_replace_incoming = incoming.duplicate(true)
if not curio_overlay.visible:
_open_modal.call(curio_overlay) # 续玩替换在途直落——三选一面未开时先入栈(模态栈纪律)
_open_replace(items, incoming)
func _open_replace(items: Array, incoming: Dictionary) -> void:
var named: Array = []
for it in items:
var rec: Dictionary = it.duplicate()
rec["display_name"] = String(item_name_lookup().get(String(rec.get("item_id", "")), String(rec.get("item_id", ""))))
named.append(rec)
curio_overlay.open_replace(named, String(item_name_lookup().get(String(incoming.get("item_id", "")), String(incoming.get("item_id", "")))),
named.size(), Attunement.slots(db, _save_get.call())) # 迭代 11 第①批 T7——呈现读数源切谓词表求值(过渡键退役)
func on_replace_chosen(discard: int) -> void:
var adventure = _adventure_get.call()
if adventure == null or not adventure.resolve_curio_replace(discard):
push_error("[bonus_interact] 同调位替换受理被拒——见输出日志")
# 兑付反馈(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 == "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)
func item_name_lookup() -> Dictionary:
var lookup := {}
var meta = _meta_get.call()
if meta != null:
for row in db.meta_workshop_items():
lookup[String(row.get("item_id", ""))] = String(row.get("display_name", ""))
return lookup