# 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_treasure_unlocks() # 迭代 22 T2——条件解锁刷新换代(旧工坊面退役) 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() # 关闭后刷新顶栏(工坊解锁可能变了余额侧读数面) # 营地快照投影(纯读——秘宝匣 12 件/同调装配/树面/余额/最近冒险;拼句归 CampOverlay,账面归 # MetaProgression。属性锻造三行投影已删——迭代 21 T4 柜台退役;工坊六行换代=迭代 22 T2 秘宝匣) func camp_data() -> Dictionary: var meta = _meta_get.call() return { "balance": meta.keystone_balance() if meta != null else 0, "workshop": meta.treasure_shop_status() if meta != null else [], # 键名沿 workshop(视图消费键不变——行内容=秘宝匣 12 件解锁投影) "attune": meta.attune_camp_status() if meta != null else {}, # 迭代 11 第①批 T4——同调装配面投影 "tree": meta.tree_camp_rows() if meta != null else [], # 树面投影(含 forge_workshop/ks_workshop 钥匙门判据行) "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 # Y 刷新升级行购买(迭代 11 第①批 T4——P1 甲;双入口之一 Q1 裁乙——树上 ks_refresh 同账; # 按钮禁用为常态防线,兜底拒绝可见 pattern 1。属性锻造面已退役——buy_forge 分支随批删除) if attr != "attune_refresh" or not meta.buy_attune_refresh(): 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_visit_info(int(runner.shop_bp_visit_buys()), float(db.shop_price_a())) # T6 爬价规则行 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_visit_info(int(runner.shop_bp_visit_buys()), float(db.shop_price_a())) # T6 爬价规则行 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()