## VERTICAL SLICE - NOT FOR PRODUCTION ## 营地/商店投影控制器:营地开合/快照投影/锻造·同调·天赋树·Fork 购买切换 + 商店房购买/离开(pool 与 product 双面)。 ## 信号连接只归组合根(camp_overlay/shop_overlay 信号接至本类 on_* 入口);模态栈/呈现刷新与 runner 现值经组合根回调注入(续玩刷新已随主菜单三入口化退役——卡② mn7)。 ## 写调用唯一经 MetaProgression 购买原语与 LevelRunner.buy_*——本类零直写,拒绝兜底 toast 保留原地。 extends RefCounted var db var _meta_get: Callable # () → 元进度账本现值(组合根 lambda——探针 isolate_save 换注面) var _save_get: Callable # () → 存档承载现值(同上——营地 recent 摘要读数面) var views: Dictionary = {} # {camp_scene, forge_view, treasure_view, tree_view, codex_view, shop_overlay, toast, start_overlay, save_select_overlay, logger, refresh_bus} # type-hint: ignore(三束异构混载——视图句柄/单口件;营地域句柄经 ctx.views 取用——组合根 ui.modals.camp 子容器同源) 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 func _init(ctx: CampFlowContext) -> void: # 构造参数收拢 context 对象(13 参长签名收一处命名面) db = ctx.db _meta_get = ctx.getters.meta_get _save_get = ctx.getters.save_get views = ctx.views # 营地域五目标+刷新单口+关层收闭面(卡④ T3——CampOverlay 退役后投影目标改线) shop_overlay = ctx.views.shop_overlay toast = ctx.views.toast start_overlay = ctx.views.start_overlay logger = ctx.views.logger _runner_get = ctx.getters.runner_get _open_modal = ctx.flow.open_modal _close_modal = ctx.flow.close_modal _refresh_presentations = ctx.flow.refresh_presentations # 打开营地场景根(进档落点——卡④ T3 改挂:开层=交互式场景图,四子界面经场景点选进) func open_camp() -> void: var meta = _meta_get.call() if meta == null: return meta.refresh_treasure_unlocks() # 条件解锁刷新 start_overlay.visible = false _open_modal.call(views.camp_scene) # 模态栈单口(场景根开——选档层被栈遮挡挂起) views.refresh_bus.refresh(camp_data()) # 进档动线尾显式刷新一闸(卡⑤ 契约 5 时机②——HUD 读数重读) _refresh_presentations.call() # ---- 营地四子界面开层(场景点选路由分派——组合根 _on_camp_object_pressed;数据投喂=快照投影)---- func open_camp_forge() -> void: _open_modal.call(views.forge_view) views.forge_view.open_camp(camp_data()) func open_camp_treasure() -> void: _open_modal.call(views.treasure_view) views.treasure_view.open_camp(camp_data()) func open_camp_tree() -> void: _open_modal.call(views.tree_view) views.tree_view.open_camp(camp_data()) func open_camp_codex() -> void: _open_modal.call(views.codex_view) views.codex_view.open_camp(camp_data()) # 子界面返回钮(视图只发 closed——关层归本口;场景根留底复现,开始层不动) func on_camp_sub_closed(view: Control) -> void: _close_modal.call(view) _refresh_presentations.call() # 关闭整个营地(关层收闭——卡④ T3:营地层与选档层同收+开始层恢复,禁只关营地留下选档回闪; # 触发面=篝火/ESC 回主菜单动线,卡④ T4 接线) func on_camp_closed() -> void: for v: Control in [views.camp_scene, views.forge_view, views.treasure_view, views.tree_view, views.codex_view]: if v.visible: _close_modal.call(v) _close_modal.call(views.save_select_overlay) start_overlay.visible = true _refresh_presentations.call() # 关闭后刷新顶栏(工坊解锁可能变了余额侧读数面) # 营地快照投影(纯读——秘宝匣 12 件/同调装配/树面/锻造工坊两区六路线/余额/最近冒险;拼句归 # CampOverlay,账面归 MetaProgression。锻造三行投影不在本投影内; # workshop 键=秘宝匣 12 件;forge 键=锻造工坊路线行板投影) func camp_data() -> Dictionary: var meta = _meta_get.call() return { "balance": meta.meta_state.keystone_balance() if meta != null else 0, "workshop": meta.treasure_shop_status() if meta != null else [], # 键名沿 workshop(视图消费键不变——行内容=秘宝匣 12 件解锁投影) "forge": meta.forge_shop_status() if meta != null else [], # 锻造工坊两区六路线投影 "attune": meta.attune_camp_status() if meta != null else {}, # 同调装配面投影 "tree": meta.tree_camp_status()["rows"] if meta != null else [], # 树面投影(含 forge_workshop/ks_workshop 钥匙门判据行) "fork_groups": meta.tree_camp_status()["fork_groups"] if meta != null else [], "recent": _save_get.call().meta_state.career_state()["recent_summaries"] if _save_get.call() != null else [], } func on_camp_buy(attr: String) -> void: var meta = _meta_get.call() if meta == null: return # 秘宝钥石购买(秘宝匣柜台购买钮意图;写口唯一=buy_treasure_unlock, # 余额闸/重复购/条件件误购三重拒绝零副作用) if attr != "attune_refresh": if not meta.buy_treasure_unlock(attr): toast.show_toast("购买被拒(余额不足或不可购买)", "warn") return views.refresh_bus.refresh(camp_data()) _refresh_presentations.call() return # Y 刷新升级行购买(双入口之一——树上 ks_refresh 同账; # 按钮禁用为常态防线,兜底拒绝可见 pattern 1。本函数无属性锻造分支) if not meta.buy_attune_refresh(): toast.show_toast("购买被拒", "warn") return views.refresh_bus.refresh(camp_data()) _refresh_presentations.call() # 同调装配切换(装配关系唯一写口经 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 views.refresh_bus.refresh(camp_data()) # 天赋树购买(写口唯一归 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.crossrun.meta_tree.row_by_id(node_id).get("kind", "")) == "fork" else meta.tree_buy(node_id) if not ok: toast.show_toast("购买被拒——前置/行门槛/余额不足", "warn") return views.refresh_bus.refresh(camp_data()) _refresh_presentations.call() # A/B 免费切换(同组换侧零币;未激活组走购买面) 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 views.refresh_bus.refresh(camp_data()) _refresh_presentations.call() # ---- 锻造工坊三意图(契约 9:写口唯一归 MetaProgression 锻造写口族;UI 只发意图)---- # 路线购买(几何价;余额不足/未解锁/满级=核心拒绝零副作用,兜底拒绝可见 pattern 1) func on_camp_forge_buy(route_id: String) -> void: var meta = _meta_get.call() if meta == null: return if not meta.forge_buy_level(route_id): toast.show_toast("锻造被拒——钥石不足或路线不可锻造", "warn") return views.refresh_bus.refresh(camp_data()) _refresh_presentations.call() # 一次性解锁价直购(并联双通道②;已解锁=拒绝零收费) func on_camp_forge_unlock(route_id: String) -> void: var meta = _meta_get.call() if meta == null: return if not meta.forge_buy_unlock(route_id): toast.show_toast("解锁被拒——钥石不足或已解锁", "warn") return views.refresh_bus.refresh(camp_data()) _refresh_presentations.call() # 当前武器/甲选中切换(免费;未解锁拒选) func on_camp_forge_select(route_id: String) -> void: var meta = _meta_get.call() if meta == null: return var ok: bool = meta.forge_select("weapon", route_id) if String(db.crossrun.forge.route(route_id).get("kind", "")) == "weapon" \ else meta.forge_select("armor", route_id) if not ok: toast.show_toast("切换被拒——路线未解锁", "warn") return views.refresh_bus.refresh(camp_data()) _refresh_presentations.call() func on_shop_buy(product_id: String) -> void: var runner = _runner_get.call() if runner == null: return if runner.shop.is_shop_pool(): # pool 面:买 1 → 核心全重抽——货架整架重投喂(重抽反馈;余额不足核心拒绝零副作用) if not runner.shop.buy_pool_item(product_id): logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"}) toast.show_toast("GP不足——买不起这件商品", "warn") return shop_overlay.open_shelf(runner.shop.shop_shelf(), int(runner.gp)) shop_overlay.set_visit_info(int(runner.shop.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行 shop_overlay.set_refresh_display(runner.shop.shop_refresh_free_left(), runner.shop.shop_refresh_cost(), int(runner.gp) >= runner.shop.shop_refresh_cost()) _refresh_presentations.call() return if not runner.shop.buy_product(product_id): # 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1) logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"}) toast.show_toast("GP不足——买不起这件商品", "warn") return shop_overlay.refresh_state(runner.shop.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.shop.is_shop_pool(): return if not runner.shop.shop_refresh(): toast.show_toast("GP不足——刷新失败", "warn") return shop_overlay.open_shelf(runner.shop.shop_shelf(), int(runner.gp)) shop_overlay.set_visit_info(int(runner.shop.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行 shop_overlay.set_refresh_display(runner.shop.shop_refresh_free_left(), runner.shop.shop_refresh_cost(), int(runner.gp) >= runner.shop.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()