# VERTICAL SLICE - NOT FOR PRODUCTION # 营地/商店投影控制器(迭代 13 T4/D4——二期家族③ 自 main.gd 迁出;规范 1.1 单一职责): # 营地面开合/快照投影/锻造·同调·天赋树·Fork 购买与切换 + 商店房购买/离开(pool 与 product 双面)。 # 装配纪律(ADR-0001,仿 treasure_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 # 开始层互斥面(开合仿设置面先例——非模态且树序在后,规范 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 # 打开营地(主菜单入口——开合与开始层互斥仿设置面先例:开始覆盖层非模态且树序在后, # 规范 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)) shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost()) _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() # MC-1:刷新意图(shop_overlay.refresh_requested——核心 shop_refresh 受理后整架重投喂=重抽反馈) func on_shop_refresh() -> void: var runner = _runner_get.call() if runner == null or not runner.is_shop_pool(): return if not runner.shop_refresh(): toast.show_toast("金币不足——刷新失败", "warn") return shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp)) shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost()) _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()