From 4c7931064e2726a31aaf1f77062ec8e51328515f Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sun, 4 Oct 2026 07:38:48 +0800 Subject: [PATCH] =?UTF-8?q?T4(D4):=20main.gd=20=E4=BA=8C=E6=9C=9F=E7=98=A6?= =?UTF-8?q?=E8=BA=AB=E2=80=94=E2=80=94=E5=AE=B6=E6=97=8F=E2=91=A0=E4=B8=89?= =?UTF-8?q?=E9=80=89=E4=B8=80/=E6=9B=BF=E6=8D=A2=E5=AD=90+=E5=AE=B6?= =?UTF-8?q?=E6=97=8F=E2=91=A2=E8=90=A5=E5=9C=B0=E5=95=86=E5=BA=97=E6=8A=95?= =?UTF-8?q?=E5=BD=B1=E6=8B=86=E4=BB=B6=EF=BC=881378=E2=86=921119=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新件两枚(RefCounted,仿 aux_interact_controller 房内范式): - bonus_interact_controller.gd(241行):三选一停点编排 on_presented/present_node_bonus/ show_pick/on_refresh/on_pick_confirmed/on_aux_replace_presented/on_replace_chosen/ on_resolved/on_continue + TP0 复合键卡片装配 + 工坊名投影;_pending_bonus/_bonus_context 随迁;兑付写调用唯一经 adventure.pick_bonus/apply_node_bonus/resolve_bonus_replace。 - camp_shop_presenter.gd(172行):营地开合/快照/锻造·同调·天赋树·Fork 四写口 + 商店 pool/product 双面购买/离开;写调用唯一经 MetaProgression 原语与 LevelRunner.buy_*。 接线留组合根(ADR-0001):overlay/adventure 十三信号由组合根接至控制器 on_* 入口; 模态栈/演播后排窗/呈现刷新/fatal 经回调注入;meta/save_manager/adventure/runner 现值 回调注入——探针 isolate_save 换注 main.meta 实证 ctor 捕获会记错账(营地购买未出账), 现值回调为房内先例口径。 家族②debug 接线收口=契约3 组合根绑定面(settlement 写+_wire_battle+override/seed 取数 在组合根 UI 态)不迁移,节头注记记档——任务书族级估 ~50 行与 ADR-0001 相抵,处置在 代拍白名单「拆件归属」内。 探针缝保缝:main._on_camp_closed/_camp_data 保缝转发(click/ui_probe 直呼面); bonus_overlay 主类公有字段维持。 行数对照:1378→1119(净减 259);对照任务书锚 ~900±50(净减 ~430)差 -160,归因= 三家族可迁清单实测上限 ~336 行(任务书自估 315),锚位与族级清单不自洽——差异进出口 报告呈用户。闸:三种子 click 绿(7/9117、11/10202、23/9156)+冒烟 718/0+run_all 15+ui_probe。 --- .../presentation/bonus_interact_controller.gd | 241 ++++++++++++ .../bonus_interact_controller.gd.uid | 1 + game/presentation/camp_shop_presenter.gd | 172 +++++++++ game/presentation/camp_shop_presenter.gd.uid | 1 + game/presentation/main.gd | 353 +++--------------- 5 files changed, 462 insertions(+), 306 deletions(-) create mode 100644 game/presentation/bonus_interact_controller.gd create mode 100644 game/presentation/bonus_interact_controller.gd.uid create mode 100644 game/presentation/camp_shop_presenter.gd create mode 100644 game/presentation/camp_shop_presenter.gd.uid diff --git a/game/presentation/bonus_interact_controller.gd b/game/presentation/bonus_interact_controller.gd new file mode 100644 index 00000000..6abe4178 --- /dev/null +++ b/game/presentation/bonus_interact_controller.gd @@ -0,0 +1,241 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# 提升三选一与替换子交互控制器(迭代 13 T4/D4——二期家族① 自 main.gd 迁出;规范 1.1 单一职责): +# 三选一停点编排(presented→pick→confirm/refresh→resolved→continue)+ 同调位替换子态 +#(presented→chosen)+ TP0 令牌池卡片文案装配("kind|tier|relic_id" 复合键)+ 工坊显示名投影。 +# 装配纪律(ADR-0001,仿 aux_interact_controller 先例):信号连接不在此发生——bonus_overlay +# 四信号与 adventure 两信号由组合根接至本类 on_* 入口;模态栈/演播后排窗/呈现刷新/终局面/ +# 推进失败 fatal 经组合根回调注入;adventure/runner 随局更替——现值回调注入;兑付写调用唯一 +# 经 adventure.pick_bonus/apply_node_bonus/resolve_bonus_replace(core 侧单口,本类零直写)。 +extends RefCounted + +const Narrator := preload("res://presentation/battle_narrator.gd") + +var db +var _meta_get: Callable # () → 元进度账本现值(组合根 lambda——探针 isolate_save 换注面,迭代13 T4 实证) +var bonus_overlay: BonusPickOverlay +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_bonus := {} # 三选一停点载荷暂存(迭代 10——战斗总结结算确认后开三选一面;续玩停点复用) +var _bonus_context := "level_clear" # 三选一面上下文(level_clear=通关停点→推进下一关 / reward_node=奖励节点→回图选路) +var _last_replace_items: Array = [] # 替换子态续玩重建缓存(aux_replace_presented 载荷) +var _last_replace_incoming := {} # 同上——incoming 记录 + + +func _init(db_ref, meta_get: Callable, bonus_overlay_ref: BonusPickOverlay, 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 + bonus_overlay = bonus_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_bonus.is_empty() + + +# 续玩停点恢复入口(true=已接手呈现,调用方中止后续分派) +func resume_if_pending() -> bool: + if _pending_bonus.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_bonus = payload.duplicate(true) + _bonus_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() + + +# 三选一面开出(战斗总结结算确认 / 续玩停点恢复;替换在途由 aux_replace_presented 续发直落子态) +# 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;BonusPickOverlay 共用面, +# 道具件满位替换子交互同构)。原「魔法道具投放渠道」三选一(迭代 7 过渡形态)随批退役。 +func present_node_bonus() -> void: + var adventure = _adventure_get.call() + _bonus_context = "reward_node" + var node_draw: Dictionary = adventure.node_bonus_candidates() + var node_cands: Array = node_draw.get("candidates", []) as Array + _open_modal.call(bonus_overlay) + bonus_overlay.open_pick(Narrator.node_bonus_header(), _bonus_card_texts(node_cands, node_draw.get("aux_detail", []) as Array)) + adventure.present_node_bonus(node_cands, node_draw.get("aux_detail", []) as Array) + bonus_overlay.set_refresh_info(adventure.attune_refresh_left()) # 迭代 11 第①批 T5b——刷新余量投喂 + + +# 三选一面开出(战斗总结结算确认 / 续玩停点恢复;替换在途由 aux_replace_presented 续发直落子态) +func show_pick() -> void: + var payload: Dictionary = _pending_bonus + _open_modal.call(bonus_overlay) + bonus_overlay.open_pick(Narrator.bonus_pick_header(payload), _bonus_card_texts(payload.get("candidates", []) as Array, payload.get("aux_detail", []) as Array)) + var adventure = _adventure_get.call() + bonus_overlay.set_refresh_info(adventure.attune_refresh_left() if adventure != null else 0) # 迭代 11 第①批 T5b——刷新余量投喂 + _refresh_presentations.call() + + +func _bonus_card_texts(candidates: Array, aux_detail: Array = []) -> Array: + # TP0 令牌池——候选 = {"kind","tier"} 令牌字典;卡片 id = "kind|tier|relic_id" 复合键(同 kind + # 大小两档/同档多 relic 可同屏,kind 单键不构成唯一身份),确认回程拆键(on_pick_confirmed) + var out: Array = [] + var aux_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("relic_id", "")) + var cid := "%s|%s|%s" % [kind, tier, rid] + var text := "" + if kind == "relic": + text = Narrator.relic_card_text(db.relic_row(rid)) + elif kind == "aux_item": + var iid := String(aux_detail[aux_i]) if aux_i < aux_detail.size() else "" + aux_i += 1 + text = Narrator.bonus_card_text(kind, tier, int(db.adventure.bonus_value(kind, tier)), _aux_card_line(iid)) + else: + text = Narrator.bonus_card_text(kind, tier, int(db.adventure.bonus_value(kind, tier))) + out.append({"id": cid, "text": text}) + return out + + +# 道具件卡片行(掷定前置——具体道具名 + 效果摘要;空 iid = 无预掷回退态显示通用文案) +func _aux_card_line(iid: String) -> String: + if iid == "": + return "" + var row: Dictionary = db.aux_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 双源;通关停点经重发 bonus_choice_presented 自然重建, +# 节点停点在此读在途态重 present——零自动推进契约不破,停点仍在途) +func on_refresh() -> void: + var adventure = _adventure_get.call() + if adventure == null: + return + if not adventure.refresh_bonus(): + return + if _bonus_context == "reward_node": + var st: Dictionary = adventure.node_bonus_pending_state() + bonus_overlay.open_pick(Narrator.node_bonus_header(), _bonus_card_texts(st.get("candidates", []) as Array, st.get("aux_detail", []) as Array)) + bonus_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|relic_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_bonus(kind, tier, rid) if _bonus_context == "reward_node" else adventure.pick_bonus(kind, tier, rid) + if not ok: + push_error("[bonus_interact] 三选一兑付被拒——见输出日志") + + +func on_aux_replace_presented(items: Array, incoming: Dictionary) -> void: + _last_replace_items = items.duplicate(true) + _last_replace_incoming = incoming.duplicate(true) + if not bonus_overlay.visible: + _open_modal.call(bonus_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) + bonus_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_bonus_replace(discard): + push_error("[bonus_interact] 同调位替换受理被拒——见输出日志") + + +# 兑付反馈(bonus_resolved 到达——结果行上屏,唯一出口 = 继续) +func on_resolved(result: Dictionary) -> void: + _pending_bonus = {} + bonus_overlay.show_result(Narrator.bonus_result_text(result)) + _refresh_presentations.call() + + +# 继续钮:收三选一面 → 上下文分派(通关停点=推进下一关 / 奖励节点=兑付确认回图选路——零自动推进) +func on_continue() -> void: + _close_modal.call(bonus_overlay) + bonus_overlay.close_pick() + var adventure = _adventure_get.call() + if _bonus_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 diff --git a/game/presentation/bonus_interact_controller.gd.uid b/game/presentation/bonus_interact_controller.gd.uid new file mode 100644 index 00000000..137a8dcc --- /dev/null +++ b/game/presentation/bonus_interact_controller.gd.uid @@ -0,0 +1 @@ +uid://cp308xdkheml3 diff --git a/game/presentation/camp_shop_presenter.gd b/game/presentation/camp_shop_presenter.gd new file mode 100644 index 00000000..5662abf6 --- /dev/null +++ b/game/presentation/camp_shop_presenter.gd @@ -0,0 +1,172 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# 营地/商店投影控制器(迭代 13 T4/D4——二期家族③ 自 main.gd 迁出;规范 1.1 单一职责): +# 营地面开合/快照投影/锻造·同调·天赋树·Fork 购买与切换 + 商店房购买/离开(pool 与 product 双面)。 +# 装配纪律(ADR-0001,仿 aux_interact_controller 先例):信号连接不在此发生——campbtn/ +# camp_overlay 四信号/shop_overlay 两信号由组合根接至本类 on_* 入口;模态栈/呈现刷新/续玩按钮 +# 刷新经组合根回调注入;runner 随局更替——现值回调注入;写调用唯一经 MetaProgression 购买原语 +# 与 LevelRunner.buy_*(core 侧单口,本类零直写——拒绝可见 pattern 1 兜底 toast 保留原地)。 +extends RefCounted + +var db +var _meta_get: Callable # () → 元进度账本现值(组合根 lambda——探针 isolate_save 换注面,迭代13 T4 实证) +var _save_get: Callable # () → 存档承载现值(同上——营地 recent 摘要读数面) +var camp_overlay: CampOverlay +var shop_overlay: ShopOverlay +var toast: ToastStack +var start_overlay: Control # 开始层互斥面(开合仿 TunePanel 先例——非模态且树序在后,规范 2.3) +var logger # 试玩日志(ux_reject 取数面) +var _runner_get: Callable # () → 关卡推进器现值(组合根 lambda——随局更替) +var _open_modal: Callable +var _close_modal: Callable +var _refresh_presentations: Callable +var _refresh_resume_button: Callable + + +func _init(db_ref, meta_get: Callable, save_get: Callable, camp_overlay_ref: CampOverlay, + shop_overlay_ref: ShopOverlay, toast_ref: ToastStack, start_overlay_ref: Control, + logger_ref, runner_get: Callable, open_modal: Callable, close_modal: Callable, + refresh_presentations: Callable, refresh_resume_button: Callable) -> void: + db = db_ref + _meta_get = meta_get + _save_get = save_get + camp_overlay = camp_overlay_ref + shop_overlay = shop_overlay_ref + toast = toast_ref + start_overlay = start_overlay_ref + logger = logger_ref + _runner_get = runner_get + _open_modal = open_modal + _close_modal = close_modal + _refresh_presentations = refresh_presentations + _refresh_resume_button = refresh_resume_button + + +# 打开营地(主菜单入口——开合与开始层互斥仿 TunePanel 先例:开始覆盖层非模态且树序在后, +# 规范 2.3 遮挡纪律;打开时先刷工坊解锁——达标未解锁就地入账) +func open_camp() -> void: + var meta = _meta_get.call() + if meta == null: + return + meta.refresh_workshop_unlocks() + start_overlay.visible = false + _open_modal.call(camp_overlay) # 模态栈单口(T3 收口批——登记层开合一律走栈) + camp_overlay.open_camp(camp_data()) + _refresh_presentations.call() + + +func on_camp_closed() -> void: + _close_modal.call(camp_overlay) # 模态栈单口(T3 收口批) + start_overlay.visible = true + _refresh_resume_button.call() # 迭代 8 卡②——回菜单按断点在档态刷新 + _refresh_presentations.call() # 关闭后刷新顶栏(工坊解锁可能变了余额侧读数面) + + +# 营地快照投影(纯读——锻造三行/工坊六行/余额/最近冒险;拼句归 CampOverlay,账面归 MetaProgression) +func camp_data() -> Dictionary: + var meta = _meta_get.call() + var forge_rows: Array[Dictionary] = [] + if meta != null: + for attr in MetaTable.FORGE_ATTRS: + var row: Dictionary = db.meta_forge_row(attr) + forge_rows.append({ + "attr": attr, + "display_name": String(row.get("display_name", "")), + "level": meta.forge_level(attr), + "flat": int(row.get("flat_value", 0)), + "price": meta.forge_price(attr), + }) + return { + "balance": meta.keystone_balance() if meta != null else 0, + "forge": forge_rows, + "workshop": meta.workshop_status() if meta != null else [], + "attune": meta.attune_camp_status() if meta != null else {}, # 迭代 11 第①批 T4——同调装配面投影 + "tree": meta.tree_camp_rows() if meta != null else [], # 迭代 12 T9——天赋树面投影 + "fork_groups": meta.tree_fork_groups() if meta != null else [], + "recent": _save_get.call().recent_summaries() if _save_get.call() != null else [], + } + + +func on_camp_buy(attr: String) -> void: + var meta = _meta_get.call() + if meta == null: + return + if attr == "attune_refresh": + # Y 刷新升级行购买(迭代 11 第①批 T4——P1 甲;按钮禁用为常态防线,兜底拒绝可见 pattern 1) + if not meta.buy_attune_refresh(): + toast.show_toast("钥石不足——买不起这次升级", "warn") + return + camp_overlay.refresh_camp(camp_data()) + _refresh_presentations.call() + return + if not meta.buy_forge(attr): + # 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1) + toast.show_toast("钥石不足——买不起这件强化", "warn") + return + camp_overlay.refresh_camp(camp_data()) + _refresh_presentations.call() + + +# 同调装配切换(迭代 11 第①批 T4——装配关系唯一写口经 MetaProgression.toggle_attunement;满位兜底拒绝可见) +func on_camp_attune_toggle(item_id: String) -> void: + var meta = _meta_get.call() + if meta == null: + return + if not meta.toggle_attunement(item_id): + toast.show_toast("同调位已满——先卸下再装配", "warn") + return + camp_overlay.refresh_camp(camp_data()) + + +# 天赋树购买(迭代 12 T9——写口唯一归 MetaProgression.tree_buy/tree_buy_fork;Fork 节点购=激活侧) +func on_camp_tree_buy(node_id: String) -> void: + var meta = _meta_get.call() + if meta == null: + return + var ok: bool = meta.tree_buy_fork(node_id) if String(db.tree_node(node_id).get("kind", "")) == "fork" else meta.tree_buy(node_id) + if not ok: + toast.show_toast("购买被拒——前置/行门槛/余额不足", "warn") + return + camp_overlay.refresh_camp(camp_data()) + _refresh_presentations.call() + + +# A/B 免费切换(迭代 12 T8——同组换侧零币;未激活组走购买面) +func on_camp_fork_toggle(group: String, side: String) -> void: + var meta = _meta_get.call() + if meta == null: + return + if not meta.tree_toggle_fork(group, side): + toast.show_toast("切换被拒——未激活组请先购买", "warn") + return + camp_overlay.refresh_camp(camp_data()) + _refresh_presentations.call() + + +func on_shop_buy(product_id: String) -> void: + var runner = _runner_get.call() + if runner == null: + return + if runner.is_shop_pool(): + # pool 面:买 1 → 核心全重抽——货架整架重投喂(重抽反馈;余额不足核心拒绝零副作用) + if not runner.buy_pool_item(product_id): + logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"}) + toast.show_toast("金币不足——买不起这件商品", "warn") + return + shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp)) + _refresh_presentations.call() + return + if not runner.buy_product(product_id): + # 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1) + logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"}) + toast.show_toast("金币不足——买不起这件商品", "warn") + return + shop_overlay.refresh_state(runner.shop_products(), int(runner.gp)) + _refresh_presentations.call() + + +func on_shop_leave() -> void: + _close_modal.call(shop_overlay) + var runner = _runner_get.call() + if runner != null: + # 复用兑付确认纪律(接口漂移清偿):离开 ≠ 推进——确认后经 choices_presented 弹出选路口 + runner.continue_after_node() diff --git a/game/presentation/camp_shop_presenter.gd.uid b/game/presentation/camp_shop_presenter.gd.uid new file mode 100644 index 00000000..f474e102 --- /dev/null +++ b/game/presentation/camp_shop_presenter.gd.uid @@ -0,0 +1 @@ +uid://b3t0ocw10p5rs diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 691f1587..baca32f8 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -38,6 +38,8 @@ const DebugSessionScript := preload("res://presentation/debug_session.gd") const RowPacer := preload("res://presentation/row_pacer.gd") # 结算行排播泵(T3 拆件批) const AuxInteractController := preload("res://presentation/aux_interact_controller.gd") # 魔法道具两步交互(T3 拆件批) const GpShadowLedgerScript := preload("res://presentation/gp_shadow_ledger.gd") # GP 影子账本(T3 拆件批) +const BonusInteractController := preload("res://presentation/bonus_interact_controller.gd") # 三选一与替换子交互(T4/D4 拆件批) +const CampShopPresenter := preload("res://presentation/camp_shop_presenter.gd") # 营地/商店投影(T4/D4 拆件批) const SaveManagerScript := preload("res://core/save_manager.gd") # 存档承载(preload 避开全局类缓存时序——与 DebugSessionScript 同款) const MetaProgressionScript := preload("res://core/meta_progression.gd") # 元进度账本服务(元进度实现批——同 preload 纪律) @@ -64,9 +66,7 @@ var _settlement_stop_pending: bool: # 挂起态属性化(探针/冒烟直读 _settlement_stop_timer.disarm() var _pending_candidates: Array = [] # 结算框挂起期的选路候选暂存(choices_presented 载荷) var _last_gain_amount := 0 # 最近一笔钱包入账(卡⑥ GP 增量取数面——wallet_changed gain 载荷) -var _pending_bonus := {} # 三选一停点载荷暂存(迭代 10——战斗总结结算确认后开三选一面;续玩停点复用) var bonus_overlay: BonusPickOverlay # 提升三选一覆盖层(迭代 10——level-clear.md v2 选择面承载;追加批起奖励节点共用) -var _bonus_context := "level_clear" # 三选一面上下文(level_clear=通关停点→推进下一关 / reward_node=奖励节点→回图选路) var _start_overlay: Control var _end_overlay: BattleEndOverlay var _inter_overlay: Control @@ -91,6 +91,8 @@ var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道 # T3 拆件批(2026-10-02)——三件迁出后的组合根持有面(职责见各类头注;装配见 _build_ui) var _row_pacer = null # 结算行排播泵(row_pacer.gd——本类只入队/排空/读队列) var _aux = null # 魔法道具两步交互(aux_interact_controller.gd——本类只接线与转发意图) +var _bonus = null # 三选一与替换子交互(bonus_interact_controller.gd——本类只接线与回调注入) +var _camp_pres = null # 营地/商店投影(camp_shop_presenter.gd——本类只接线、探针缝保缝转发) var _gp_ledger := GpShadowLedgerScript.new() # GP 冒险钱包影子账(gp_shadow_ledger.gd——wallet 事件投喂) # GP 影子账委托面(探针/终局面板取数保留——ui_probe 合成注入经 set 转发账本,读面即账本实况) @@ -259,13 +261,6 @@ func _build_ui() -> void: node_overlay.continue_pressed.connect(_on_node_continue) level_screen.route_chosen.connect(_on_route_chosen) level_screen.abandon_requested.connect(_on_abandon_requested) # 放弃入口(2026-09-21 用户拍板——台账 119) - shop_overlay.buy_requested.connect(_on_shop_buy) - shop_overlay.leave_pressed.connect(_on_shop_leave) - camp_overlay.buy_requested.connect(_on_camp_buy) - camp_overlay.attune_toggled.connect(_on_camp_attune_toggle) - camp_overlay.tree_buy_requested.connect(_on_camp_tree_buy) - camp_overlay.tree_fork_toggled.connect(_on_camp_fork_toggle) - camp_overlay.closed.connect(_on_camp_closed) _end_overlay.review_pressed.connect(_on_end_review) _end_overlay.continue_pressed.connect(_on_battle_end_continue) tune_panel.closed.connect(_close_tune_panel) @@ -277,10 +272,17 @@ func _build_ui() -> void: # 提升三选一覆盖层(迭代 10——z 序 = 节点层之上、终局层之下;确认后同拍推进) bonus_overlay = BonusPickOverlay.new() add_child(bonus_overlay) - bonus_overlay.pick_confirmed.connect(_on_bonus_pick_confirmed) - bonus_overlay.replace_chosen.connect(_on_bonus_replace_chosen) - bonus_overlay.continue_pressed.connect(_on_bonus_continue) - bonus_overlay.refresh_requested.connect(_on_bonus_refresh) + # 提升三选一与替换子交互(T4/D4 拆件批——迁 bonus_interact_controller.gd;本类只接线与回调注入, + # ADR-0001——模态栈/演播后排窗/呈现刷新/fatal 经回调注入,adventure/runner 现值回调) + _bonus = BonusInteractController.new(db, func() -> Variant: return meta, bonus_overlay, _end_overlay, board_view, + board_zone.toast, func() -> Variant: return save_manager, func() -> Variant: return adventure, + func() -> Variant: return runner, func() -> Variant: return _adventure_mode, + _open_modal, _open_modal_after_playback, _close_modal, _refresh_presentations, + _fatal_screen, func() -> String: return _gp_summary_text({"gp_dropped": _last_gain_amount})) + bonus_overlay.pick_confirmed.connect(_bonus.on_pick_confirmed) + bonus_overlay.replace_chosen.connect(_bonus.on_replace_chosen) + bonus_overlay.continue_pressed.connect(_bonus.on_continue) + bonus_overlay.refresh_requested.connect(_bonus.on_refresh) move_child(bonus_overlay, _end_overlay.get_index()) var start: Dictionary = UiAssembler.build_start_overlay(self, db) @@ -292,7 +294,19 @@ func _build_ui() -> void: start.tunebtn.pressed.connect(_show_tune_panel) start.dbgbtn.pressed.connect(_start_debug_levels) start.dbgbtn.visible = OS.get_environment("DS_DEBUG") == "1" # 迭代 8 卡④——调试入口默认隐藏门控(DS_DEBUG=1 点亮) - start.campbtn.pressed.connect(_open_camp) + # 营地/商店投影(T4/D4 拆件批——迁 camp_shop_presenter.gd;本类只接线、探针缝保缝转发; + # 购买写调用唯一经 MetaProgression/LevelRunner core 单口——组合根 lambda 现值注入) + _camp_pres = CampShopPresenter.new(db, func() -> Variant: return meta, func() -> Variant: return save_manager, camp_overlay, shop_overlay, + board_zone.toast, _start_overlay, logger, func() -> Variant: return runner, + _open_modal, _close_modal, _refresh_presentations, _refresh_resume_button) + shop_overlay.buy_requested.connect(_camp_pres.on_shop_buy) + shop_overlay.leave_pressed.connect(_camp_pres.on_shop_leave) + camp_overlay.buy_requested.connect(_camp_pres.on_camp_buy) + camp_overlay.attune_toggled.connect(_camp_pres.on_camp_attune_toggle) + camp_overlay.tree_buy_requested.connect(_camp_pres.on_camp_tree_buy) + camp_overlay.tree_fork_toggled.connect(_camp_pres.on_camp_fork_toggle) + camp_overlay.closed.connect(_camp_pres.on_camp_closed) + start.campbtn.pressed.connect(_camp_pres.open_camp) start.settingsbtn.pressed.connect(_open_settings) # 迭代 8 卡④——最小设置面入口 start.quitbtn.pressed.connect(_quit_game) # 二轮修改批 D1——退出游戏提级一级菜单 @@ -435,9 +449,9 @@ func _wire_adventure() -> void: adventure.choices_presented.connect(_on_choices_presented) adventure.wallet_changed.connect(_on_wallet_changed) # 迭代 10 奖励重构批——big_reward_paid 退役,三选一信号族接线 - adventure.bonus_choice_presented.connect(_on_bonus_presented) - adventure.bonus_resolved.connect(_on_bonus_resolved) - adventure.aux_replace_presented.connect(_on_aux_replace_presented) + adventure.bonus_choice_presented.connect(_bonus.on_presented) + adventure.bonus_resolved.connect(_bonus.on_resolved) + adventure.aux_replace_presented.connect(_bonus.on_aux_replace_presented) adventure.adventure_finished.connect(_on_adventure_finished) adventure.level_started.connect(_on_adventure_level_started) adventure.set_save_manager(save_manager) # 断点承载注入(迭代 8 卡①——停点快照写盘;调试/模拟不经过此路径) @@ -490,15 +504,9 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void: _show_board_fx_for_node(t) log_view.append_bb(Narrator.node_banner(runner, node, t)) elif t == "reward": - # 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;BonusPickOverlay 共用面, - # 道具件满位替换子交互同构)。原「魔法道具投放渠道」三选一(迭代 7 过渡形态)随批退役。 - _bonus_context = "reward_node" - var node_draw: Dictionary = adventure.node_bonus_candidates() - var node_cands: Array = node_draw.get("candidates", []) as Array - _open_modal(bonus_overlay) - bonus_overlay.open_pick(Narrator.node_bonus_header(), _bonus_card_texts(node_cands, node_draw.get("aux_detail", []) as Array)) - adventure.present_node_bonus(node_cands, node_draw.get("aux_detail", []) as Array) - bonus_overlay.set_refresh_info(adventure.attune_refresh_left()) # 迭代 11 第①批 T5b——刷新余量投喂 + # 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;BonusPickOverlay 共用面)。 + # T4/D4 拆件:呈现编排归 bonus_interact_controller.present_node_bonus(_bonus_context 随迁)。 + _bonus.present_node_bonus() log_view.append_bb(Narrator.reward_line()) elif t == "rest": var healed: int = int(info.get("healed", 0)) @@ -521,151 +529,6 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void: # ---- 冒险模式终局面(三选一选择面 / 冒险结算屏——迭代 10 奖励重构批)---- -# 三选一停点(非末关通关恰一次):Boss 胜利先经战斗总结结算面(settlement-summary v1.1——钥石 -# 增量行在其上),确认后进三选一面(level-clear.md v2);续玩停点恢复同入口(零自动推进契约) -func _on_bonus_presented(payload: Dictionary) -> void: - if int(payload.get("aux_cleared", 0)) > 0: - board_zone.toast.show_toast("上关剩余 %d 件魔法道具已随关卡结束清空" % int(payload.get("aux_cleared", 0)), "info") # 迭代 7 T4 清空反馈 - _pending_bonus = payload.duplicate(true) - _bonus_context = "level_clear" - if bool(payload.get("replace_pending", false)): - # 续玩:替换子交互在途中断——重建三选一面后直落替换子态(候选不重抽) - _show_bonus_pick() - return - _end_overlay.open_victory(Narrator.battle_end_text("victory", false), "继续", - board_view.get_global_rect(), - "%s\n%s" % [_gp_summary_text({"gp_dropped": _last_gain_amount}), Narrator.bonus_keystone_line(payload)]) - _open_modal_after_playback(_end_overlay) - _refresh_presentations() - - -# 三选一面开出(战斗总结结算确认 / 续玩停点恢复;替换在途由 aux_replace_presented 续发直落子态) -func _show_bonus_pick() -> void: - var payload: Dictionary = _pending_bonus - _open_modal(bonus_overlay) - bonus_overlay.open_pick(Narrator.bonus_pick_header(payload), _bonus_card_texts(payload.get("candidates", []) as Array, payload.get("aux_detail", []) as Array)) - bonus_overlay.set_refresh_info(adventure.attune_refresh_left() if adventure != null else 0) # 迭代 11 第①批 T5b——刷新余量投喂 - _refresh_presentations() - -var _last_replace_items: Array = [] # 替换子态续玩重建缓存(aux_replace_presented 载荷) -var _last_replace_incoming := {} # 同上——incoming 记录 - - -func _bonus_card_texts(candidates: Array, aux_detail: Array = []) -> Array: - # TP0 令牌池——候选 = {"kind","tier"} 令牌字典;卡片 id = "kind|tier|relic_id" 复合键(同 kind - # 大小两档/同档多 relic 可同屏,kind 单键不构成唯一身份),确认回程拆键(_on_bonus_pick_confirmed) - var out: Array = [] - var aux_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("relic_id", "")) - var cid := "%s|%s|%s" % [kind, tier, rid] - var text := "" - if kind == "relic": - text = Narrator.relic_card_text(db.relic_row(rid)) - elif kind == "aux_item": - var iid := String(aux_detail[aux_i]) if aux_i < aux_detail.size() else "" - aux_i += 1 - text = Narrator.bonus_card_text(kind, tier, int(db.adventure.bonus_value(kind, tier)), _aux_card_line(iid)) - else: - text = Narrator.bonus_card_text(kind, tier, int(db.adventure.bonus_value(kind, tier))) - out.append({"id": cid, "text": text}) - return out - - -# 道具件卡片行(掷定前置——具体道具名 + 效果摘要;空 iid = 无预掷回退态显示通用文案) -func _aux_card_line(iid: String) -> String: - if iid == "": - return "" - var row: Dictionary = db.aux_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 双源;通关停点经重发 bonus_choice_presented 自然重建, -# 节点停点在此读在途态重 present——零自动推进契约不破,停点仍在途) -func _on_bonus_refresh() -> void: - if adventure == null: - return - if not adventure.refresh_bonus(): - return - if _bonus_context == "reward_node": - var st: Dictionary = adventure.node_bonus_pending_state() - bonus_overlay.open_pick(Narrator.node_bonus_header(), _bonus_card_texts(st.get("candidates", []) as Array, st.get("aux_detail", []) as Array)) - bonus_overlay.set_refresh_info(adventure.attune_refresh_left()) - - -func _on_bonus_pick_confirmed(item_id: String) -> void: - if adventure == null: - return - # TP0 令牌池——卡片 id "kind|tier|relic_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_bonus(kind, tier, rid) if _bonus_context == "reward_node" else adventure.pick_bonus(kind, tier, rid) - if not ok: - push_error("[组合根] 三选一兑付被拒——见输出日志") - - -func _on_aux_replace_presented(items: Array, incoming: Dictionary) -> void: - _last_replace_items = items.duplicate(true) - _last_replace_incoming = incoming.duplicate(true) - if not bonus_overlay.visible: - _open_modal(bonus_overlay) # 续玩替换在途直落——三选一面未开时先入栈(模态栈纪律) - _open_bonus_replace(items, incoming) - - -func _open_bonus_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) - bonus_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_manager)) # 迭代 11 第①批 T7——呈现读数源切谓词表求值(过渡键退役) - - -func _on_bonus_replace_chosen(discard: int) -> void: - if adventure == null or not adventure.resolve_bonus_replace(discard): - push_error("[组合根] 同调位替换受理被拒——见输出日志") - - -# 兑付反馈(bonus_resolved 到达——结果行上屏,唯一出口 = 继续) -func _on_bonus_resolved(result: Dictionary) -> void: - _pending_bonus = {} - bonus_overlay.show_result(Narrator.bonus_result_text(result)) - _refresh_presentations() - - -# 继续钮:收三选一面 → 上下文分派(通关停点=推进下一关 / 奖励节点=兑付确认回图选路——零自动推进) -func _on_bonus_continue() -> void: - _close_modal(bonus_overlay) - bonus_overlay.close_pick() - if _bonus_context == "reward_node": - if runner != null: - runner.continue_after_node() - return - if _adventure_mode and adventure != null: - if not adventure.continue_to_next_level(): - _fatal_screen("关卡推进失败——见输出日志") - - # 冒险结算(completed/failed/abandoned):复用 BattleEndOverlay——摘要五件事 + 钥石入账明细进文案 func _on_adventure_finished(summary: Dictionary) -> void: _level_active = false @@ -681,7 +544,7 @@ func _on_adventure_finished(summary: Dictionary) -> void: detail.assign(meta.settle_adventure(summary)) var text := Narrator.adventure_summary_text(summary) if not detail.is_empty(): - text += "\n" + Narrator.keystone_settle_text(detail, _item_name_lookup()) + text += "\n" + Narrator.keystone_settle_text(detail, _bonus.item_name_lookup()) if result == "completed": _end_overlay.open_victory(text, "返回冒险", board_view.get_global_rect(), "本局 GP 入账 · 合计 %d" % _adventure_gp_final) # 迭代 6 T7 卡⑩——GP 行只上冒险终局面板(C5) @@ -694,22 +557,12 @@ func _on_adventure_finished(summary: Dictionary) -> void: _refresh_presentations() -# 工坊 item_id → 显示名投影(keystone_settle_text 文案数据源——装配归本根,拼句归 Narrator) -func _item_name_lookup() -> Dictionary: - var lookup := {} - if meta != null: - for row in db.meta_workshop_items(): - lookup[String(row.get("item_id", ""))] = String(row.get("display_name", "")) - return lookup - - func _on_battle_end_continue() -> void: # 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石已在收束时入存档——存档序列化批) _close_modal(_end_overlay) _end_overlay.close() # 迭代 10——三选一停点挂起分支:战斗总结结算(Boss 胜利钥石增量行)确认后进三选一面 - if not _pending_bonus.is_empty(): - _show_bonus_pick() + if _bonus.resume_if_pending(): # T4/D4 拆件——三选一停点在途恢复入口(bonus_interact_controller) return # 卡⑥ 挂起分派:战斗胜利总结结算框的「下一节点」≠ 终局——进选路页(暂存候选恢复呈现) if _settlement_stop_pending: @@ -740,7 +593,10 @@ func _on_end_review() -> void: # ==================== 调试模式(连续两层——编排归 debug_session.gd; -# 单场遭遇用户入口已删(2026-09-13 用户裁决)——debug_session.start_single 原语仅探针使用)==================== +# 单场遭遇用户入口已删(2026-09-13 用户裁决)——debug_session.start_single 原语仅探针使用) +# T4/D3 收口注记:本节处理器(battle_created/battle_ready/override/seed)系组合根绑定面—— +# settlement 写与 _wire_battle=ADR-0001 组合根 essence,override/seed 取数面在组合根 UI 态 +#(tune_panel/_seed_edit)——不迁移,信号连接留本根(契约 3)。==================== func _start_debug_levels() -> void: tune_panel.reset_sliders() @@ -1028,25 +884,6 @@ func _on_abandon_requested() -> void: # ---- 营地覆盖层与钥石账本(元进度实现批——写调用唯一归组合根)---- -# 打开营地(主菜单入口——开合与开始层互斥仿 TunePanel 先例:开始覆盖层非模态且树序在后, -# 规范 2.3 遮挡纪律;打开时先刷工坊解锁——达标未解锁就地入账) -func _open_camp() -> void: - if meta == null: - return - meta.refresh_workshop_unlocks() - _start_overlay.visible = false - _open_modal(camp_overlay) # 模态栈单口(T3 收口批——登记层开合一律走栈) - camp_overlay.open_camp(_camp_data()) - _refresh_presentations() - - -func _on_camp_closed() -> void: - _close_modal(camp_overlay) # 模态栈单口(T3 收口批) - _start_overlay.visible = true - _refresh_resume_button() # 迭代 8 卡②——回菜单按断点在档态刷新 - _refresh_presentations() # 关闭后刷新顶栏(工坊解锁可能变了余额侧读数面) - - # ---- 最小设置面(迭代 8 卡④——入口/开合仿营地先例:开始层互斥;生效与持久化归 SettingsStore)---- func _open_settings() -> void: @@ -1060,110 +897,14 @@ func _on_settings_closed() -> void: _refresh_resume_button() -# 营地快照投影(纯读——锻造三行/工坊六行/余额/最近冒险;拼句归 CampOverlay,账面归 MetaProgression) +# ---- 探针缝保缝(T4/D4 拆件——click/ui_probe 营地段直呼组合根面;实现归 camp_shop_presenter.gd)---- + +func _on_camp_closed() -> void: + _camp_pres.on_camp_closed() + + func _camp_data() -> Dictionary: - var forge_rows: Array[Dictionary] = [] - if meta != null: - for attr in MetaTable.FORGE_ATTRS: - var row: Dictionary = db.meta_forge_row(attr) - forge_rows.append({ - "attr": attr, - "display_name": String(row.get("display_name", "")), - "level": meta.forge_level(attr), - "flat": int(row.get("flat_value", 0)), - "price": meta.forge_price(attr), - }) - return { - "balance": meta.keystone_balance() if meta != null else 0, - "forge": forge_rows, - "workshop": meta.workshop_status() if meta != null else [], - "attune": meta.attune_camp_status() if meta != null else {}, # 迭代 11 第①批 T4——同调装配面投影 - "tree": meta.tree_camp_rows() if meta != null else [], # 迭代 12 T9——天赋树面投影 - "fork_groups": meta.tree_fork_groups() if meta != null else [], - "recent": save_manager.recent_summaries() if save_manager != null else [], - } - - -func _on_camp_buy(attr: String) -> void: - if meta == null: - return - if attr == "attune_refresh": - # Y 刷新升级行购买(迭代 11 第①批 T4——P1 甲;按钮禁用为常态防线,兜底拒绝可见 pattern 1) - if not meta.buy_attune_refresh(): - board_zone.toast.show_toast("钥石不足——买不起这次升级", "warn") - return - camp_overlay.refresh_camp(_camp_data()) - _refresh_presentations() - return - if not meta.buy_forge(attr): - # 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1) - board_zone.toast.show_toast("钥石不足——买不起这件强化", "warn") - return - camp_overlay.refresh_camp(_camp_data()) - _refresh_presentations() - - -# 同调装配切换(迭代 11 第①批 T4——装配关系唯一写口经 MetaProgression.toggle_attunement;满位兜底拒绝可见) -func _on_camp_attune_toggle(item_id: String) -> void: - if meta == null: - return - if not meta.toggle_attunement(item_id): - board_zone.toast.show_toast("同调位已满——先卸下再装配", "warn") - return - camp_overlay.refresh_camp(_camp_data()) - - -# 天赋树购买(迭代 12 T9——写口唯一归 MetaProgression.tree_buy/tree_buy_fork;Fork 节点购=激活侧) -func _on_camp_tree_buy(node_id: String) -> void: - if meta == null: - return - var ok: bool = meta.tree_buy_fork(node_id) if String(db.tree_node(node_id).get("kind", "")) == "fork" else meta.tree_buy(node_id) - if not ok: - board_zone.toast.show_toast("购买被拒——前置/行门槛/余额不足", "warn") - return - camp_overlay.refresh_camp(_camp_data()) - _refresh_presentations() - - -# A/B 免费切换(迭代 12 T8——同组换侧零币;未激活组走购买面) -func _on_camp_fork_toggle(group: String, side: String) -> void: - if meta == null: - return - if not meta.tree_toggle_fork(group, side): - board_zone.toast.show_toast("切换被拒——未激活组请先购买", "warn") - return - camp_overlay.refresh_camp(_camp_data()) - _refresh_presentations() - - -# ---- 商店覆盖层与 GP 钱包(GP+商店房实施批——写调用唯一归组合根)---- - -func _on_shop_buy(product_id: String) -> void: - if runner == null: - return - if runner.is_shop_pool(): - # pool 面:买 1 → 核心全重抽——货架整架重投喂(重抽反馈;余额不足核心拒绝零副作用) - if not runner.buy_pool_item(product_id): - logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"}) - board_zone.toast.show_toast("金币不足——买不起这件商品", "warn") - return - shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp)) - _refresh_presentations() - return - if not runner.buy_product(product_id): - # 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1) - logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"}) - board_zone.toast.show_toast("金币不足——买不起这件商品", "warn") - return - shop_overlay.refresh_state(runner.shop_products(), int(runner.gp)) - _refresh_presentations() - - -func _on_shop_leave() -> void: - _close_modal(shop_overlay) - if runner != null: - # 复用兑付确认纪律(接口漂移清偿):离开 ≠ 推进——确认后经 choices_presented 弹出选路口 - runner.continue_after_node() + return _camp_pres.camp_data() func _on_wallet_changed(event: String, gp: int, extra: Dictionary) -> void: