标识符面:flag_*→seal_ 族(含 Godot 信号 flag_judged→seal_judged 与连接/回调同名、 FACTS 两键 flags_count→seals_count/dug_under_flag→dug_under_seal 全链、 .tres 键名与 schema .gd 同批同步);显式豁免零改动=size_flags_*(Godot 布局属性)+_fatal_flag。 中文面:金币→GP(金币袋→GP 袋)/插旗→封印/雷格→危险格/惩罚格→危险格/旗→封印·封印印记族, game/ 内 *.gd/.tres/.md/.json 四类禁用词与 flag 双双零命中(V1/V4)。 守卫字面量不动(被检查的禁字不是使用):smoke_all V21 零「雷/旗」检查面、 ui_font_asset_test 字形集攻/雷/旗/封 保持原断言逻辑。 代拍板:板面单字槽 glyph「旗」→「封」(禁字纪律+单字位宽,消费面四处同名随动)。 素材换代(2026-10-18 用户裁案甲):assets/art/board/flag_seal.png→seal_mark.png(素材不入 git, 盘上改名+导入重刷),代码两处 res:// 字符串+COL_FLAG/CURSOR_FLAG 常量同步, 闸件四处随批(run_all_gates.sh/render_gate.sh/golden_baseline_refresh.sh/path_check.py)。 anchor.json 键名手改,重锚与改前改后 diff 随本卡收口(同裁案甲)。 基线读数(改前,主区 ed2dfaf6):冒烟 1151 检查 0 失败/单元 34 套全绿/探针 --click 存量红仅 screen_flow_probe(user:// 断点污染在册已知项)。 改后同批读数(本 worktree):冒烟 1151 检查 0 失败/探针 --click 同一存量红、其余全绿。
257 lines
13 KiB
GDScript
257 lines
13 KiB
GDScript
## 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("GP不足——刷新失败", "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
|