From 98470adc4db7b84751e352bdfdad6c10dbb04e7f Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sun, 1 Nov 2026 18:06:45 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=A1=E2=91=A3T3(=E8=BF=AD=E4=BB=A347=C2=B7?= =?UTF-8?q?=E8=90=A5=E5=9C=B0=E5=9C=BA=E6=99=AF=E5=9B=BE=E6=89=B9):=20?= =?UTF-8?q?=E5=9B=9B=E6=9F=9C=E5=8F=B0=E6=8B=86=E8=A7=A3=E5=9B=9B=E5=AD=90?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E7=8B=AC=E7=AB=8B=E5=8F=AF=E5=BC=80+CampRefr?= =?UTF-8?q?eshBus=E5=8D=95=E5=8F=A3+CampOverlay=E9=80=80=E5=BD=B9=E2=80=94?= =?UTF-8?q?=E2=80=94=E4=BA=94=E5=8F=A5=E6=9F=84=E8=90=BD=E5=AD=90=E5=AE=B9?= =?UTF-8?q?=E5=99=A8/=E5=9C=BA=E6=99=AF=E7=82=B9=E9=80=89=E6=94=B9?= =?UTF-8?q?=E6=8C=82/=E7=94=9F=E6=B6=AF=E6=94=B9=E6=8C=82=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC/=E5=85=B3=E5=B1=82=E6=94=B6=E9=97=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 四view新件(自CampOverlay承载面迁移,界面逻辑复用零重设计契约4): forge_view(钥匙门+两区六路线行板)/treasure_view(12件行板+同调装配段+Y升级行+钥匙门)/tree_view(脉分组行板+Fork双态)/codex_view(占位挂起壳,锁文随形态订正『柜台』称谓L2)——各带返回钮发closed(关层归presenter);公共壳CampSubViewBase(实底dim+铭牌+钥石徽+返回钮) - CampRefreshBus新件: 各写口尾→现开界面刷新+场景根HUD一闸(卡⑤HUD同口挂接;非autoload非信号广播——组合根单点推送);presenter八写口尾refresh_camp→bus.refresh(F9:七口+on_camp_buy双尾) - 承载形态(§9-2拍板): 单一内页切换=场景根留底——模态栈open只隐上方登记层(modal_stack.gd:27实测),四子界面登记于场景根之上且实底遮挡,双开互斥靠遮挡天然成立;V2断言按留底派生(T5落) - ui_registry: LAYER_CONTRACT camp_overlay槽→camp_scene(shell camp_screen)+forge/treasure/tree/codex_view四槽;node()改经modals.camp.X二层(卡⑥契约2);ui_camp_modals五枚成员(overlay枚随退役删除——卡⑥契约4) - camp_screen: 持camp_scene取代CampOverlay成员;main_ui_build: 场景装配(布局表ConfigDB注入)+四view实例化+信号接线(意图→presenter/closed bind携视图/object_pressed→组合根路由) - presenter承接面: 五目标改线(ctx.views束——成员预算12≤15)+open_camp_forge/treasure/tree/codex四开层口+on_camp_sub_closed+on_camp_closed关层收闭(营地层与选档层同收+开始层恢复);main_overlay_flow._on_camp_object_pressed五物件路由(gate/campfire归T4)+生涯营地语境关层分支(笔记本改挂——CareerOverlay本体零改);main.gd薄委托零新增成员 - 退役: camp_overlay_view.gd删除(吸收即死);旧形态断言同批摘除(test-conventions §5.5):theme_projection等高源码断言+最近冒险行投影段(卡面削弱申报授权:注销,条带Q4=甲撤下) - 同批迁移: forge_workbench_test承载类camp_overlay_view→forge_view+_forge._X扁平化(断言零改12项绿);system_test三处机械随迁(V2实例化/:108同一性链/:121候选清单——camp_scene句柄更名) - 验证: 冒烟boot PASS(15项0失败——装配全链通)+全流程孤检绿(12项:五句柄/铺层/点选路由四子界面/返回/生涯改挂/营地语境分支/关层收闭——临时件不入库);S9/S10/S13归基线(codex头注日期清偿/_build_shell超局部变量按顶栏底栏拆) --- game/presentation/camp_flow_context.gd | 2 +- game/presentation/camp_shop_presenter.gd | 62 +- game/presentation/main.gd | 6 +- game/presentation/main_overlay_flow.gd | 24 +- game/presentation/main_ui_build.gd | 56 +- game/presentation/scenes/camp_screen.gd | 14 +- game/presentation/ui_camp_modals.gd | 10 +- game/presentation/ui_registry.gd | 12 +- game/presentation/views/camp_overlay_view.gd | 655 ------------------ .../views/camp_overlay_view.gd.uid | 1 - game/presentation/views/camp_refresh_bus.gd | 17 + .../views/camp_refresh_bus.gd.uid | 1 + game/presentation/views/camp_sub_view_base.gd | 86 +++ .../views/camp_sub_view_base.gd.uid | 1 + game/presentation/views/codex_view.gd | 25 + game/presentation/views/codex_view.gd.uid | 1 + game/presentation/views/forge_view.gd | 160 +++++ game/presentation/views/forge_view.gd.uid | 1 + game/presentation/views/treasure_view.gd | 239 +++++++ game/presentation/views/treasure_view.gd.uid | 1 + game/presentation/views/tree_view.gd | 105 +++ game/presentation/views/tree_view.gd.uid | 1 + game/tests/system_test.gd | 8 +- .../unit/presentation/forge_workbench_test.gd | 12 +- .../presentation/theme_projection_test.gd | 21 - 25 files changed, 789 insertions(+), 732 deletions(-) delete mode 100644 game/presentation/views/camp_overlay_view.gd delete mode 100644 game/presentation/views/camp_overlay_view.gd.uid create mode 100644 game/presentation/views/camp_refresh_bus.gd create mode 100644 game/presentation/views/camp_refresh_bus.gd.uid create mode 100644 game/presentation/views/camp_sub_view_base.gd create mode 100644 game/presentation/views/camp_sub_view_base.gd.uid create mode 100644 game/presentation/views/codex_view.gd create mode 100644 game/presentation/views/codex_view.gd.uid create mode 100644 game/presentation/views/forge_view.gd create mode 100644 game/presentation/views/forge_view.gd.uid create mode 100644 game/presentation/views/treasure_view.gd create mode 100644 game/presentation/views/treasure_view.gd.uid create mode 100644 game/presentation/views/tree_view.gd create mode 100644 game/presentation/views/tree_view.gd.uid diff --git a/game/presentation/camp_flow_context.gd b/game/presentation/camp_flow_context.gd index d1f67be4..d12f5c96 100644 --- a/game/presentation/camp_flow_context.gd +++ b/game/presentation/camp_flow_context.gd @@ -7,7 +7,7 @@ extends RefCounted ## 三束按域收拢(u21——同 u20 形;views=视图句柄/getters=查询 Callable 活引用/flow=编排 Callable): ## 原 12 参长签名收 4 参——字段即原构造参数逐一同名迁移,键面=消费方改址合同。 var db -var views: Dictionary = {} # {camp_overlay, shop_overlay, toast, start_overlay, logger} # type-hint: ignore(三束异构混载——视图句柄/查询 Callable/编排 Callable) +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(三束异构混载——视图句柄/刷新单口/查询 Callable/编排 Callable;卡④ T3 营地域改挂) var getters: Dictionary = {} # {meta_get, save_get, runner_get} # type-hint: ignore(同上——Callable 束) var flow: Dictionary = {} # {open_modal, close_modal, refresh_presentations} # type-hint: ignore(同上——Callable 束) diff --git a/game/presentation/camp_shop_presenter.gd b/game/presentation/camp_shop_presenter.gd index aab5c1eb..07094d3b 100644 --- a/game/presentation/camp_shop_presenter.gd +++ b/game/presentation/camp_shop_presenter.gd @@ -7,7 +7,7 @@ extends RefCounted var db var _meta_get: Callable # () → 元进度账本现值(组合根 lambda——探针 isolate_save 换注面) var _save_get: Callable # () → 存档承载现值(同上——营地 recent 摘要读数面) -var camp_overlay: CampOverlay +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) @@ -23,7 +23,7 @@ func _init(ctx: CampFlowContext) -> void: db = ctx.db _meta_get = ctx.getters.meta_get _save_get = ctx.getters.save_get - camp_overlay = ctx.views.camp_overlay + views = ctx.views # 营地域五目标+刷新单口+关层收闭面(卡④ T3——CampOverlay 退役后投影目标改线) shop_overlay = ctx.views.shop_overlay toast = ctx.views.toast start_overlay = ctx.views.start_overlay @@ -34,21 +34,53 @@ func _init(ctx: CampFlowContext) -> void: _refresh_presentations = ctx.flow.refresh_presentations -# 打开营地(主菜单入口——开合与开始层互斥仿设置面先例:开始覆盖层非模态且树序在后, -# 规范 2.3 遮挡纪律;打开时先刷条件解锁——达标未解锁就地入账) +# 打开营地场景根(进档落点——卡④ 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(camp_overlay) # 模态栈单口(登记层开合一律走栈) - camp_overlay.open_camp(camp_data()) + _open_modal.call(views.camp_scene) # 模态栈单口(场景根开——选档层被栈遮挡挂起) _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: - _close_modal.call(camp_overlay) # 模态栈单口 + 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() # 关闭后刷新顶栏(工坊解锁可能变了余额侧读数面) @@ -79,7 +111,7 @@ func on_camp_buy(attr: String) -> void: if not meta.buy_treasure_unlock(attr): toast.show_toast("购买被拒(余额不足或不可购买)", "warn") return - camp_overlay.refresh_camp(camp_data()) + views.refresh_bus.refresh(camp_data()) _refresh_presentations.call() return # Y 刷新升级行购买(双入口之一——树上 ks_refresh 同账; @@ -87,7 +119,7 @@ func on_camp_buy(attr: String) -> void: if not meta.buy_attune_refresh(): toast.show_toast("购买被拒", "warn") return - camp_overlay.refresh_camp(camp_data()) + views.refresh_bus.refresh(camp_data()) _refresh_presentations.call() @@ -99,7 +131,7 @@ func on_camp_attune_toggle(item_id: String) -> void: if not meta.toggle_attunement(item_id): toast.show_toast("同调位已满——先卸下再装配", "warn") return - camp_overlay.refresh_camp(camp_data()) + views.refresh_bus.refresh(camp_data()) # 天赋树购买(写口唯一归 MetaProgression.tree_buy/tree_buy_fork;Fork 节点购=激活侧) @@ -111,7 +143,7 @@ func on_camp_tree_buy(node_id: String) -> void: if not ok: toast.show_toast("购买被拒——前置/行门槛/余额不足", "warn") return - camp_overlay.refresh_camp(camp_data()) + views.refresh_bus.refresh(camp_data()) _refresh_presentations.call() @@ -123,7 +155,7 @@ func on_camp_fork_toggle(group: String, side: String) -> void: if not meta.tree_toggle_fork(group, side): toast.show_toast("切换被拒——未激活组请先购买", "warn") return - camp_overlay.refresh_camp(camp_data()) + views.refresh_bus.refresh(camp_data()) _refresh_presentations.call() @@ -136,7 +168,7 @@ func on_camp_forge_buy(route_id: String) -> void: if not meta.forge_buy_level(route_id): toast.show_toast("锻造被拒——钥石不足或路线不可锻造", "warn") return - camp_overlay.refresh_camp(camp_data()) + views.refresh_bus.refresh(camp_data()) _refresh_presentations.call() @@ -148,7 +180,7 @@ func on_camp_forge_unlock(route_id: String) -> void: if not meta.forge_buy_unlock(route_id): toast.show_toast("解锁被拒——钥石不足或已解锁", "warn") return - camp_overlay.refresh_camp(camp_data()) + views.refresh_bus.refresh(camp_data()) _refresh_presentations.call() @@ -162,7 +194,7 @@ func on_camp_forge_select(route_id: String) -> void: if not ok: toast.show_toast("切换被拒——路线未解锁", "warn") return - camp_overlay.refresh_camp(camp_data()) + views.refresh_bus.refresh(camp_data()) _refresh_presentations.call() diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 0619569c..1db78d46 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -500,12 +500,16 @@ func _sync_status_entry() -> void: MainOverlayFlow._sync_status_entry(self) -# ---- 探针缝保缝(click/ui_probe 营地段直呼组合根面;实现归 camp_shop_presenter.gd)---- +# ---- 探针缝保缝(click/ui_probe 营地段直呼组合根面;实现归 camp_shop_presenter.gd / main_overlay_flow.gd)---- func _on_camp_closed() -> void: MainOverlayFlow._on_camp_closed(self) +func _on_camp_object_pressed(key: StringName) -> void: + MainOverlayFlow._on_camp_object_pressed(self, key) # 场景点选路由(卡④ T3——薄委托,实现归覆盖层流程承接件) + + func _camp_data() -> Dictionary: return MainOverlayFlow._camp_data(self) diff --git a/game/presentation/main_overlay_flow.gd b/game/presentation/main_overlay_flow.gd index 5c12fd60..8389bf5d 100644 --- a/game/presentation/main_overlay_flow.gd +++ b/game/presentation/main_overlay_flow.gd @@ -40,7 +40,25 @@ static func _open_career(root: Variant) -> void: static func _on_career_closed(root: Variant) -> void: root.ui.modals.flow.close(root.ui.modals.career_overlay) # 模态栈单口 - root.ui.screens.start_overlay.visible = true + if not root.ui.modals.camp.scene.visible: # 营地语境关生涯不回开始层(笔记本入口——卡④ T3;栈已复现场景根) + root.ui.screens.start_overlay.visible = true + + +# ---- 营地场景点选路由(卡④ T3——场景根 object_pressed 组合根分派;gate/campfire 动线归卡④ T4)---- + + +static func _on_camp_object_pressed(root: Variant, key: StringName) -> void: + match key: + &"forge": + root._camp_pres.open_camp_forge() + &"chest": + root._camp_pres.open_camp_treasure() + &"dummy": + root._camp_pres.open_camp_tree() + &"board": + root._camp_pres.open_camp_codex() + &"notebook": + _open_career(root) # 生涯页入口改挂帐篷旁笔记本(CareerOverlay 本体零改——卡④ T3) # 生涯页快照投影 —— 承接体见 HudPresenter.career_data(开层调用点直接改引) @@ -65,7 +83,7 @@ static func _on_save_select_closed(root: Variant) -> void: -# 空槽新档链(Q3 裁乙暂行落点=现役 CampOverlay;卡④ 重立卡后改挂场景图):卡① 创建=激活=白板落盘, +# 空槽新档链(卡④ T3 换代:落点=营地场景根——CampOverlay 随四柜台拆解退役):卡① 创建=激活=白板落盘, # 落点开层。动线序(禁先关后开):先落点开层(开始层保持隐藏),选档层被模态栈遮挡隐藏、不关闭。 static func _on_save_select_new(root: Variant, slot: int) -> void: root.save_manager.slots.create_slot(slot) @@ -73,7 +91,7 @@ static func _on_save_select_new(root: Variant, slot: int) -> void: -# 有档槽进档链(契约 8 进档即激活):卡① 激活+读盘(兜底/拒读链按槽成立)→暂行落点 CampOverlay。 +# 有档槽进档链(契约 8 进档即激活):卡① 激活+读盘(兜底/拒读链按槽成立)→落点=营地场景根(卡④ T3)。 static func _on_save_select_load(root: Variant, slot: int) -> void: root.save_manager.slots.activate_slot(slot) root._camp_pres.open_camp() diff --git a/game/presentation/main_ui_build.gd b/game/presentation/main_ui_build.gd index 043c4475..b6eb8ad3 100644 --- a/game/presentation/main_ui_build.gd +++ b/game/presentation/main_ui_build.gd @@ -83,7 +83,7 @@ static func _build_battle_wiring(root: Variant) -> void: static func _build_overlays(root: Variant) -> void: # 模态覆盖层(登记序 = 树序 = z 遮挡序:图例 < 明细回看 < 节点 < 关卡画面 < 三选一 < 终局 - # < 商店 < 营地 < 设置 < 状态面板(只读) < 生涯页(只读) < 确认弹窗 < 调试浮窗; + # < 商店 < 营地场景根 < 四子界面 < 设置 < 状态面板(只读) < 生涯页(只读) < 确认弹窗 < 调试浮窗; # 关卡画面住冒险屏壳(树序在模态之下)——登记序仍夹在节点层与三选一层之间,遮挡语义零改动) var overlays: Dictionary[Variant, Variant] = UiAssembler.build_overlays(root, root.db) root.ui.modals.legend_overlay = overlays.legend_overlay @@ -92,8 +92,25 @@ static func _build_overlays(root: Variant) -> void: root.ui.modals.shop_overlay = overlays.shop_overlay root.ui.screens.camp_screen = CampScreenScene.instantiate() root.add_child(root.ui.screens.camp_screen) - root.ui.modals.camp.overlay = root.ui.screens.camp_screen.camp_overlay - # 营地屏壳插位(夹商店层与设置层之间)由 LAYER_CONTRACT 营地槽统一落位——禁散落 move_child + # 营地场景根装配(布局表经 ConfigDB 单点注入——视图禁自载表路径;CampOverlay 随四柜台拆解退役) + var camp_layout: CampSceneLayoutTable = root.db.presentation.camp_scene_layout + root.ui.screens.camp_screen.camp_scene.setup_layers(camp_layout) + root.ui.screens.camp_screen.camp_scene.bind_hotspots(camp_layout) + root.ui.modals.camp.scene = root.ui.screens.camp_screen.camp_scene + # 营地四子界面(独立全屏页——场景根留底形态;句柄落 ui.modals.camp 子容器——禁平铺回 ui_modals) + var forge := ForgeView.new() + root.add_child(forge) + root.ui.modals.camp.forge = forge + var treasure := TreasureView.new() + root.add_child(treasure) + root.ui.modals.camp.treasure = treasure + var tree_view := TreeView.new() + root.add_child(tree_view) + root.ui.modals.camp.tree = tree_view + var codex := CodexView.new() + root.add_child(codex) + root.ui.modals.camp.codex = codex + # 屏壳/子界面插位由 LAYER_CONTRACT 槽统一落位——禁散落 move_child root.ui.modals.legend_overlay.closed.connect(root.ui.modals.flow.close.bind(root.ui.modals.legend_overlay)) root.ui.modals.node_overlay.continue_pressed.connect(root._on_node_continue) root.ui.modals.level_screen.route_chosen.connect(root._on_route_chosen) @@ -158,9 +175,17 @@ static func _build_menu_wiring(root: Variant) -> void: root.ui.screens.debug_badge.pressed.connect(root._on_debug_badge_pressed) # 营地/商店投影(本体在 camp_shop_presenter.gd;本类只接线、探针缝保缝转发; # 购买写调用唯一经 MetaProgression/LevelRunner core 单口——组合根 lambda 现值注入) + # 营地刷新单口(各写口尾→现开界面+场景根 HUD 一闸完成——卡④ T3;卡⑤ HUD band 随卡⑤挂册) + var camp_bus := CampRefreshBus.new() + camp_bus.bind_targets([root.ui.modals.camp.forge, root.ui.modals.camp.treasure, + root.ui.modals.camp.tree, root.ui.modals.camp.codex] as Array[Control]) root._camp_pres = CampShopPresenter.new(CampFlowContext.new(root.db, - {"camp_overlay": root.ui.modals.camp.overlay, "shop_overlay": root.ui.modals.shop_overlay, - "toast": root.ui.battle.board_zone.toast, "start_overlay": root.ui.screens.start_overlay, "logger": root.logger}, + {"camp_scene": root.ui.modals.camp.scene, "forge_view": root.ui.modals.camp.forge, + "treasure_view": root.ui.modals.camp.treasure, "tree_view": root.ui.modals.camp.tree, + "codex_view": root.ui.modals.camp.codex, "shop_overlay": root.ui.modals.shop_overlay, + "toast": root.ui.battle.board_zone.toast, "start_overlay": root.ui.screens.start_overlay, + "save_select_overlay": root.ui.modals.save_select_overlay, "logger": root.logger, + "refresh_bus": camp_bus}, {"meta_get": func() -> Variant: return root.meta, "save_get": func() -> Variant: return root.save_manager, "runner_get": func() -> Variant: return root.run_scope.runner}, {"open_modal": root.ui.modals.flow.open, "close_modal": root.ui.modals.flow.close, @@ -168,14 +193,19 @@ static func _build_menu_wiring(root: Variant) -> void: root.ui.modals.shop_overlay.buy_requested.connect(root._camp_pres.on_shop_buy) root.ui.modals.shop_overlay.refresh_requested.connect(root._camp_pres.on_shop_refresh) # MC-1 root.ui.modals.shop_overlay.leave_pressed.connect(root._camp_pres.on_shop_leave) - root.ui.modals.camp.overlay.buy_requested.connect(root._camp_pres.on_camp_buy) - root.ui.modals.camp.overlay.attune_toggled.connect(root._camp_pres.on_camp_attune_toggle) - root.ui.modals.camp.overlay.tree_buy_requested.connect(root._camp_pres.on_camp_tree_buy) - root.ui.modals.camp.overlay.tree_fork_toggled.connect(root._camp_pres.on_camp_fork_toggle) - root.ui.modals.camp.overlay.forge_buy_requested.connect(root._camp_pres.on_camp_forge_buy) # 锻造三意图(写口唯一归 MetaProgression) - root.ui.modals.camp.overlay.forge_unlock_requested.connect(root._camp_pres.on_camp_forge_unlock) - root.ui.modals.camp.overlay.forge_select_requested.connect(root._camp_pres.on_camp_forge_select) - root.ui.modals.camp.overlay.closed.connect(root._camp_pres.on_camp_closed) + # 营地信号接线(CampOverlay 退役——四子界面意图信号+场景点选组合根路由+closed 经 bind 携视图) + root.ui.modals.camp.forge.forge_buy_requested.connect(root._camp_pres.on_camp_forge_buy) + root.ui.modals.camp.forge.forge_unlock_requested.connect(root._camp_pres.on_camp_forge_unlock) + root.ui.modals.camp.forge.forge_select_requested.connect(root._camp_pres.on_camp_forge_select) + root.ui.modals.camp.treasure.buy_requested.connect(root._camp_pres.on_camp_buy) + root.ui.modals.camp.treasure.attune_toggled.connect(root._camp_pres.on_camp_attune_toggle) + root.ui.modals.camp.tree.tree_buy_requested.connect(root._camp_pres.on_camp_tree_buy) + root.ui.modals.camp.tree.tree_fork_toggled.connect(root._camp_pres.on_camp_fork_toggle) + root.ui.modals.camp.forge.closed.connect(root._camp_pres.on_camp_sub_closed.bind(root.ui.modals.camp.forge)) + root.ui.modals.camp.treasure.closed.connect(root._camp_pres.on_camp_sub_closed.bind(root.ui.modals.camp.treasure)) + root.ui.modals.camp.tree.closed.connect(root._camp_pres.on_camp_sub_closed.bind(root.ui.modals.camp.tree)) + root.ui.modals.camp.codex.closed.connect(root._camp_pres.on_camp_sub_closed.bind(root.ui.modals.camp.codex)) + root.ui.modals.camp.scene.object_pressed.connect(root._on_camp_object_pressed) root.ui.screens.menu_screen.settingsbtn.pressed.connect(root._open_settings) # 最小设置面入口 root.ui.screens.menu_screen.quitbtn.pressed.connect(root._quit_game) # 退出游戏提级一级菜单 diff --git a/game/presentation/scenes/camp_screen.gd b/game/presentation/scenes/camp_screen.gd index 47c7bb08..957bba58 100644 --- a/game/presentation/scenes/camp_screen.gd +++ b/game/presentation/scenes/camp_screen.gd @@ -1,14 +1,14 @@ class_name CampScreen extends Control -# 营地屏壳(营地覆盖层的屏壳容器)。 -# 营地覆盖层视图原样构建(CampOverlay 类视图继续代码构建、零 .tscn 化改造——契约 2); -# 购买/开合接线归组合根经 CampShopPresenter(信号连接只在组合根)。 -# 可独立实例化(F6/headless 挂载)——V2 探针断言载体。 +# 营地屏壳(营地场景根的屏壳容器)。 +# 场景图视图代码构建(CampSceneView 继续代码构建、零 .tscn 化改造);布局表经组合根注入 +# (main_ui_build 装配段 setup_layers/bind_hotspots——视图禁自载表路径);购买/开合接线归组合根经 +# CampShopPresenter(信号连接只在组合根)。可独立实例化(F6/headless 挂载)——V2 探针断言载体。 -var camp_overlay: CampOverlay # 营地覆盖层(原 build_overlays 营地段迁入——main 组合根句柄指向本屏成员) +var camp_scene: CampSceneView # 营地场景图(交互式营地场景根——main 组合根句柄指向本屏成员) func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE # 屏壳=纯容器(ui_input 实证:全屏 STOP 壳会吃掉下层模态鼠标输入) - camp_overlay = CampOverlay.new() - add_child(camp_overlay) + camp_scene = CampSceneView.new() + add_child(camp_scene) diff --git a/game/presentation/ui_camp_modals.gd b/game/presentation/ui_camp_modals.gd index bd7eb23e..12297b74 100644 --- a/game/presentation/ui_camp_modals.gd +++ b/game/presentation/ui_camp_modals.gd @@ -1,7 +1,11 @@ -## 营地交互域模态句柄子容器:持营地域全部模态句柄(终态六枚=营地场景根+锻造/秘宝/天赋树/图鉴四子界面+Esc 浮窗;现役一枚=CampOverlay 覆盖层)。 -## RefCounted 不挂树;随 UiModals._init 构造(与 flow 同拍)。终态六枚未落满前只持在役枚——禁空占位凑结构。 +## 营地交互域模态句柄子容器:持营地域全部模态句柄(终态六枚=营地场景根+锻造/秘宝/天赋树/图鉴四子界面+Esc 浮窗)。 +## RefCounted 不挂树;随 UiModals._init 构造(与 flow 同拍)。Esc 浮窗枚随卡⑤落——禁空占位凑结构。 ## 新营地句柄禁平铺回 ui_modals:营地域句柄寻址一律 ui.modals.camp.X 二层(模态句柄册分域纪律——全量分家另立批)。 class_name UiCampModals extends RefCounted -var overlay: CampOverlay +var scene: CampSceneView # 营地场景根(交互式营地场景图——camp_screen 屏壳成员住宿) +var forge: ForgeView # 锻造工坊子界面 +var treasure: TreasureView # 秘宝匣子界面 +var tree: TreeView # 天赋树子界面 +var codex: CodexView # 奖励格图鉴子界面(占位挂起态) diff --git a/game/presentation/ui_registry.gd b/game/presentation/ui_registry.gd index 6c861d1e..67358424 100644 --- a/game/presentation/ui_registry.gd +++ b/game/presentation/ui_registry.gd @@ -22,7 +22,11 @@ const LAYER_CONTRACT: Array[Dictionary] = [ {"key": "curio_overlay", "kind": "modal", "tags": []}, {"key": "end_overlay", "kind": "modal", "tags": []}, {"key": "shop_overlay", "kind": "modal", "tags": []}, - {"key": "camp_overlay", "kind": "modal", "tags": [], "shell": "camp_screen"}, + {"key": "camp_scene", "kind": "modal", "tags": [], "shell": "camp_screen"}, # 营地场景根(交互式场景图——卡④) + {"key": "forge_view", "kind": "modal", "tags": []}, # 营地四子界面(独立全屏页——场景根留底形态,卡④ T3) + {"key": "treasure_view", "kind": "modal", "tags": []}, + {"key": "tree_view", "kind": "modal", "tags": []}, + {"key": "codex_view", "kind": "modal", "tags": []}, {"key": "settings_overlay", "kind": "modal", "tags": []}, {"key": "status_panel", "kind": "modal", "tags": []}, {"key": "career_overlay", "kind": "modal", "tags": []}, @@ -58,7 +62,11 @@ func node(key: String) -> Node: "end_overlay": return modals.end_overlay "shop_overlay": return modals.shop_overlay "camp_screen": return screens.camp_screen - "camp_overlay": return modals.camp.overlay + "camp_scene": return modals.camp.scene + "forge_view": return modals.camp.forge + "treasure_view": return modals.camp.treasure + "tree_view": return modals.camp.tree + "codex_view": return modals.camp.codex "settings_overlay": return modals.settings_overlay "status_panel": return modals.status_panel "career_overlay": return modals.career_overlay diff --git a/game/presentation/views/camp_overlay_view.gd b/game/presentation/views/camp_overlay_view.gd deleted file mode 100644 index 77458ab6..00000000 --- a/game/presentation/views/camp_overlay_view.gd +++ /dev/null @@ -1,655 +0,0 @@ -## VERTICAL SLICE - NOT FOR PRODUCTION -## 营地覆盖层:树淬炼/锻造工坊/秘宝匣/奖励格图鉴四柜台只读投影,购买与切换意图经信号外发。 -## 柜台锁定态(钥匙未点亮)开/关零副作用——零写档零报错、锁定行纯文案无可交互购买路径。 -## 开合与开始层互斥归组合根承接(打开前隐藏开始层)。 -## 内部状态件(u1 子域拆件——契约 9 职责轴):_TreeState=树淬炼柜台内容容器(单一变化原因=树行板结构); -## _ForgeState=锻造工坊与秘宝匣两柜台行板与锁定门(钥匙门同构);_AttuneState=同调装配段节点与钮册; -## _SideState=Y 刷新升级行与最近冒险账面。探针直读冻结成员(_balance_label/_tree_buy_btns/ -## _tree_fork_btns/_workshop_holder/_workshop_rows)住宿主不进状态件。 -class_name CampOverlay -extends Control - -signal buy_requested(attr: String) -signal closed -signal attune_toggled(item_id: String) ## 同调装配切换意图(写口唯一归 MetaProgression.toggle_attunement) -signal tree_buy_requested(node_id: String) ## 天赋树购买意图(写口唯一归 MetaProgression.tree_buy) -signal tree_fork_toggled(group: String, side: String) ## 双态切换意图(一节点双态——零币切态) -signal forge_buy_requested(route_id: String) ## 锻造路线购买意图(写口唯一归 MetaProgression.forge_buy_level) -signal forge_unlock_requested(route_id: String) ## 锻造路线解锁意图(写口唯一归 MetaProgression.forge_buy_unlock) -signal forge_select_requested(route_id: String) ## 锻造路线选中意图(写口唯一归 MetaProgression.forge_select) - -# 方向稿取色(柜台头带深棕/行板暖米;双色墨=亮板配深墨)——值单点收 UiKit -const HEADER_BAND_COL := UiKit.CAMP_HEADER_BAND -const ROW_PLATE_COL := UiKit.CAMP_ROW_PLATE - - -class _TreeState extends RefCounted: - var _tree_holder: VBoxContainer # 树淬炼柜台内容(主界面——脊柱两行+钥匙/socket 行板+Fork 双态钮) - - -class _ForgeState extends RefCounted: - var _forge_lock_l: Label # 锻造工坊柜台锁定态行(钥匙未点亮) - var _forge_workbench: VBoxContainer # 锻造工坊工作间(钥匙点亮后展开——两区六路线行板) - var _forge_zone_weapon: VBoxContainer # 武器锻造区行板容器(三路线) - var _forge_zone_armor: VBoxContainer # 护甲锻造区行板容器(三路线) - var _forge_rows: Array[Dictionary] = [] # [{route_id, name_l, lvl_l, status_l, buy_btn, unlock_btn, select_btn, next_price, unlock_price}](open/refresh 重建;探针消费) - var _ws_scroll: ScrollContainer # 秘宝匣柜台内容容器(钥匙点亮后可见) - var _ws_lock_l: Label # 秘宝匣柜台锁定态行 - - -class _AttuneState extends RefCounted: - var _attune_header_l: Label # 同调装配段(Slots 点亮呈现) - var _attune_btns: Array[Dictionary] = [] # [{item_id, btn}]——装配/卸下切换钮(满位禁选随刷) - var _attune_nodes: Array[Control] = [] # 段内节点册(open_camp 重建时定向清理,不扰工坊六行) - - -class _SideState extends RefCounted: - var _refresh_l: Label # Y 刷新升级行(双入口之一——树上 ks_refresh 同账) - var _refresh_btn: Button - var _recent_list: Label # 最近冒险(逐行拼接 label——只读账面,随 open_camp 重建) - - -var _balance_label: Label # 探针直读冻结(S17①——住宿主) -var _tree_buy_btns: Array[Dictionary] = [] # [{node_id, btn}]——树购买/解锁钮登记面(open/refresh 重建时刷新;探针消费;住宿主) -var _tree_fork_btns: Array[Dictionary] = [] # [{group, side, btn}]——Fork 双态切换钮登记面(探针消费;住宿主) -var _workshop_holder: VBoxContainer # 秘宝匣柜台行板(六行+同调装配段+Y 升级行;探针直读冻结——住宿主) -var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l}](探针消费;住宿主) -var _tree := _TreeState.new() -var _forge := _ForgeState.new() -var _attune := _AttuneState.new() -var _side := _SideState.new() - - -func _init() -> void: - # 底=不透明暖亮档(半透 dim 会透出底下棋盘/战斗提示条,故须实底;demo #v-camp=bg-bright 实底, - # 遮蔽语义由组合根输入锁兜底) - var dim := UiKit._modal_dim(self, UiKit.BG_BRIGHT) - # 石纹弱质感+暗角(demo .tilebg 0.14/.vignette;缺位不挂即回退平色) - var bd := UiKit.backdrop_tile() - if bd != null: - dim.add_child(bd) - dim.add_child(UiKit.vignette_rect()) - - var root := MarginContainer.new() - root.set_anchors_preset(Control.PRESET_FULL_RECT) - root.add_theme_constant_override("margin_left", 16) - root.add_theme_constant_override("margin_right", 16) - root.add_theme_constant_override("margin_top", 12) - root.add_theme_constant_override("margin_bottom", 12) - dim.add_child(root) - var col := VBoxContainer.new() - col.add_theme_constant_override("separation", 8) - root.add_child(col) - - # ── 顶栏:左铭牌(标题+副题)/ 右钥石徽 ── - _build_top_bar(col) - - # ── 四柜台并排(程序占位重排)── - # 宽度不足横向滚动(四柜 300 min 宽 > 窄窗可用宽时横滚兜底;纵向撑满余高) - var camp_scroll := ScrollContainer.new() - camp_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL - camp_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO - camp_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED - col.add_child(camp_scroll) - var mid := HBoxContainer.new() - mid.add_theme_constant_override("separation", 14) - mid.size_flags_vertical = Control.SIZE_EXPAND_FILL - camp_scroll.add_child(mid) - _build_tree_counter(mid) - _build_forge_counter(mid) - _build_workshop_counter(mid) - _build_atlas_counter(mid) - _build_bottom_bar(col) - visible = false - - -## 顶栏域:铭牌(标题+副题)+钥石徽章 -func _build_top_bar(col: VBoxContainer) -> void: - var top := HBoxContainer.new() - top.add_theme_constant_override("separation", 10) - col.add_child(top) - var plaque := PanelContainer.new() - plaque.add_theme_stylebox_override("panel", - UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 1, Vector4(14, 8, 14, 8))) - top.add_child(plaque) - var plaque_col := VBoxContainer.new() - plaque.add_child(plaque_col) - var title := UiKit.label("营地 · 天赋树",UiKit.FS_TITLE, true, UiKit.INK_TITLE) - plaque_col.add_child(title) - var sub := UiKit.label("营地本身即树——钥石逐节点点亮,属性/规则/工作间都在树上",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # STONE 对铭牌板 3.59:1 越带,换灰石深化 5.98:1 - plaque_col.add_child(sub) - var spacer := Control.new() - spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL - top.add_child(spacer) - # 钥石徽章烛光黄底+深墨字(demo ②)。UiKit.keystone_badge 工厂单点 - var badge := UiKit.keystone_badge("钥石 0") - top.add_child(badge) - _balance_label = badge.value_label - - -## 柜一·树淬炼(树脊柱界面形态=主界面:一线一节点行板内升级+Fork 双态切换钮) -func _build_tree_counter(mid: HBoxContainer) -> void: - _tree._tree_holder = VBoxContainer.new() - _tree._tree_holder.add_theme_constant_override("separation", 6) - var tree_scroll := ScrollContainer.new() - tree_scroll.custom_minimum_size = Vector2(300, 336) - tree_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL - tree_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED - tree_scroll.add_child(_tree._tree_holder) - var tree_panel := _counter_panel("天赋树 · 淬炼", "", tree_scroll, 300) - tree_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL - tree_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸 - mid.add_child(tree_panel) - - -## 柜二·锻造工坊(钥匙门——forge_workshop 点亮前锁定态;点亮=工作间两区六路线行板) -func _build_forge_counter(mid: HBoxContainer) -> void: - _forge._forge_lock_l = UiKit.label("🔒 锻造工坊未开放\n\n点亮树上「锻造工坊」钥匙节点后展开工作间。\n(钥匙免费、无前置——树上直接点亮)",UiKit.FS_HINT, false, UiKit.STONE_DEEP) - _forge._forge_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中 - _forge._forge_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - _forge._forge_lock_l.custom_minimum_size = Vector2(280, 0) - _forge._forge_workbench = VBoxContainer.new() - _forge._forge_workbench.add_theme_constant_override("separation", 6) - _forge._forge_workbench.visible = false # 默认锁定态(钥匙门未判前零行板——门开才建) - _forge._forge_zone_weapon = VBoxContainer.new() - _forge._forge_zone_weapon.add_theme_constant_override("separation", 4) - _forge._forge_zone_armor = VBoxContainer.new() - _forge._forge_zone_armor.add_theme_constant_override("separation", 4) - var wb_wh := UiKit.label("── 武器锻造区 ──",UiKit.FS_HINT, true, UiKit.GOLD_INK) # 同款深金墨(对柜台板 5.21:1) - _forge._forge_workbench.add_child(wb_wh) - _forge._forge_workbench.add_child(_forge._forge_zone_weapon) - var wb_ah := UiKit.label("── 护甲锻造区 ──",UiKit.FS_HINT, true, UiKit.GOLD_INK) - _forge._forge_workbench.add_child(wb_ah) - _forge._forge_workbench.add_child(_forge._forge_zone_armor) - var forge_holder := VBoxContainer.new() - forge_holder.add_theme_constant_override("separation", 6) - forge_holder.custom_minimum_size = Vector2(0, 336) # 四栏等高(与树/秘宝匣 scroll 336 min 对齐——锁定态仅锁文也撑住栏高) - forge_holder.add_child(_forge._forge_lock_l) - forge_holder.add_child(_forge._forge_workbench) - var forge_panel := _counter_panel("锻造工坊", UiKit.TEX_ICON_FORGE, forge_holder, 300) - forge_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL - forge_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸 - mid.add_child(forge_panel) - - -## 柜三·秘宝匣(钥匙门——ks_workshop 点亮前锁定态;点亮=现工坊内容整体迁入:六行+同调装配段+Y 升级行) -func _build_workshop_counter(mid: HBoxContainer) -> void: - _forge._ws_lock_l = UiKit.label("🔒 秘宝匣未开放\n\n点亮树上「秘宝匣」钥匙节点后展开。\n(前置=脊柱生命第 1 级)",UiKit.FS_HINT, false, UiKit.STONE_DEEP) - _forge._ws_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中 - _forge._ws_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - _forge._ws_lock_l.custom_minimum_size = Vector2(280, 0) - _forge._ws_scroll = ScrollContainer.new() - _forge._ws_scroll.custom_minimum_size = Vector2(300, 336) - _forge._ws_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL - _forge._ws_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED - _workshop_holder = VBoxContainer.new() - _workshop_holder.add_theme_constant_override("separation", 6) - _forge._ws_scroll.add_child(_workshop_holder) - var ws_holder := VBoxContainer.new() - ws_holder.add_theme_constant_override("separation", 6) - ws_holder.custom_minimum_size = Vector2(0, 336) # 四栏等高(锁定态 scroll 隐、仅锁文也撑住栏高) - ws_holder.add_child(_forge._ws_lock_l) - ws_holder.add_child(_forge._ws_scroll) - var ws_panel := _counter_panel("秘宝匣", UiKit.TEX_ICON_WORKSHOP, ws_holder, 300) - ws_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL - ws_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸 - mid.add_child(ws_panel) - - -## 柜四·奖励格图鉴(常开独立位——结构位预留,内容排 Alpha) -func _build_atlas_counter(mid: HBoxContainer) -> void: - var atlas_holder := VBoxContainer.new() - atlas_holder.custom_minimum_size = Vector2(0, 336) # 四栏等高(图鉴仅锁文也撑住栏高) - var atlas_panel := _counter_panel("奖励格图鉴", UiKit.TEX_ICON_ATLAS, atlas_holder, 300) - atlas_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL - atlas_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸 - mid.add_child(atlas_panel) - var lock_l := UiKit.label(" —— 柜台结构位预留:内容排 Alpha(地图战斗资源差异化批)",UiKit.FS_HINT, false, UiKit.STONE_DEEP) - lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中 - lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - lock_l.custom_minimum_size = Vector2(280, 0) - atlas_holder.add_child(lock_l) - - -## 底栏域:关闭钮(左)/ 操作注记(右)+ 最近冒险条带 -func _build_bottom_bar(col: VBoxContainer) -> void: - var close_btn := UiKit.button("关闭营地(回开始界面)") - close_btn.pressed.connect(_emit_closed) - var bottom := HBoxContainer.new() - bottom.add_theme_constant_override("separation", 10) - bottom.add_child(close_btn) - var spacer2 := Control.new() - spacer2.size_flags_horizontal = Control.SIZE_EXPAND_FILL - bottom.add_child(spacer2) - var caption := UiKit.label("树上点亮 · 工作间随钥匙展开 · 图鉴常开",UiKit.FS_HINT, false, UiKit.WARM_SOFT) - bottom.add_child(caption) - col.add_child(bottom) - _side._recent_list = UiKit.label("",UiKit.FS_HINT, false, UiKit.WARM_SOFT) - _side._recent_list.clip_text = true - _side._recent_list.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - col.add_child(_side._recent_list) - - -# 柜台列面板(方向稿同构):PanelContainer 米板身 + 头带深棕(图标+标题暖白) -func _counter_panel(title_text: String, icon_path: String, holder: Control, min_w: int) -> PanelContainer: - var panel := PanelContainer.new() - panel.custom_minimum_size = Vector2(min_w, 0) - panel.add_theme_stylebox_override("panel", - UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 1, Vector4(10, 10, 10, 10))) - var vbox := VBoxContainer.new() - vbox.add_theme_constant_override("separation", 8) - panel.add_child(vbox) - var band := PanelContainer.new() - band.add_theme_stylebox_override("panel", UiKit.panel_flat(HEADER_BAND_COL, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 1, Vector4(10, 5, 10, 5))) - vbox.add_child(band) - var head := HBoxContainer.new() - head.add_theme_constant_override("separation", 6) - band.add_child(head) - if icon_path != "": - var ic := UiKit.icon_rect(icon_path, 32) # 柜台头带 64 源 0.5×;缺位 null → 纯文本回退 - if ic != null: - head.add_child(ic) - var l := UiKit.label(title_text,UiKit.FS_BODY, true, UiKit.WARM) - head.add_child(l) - vbox.add_child(holder) - return panel - - -# ---- 柜台行构建(open_camp 重建;行结构仿 ShopOverlay 商品卡——亮板双色墨制)---- - -# 天赋树段重建(树淬炼柜台主界面:脉分组行板:节点名/等级/价/购买钮 + Fork 双态切换钮; -# 树数据 = 组合根 _camp_data().tree 投影 [{node_id, display_name, vein, kind, level, price, can_buy, -# currency}...] + fork_groups [{group, node_id, active_side, owned, can_buy, price, sides:[{side, display_name, effects}]}]) -func _rebuild_tree(data: Dictionary) -> void: - for c in _tree._tree_holder.get_children(): - c.queue_free() - _tree_buy_btns.clear() - _tree_fork_btns.clear() - var tree: Array[Variant] = data.get("tree", []) as Array - var forks: Array[Variant] = data.get("fork_groups", []) as Array - var vein_names := {"spine": "·脊柱淬炼·", "forge": "·锻造脉·", "keystone": "·钥石脉·", "attune": "·同调脉·"} - var last_vein := "" - for row in tree: - var vein := String(row.get("vein", "")) - if vein != last_vein: - last_vein = vein - var vh := UiKit.label(String(vein_names.get(vein, vein)),UiKit.FS_HINT, true, UiKit.GOLD_INK) # GOLD_DEEP 对柜台板 2.67:1 越带,换深金墨 5.21:1 - _tree._tree_holder.add_child(vh) - if String(row.get("kind", "")) == "fork": - continue # Fork 行去行板化——由 fork_groups 段承载双态切换钮 - _tree._tree_holder.add_child(_tree_row_line(row)) - for fg in forks: - _tree._tree_holder.add_child(_tree_fork_line(fg)) - - -## 树行板构建域:节点名(含门槛标记)+购买钮(追加序与锚句随段整体搬迁) -func _tree_row_line(row: Dictionary) -> HBoxContainer: - var hb := HBoxContainer.new() - hb.add_theme_constant_override("separation", 6) - var lvl := int(row.get("level", 0)) - var cap := int(row.get("cap", 1)) - var name_l := UiKit.label("%s Lv%d/%d" % [String(row.get("display_name", "")), lvl, cap], 13, false, UiKit.INK_LABEL) - name_l.custom_minimum_size = Vector2(150, 0) - name_l.clip_text = true - var gate := String(row.get("gate_text", "")) - if gate != "": - name_l.text += " 🔒" # 等级阈值前置未达标标记(R1 机制位——悬停看门槛;达标后随 refresh 消失) - name_l.tooltip_text = "等级门槛:%s" % gate - hb.add_child(name_l) - var btn := UiKit.button("购买 %d 钥石" % int(row.get("price", 0)), 0.0) # 柜台行板紧凑钮豁免宽度基准 - btn.pressed.connect(func(): tree_buy_requested.emit(String(row.get("node_id", "")))) - btn.disabled = not bool(row.get("can_buy", false)) - hb.add_child(btn) - _tree_buy_btns.append({"node_id": String(row.get("node_id", "")), "btn": btn}) - return hb - - -## Fork 双态行板构建域:组状态文案+解锁钮或双态切换钮 -func _tree_fork_line(fg: Dictionary) -> HBoxContainer: - var group := String(fg.get("group", "")) - var active := String(fg.get("active_side", "")) - var owned := bool(fg.get("owned", false)) - var fb := HBoxContainer.new() - fb.add_theme_constant_override("separation", 6) - var state_txt := "未解锁" - if owned: - state_txt = "激活:%s" % active - var gl := UiKit.label("%s[%s]" % [String(fg.get("display_name", group)), state_txt], 13, false, UiKit.INK_LABEL) - fb.add_child(gl) - if not owned: - var bb := UiKit.button("解锁 %d 钥石" % int(fg.get("price", 0)), 0.0) # 行板紧凑钮豁免 - bb.pressed.connect(func(): tree_buy_requested.emit(String(fg.get("node_id", "")))) - bb.disabled = not bool(fg.get("can_buy", false)) - fb.add_child(bb) - _tree_buy_btns.append({"node_id": String(fg.get("node_id", "")), "btn": bb}) - else: - for side_row in (fg.get("sides", []) as Array): - var sd := String(side_row.get("side", "")) - var sb := UiKit.button(String(side_row.get("display_name", sd)), 0.0) # 行板紧凑钮豁免 - sb.pressed.connect(func(): tree_fork_toggled.emit(group, sd)) - fb.add_child(sb) - _tree_fork_btns.append({"group": group, "side": sd, "btn": sb}) - return fb - - -# 工作间钥匙门(锻造工坊/秘宝匣柜台锁定态切换:节点点亮=展开、未点亮=锁定行; -# 图鉴常开无门槛。判据读树投影行 level——零核心访问,纯视图) -func _refresh_key_gates(data: Dictionary) -> void: - var lvl_by_id := {} - for row in (data.get("tree", []) as Array): - lvl_by_id[String(row.get("node_id", ""))] = int(row.get("level", 0)) - var forge_open := int(lvl_by_id.get("forge_workshop", 0)) >= 1 - var ws_open := int(lvl_by_id.get("ks_workshop", 0)) >= 1 - _forge._forge_lock_l.visible = not forge_open - _forge._forge_workbench.visible = forge_open - _forge._ws_lock_l.visible = not ws_open - _forge._ws_scroll.visible = ws_open - - -# 锻造工坊行板重建(两区六路线;open_camp/refresh_camp 调用。行结构仿秘宝匣行板: -# 件名+等级+解锁态+购买/解锁/选中钮。UI 只发意图信号〔契约 9——零核心写调用、零 MetaProgression -# 持有〕;钮禁用=常态防线(余额/未解锁/满级/已选中),兜底拒绝可见归 presenter pattern 1) -func _rebuild_forge(data: Dictionary) -> void: - for zone in [_forge._forge_zone_weapon, _forge._forge_zone_armor]: - for c in zone.get_children(): - c.queue_free() - _forge._forge_rows.clear() - if not _forge._forge_workbench.visible: - return # 锁定态(钥匙未点亮):零行板零购买路径——占位纪律延续 - var bal := int(data.get("balance", 0)) - for row in data.get("forge", []): - var kind := String(row.get("kind", "")) - var zone: VBoxContainer = _forge._forge_zone_weapon if kind == "weapon" else _forge._forge_zone_armor - var card := _forge_route_card(row, bal) - zone.add_child(card["card"]) - _forge._forge_rows.append(card) - - -## 锻造行板构建域:单路线卡(件名+等级+状态+购买/解锁/选中钮——并联双通道呈现面) -func _forge_route_card(row: Dictionary, bal: int) -> Dictionary: - var d := _forge_route_scalars(row) - var card := PanelContainer.new() - card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4))) - var line := HBoxContainer.new() - line.add_theme_constant_override("separation", 6) - card.add_child(line) - var name_l := UiKit.label(String(row.get("display_name", "")), UiKit.FS_HINT, true, UiKit.INK) - name_l.custom_minimum_size = Vector2(104, 0) - name_l.clip_text = true - name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - line.add_child(name_l) - var lvl_l := UiKit.label("Lv%d/%d" % [d["lvl"], d["cap_v"]], 13, false, UiKit.INK_LABEL) - lvl_l.custom_minimum_size = Vector2(44, 0) - line.add_child(lvl_l) - var status_l := UiKit.label(_forge_status_text(d, row), UiKit.FS_HINT, false, UiKit.STONE_DEEP) - status_l.clip_text = true - status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL - line.add_child(status_l) - var btns := _forge_route_buttons(d, row, bal, line) - return {"card": card, "route_id": d["rid"], "name_l": name_l, "lvl_l": lvl_l, "status_l": status_l, - "buy_btn": btns["buy_btn"], "unlock_btn": btns["unlock_btn"], "select_btn": btns["select_btn"], - "next_price": d["next_price"], "unlock_price": d["unlock_price"]} - - -## 锻造行标量束:路线 id/解锁态/初始持有/等级/cap/几何价/解锁价一次取数 -func _forge_route_scalars(row: Dictionary) -> Dictionary: - return { - "rid": String(row.get("route_id", "")), - "unlocked": bool(row.get("unlocked", false)), - "initial": bool(row.get("initial_owned", false)), - "lvl": int(row.get("level", 0)), - "cap_v": int(row.get("cap", 0)), - "next_price": int(row.get("next_price", 0)), - "unlock_price": int(row.get("unlock_price", 0)), - } - - -## 状态文案域(并联双通道:练满前一档 or 一次性解锁价直购;已满级态) -func _forge_status_text(d: Dictionary, row: Dictionary) -> String: - var st_text := "已解锁" - if not d["unlocked"]: - if d["initial"]: - st_text = "未解锁" - else: - st_text = "未解锁 · 练满%s 或 %d 钥石直购" % [String(row.get("prereq_name", "")), d["unlock_price"]] - elif d["lvl"] >= d["cap_v"]: - st_text = "已满级" - return st_text - - -## 三钮域:购买(已解锁未满级——余额不足禁用)/解锁(待解锁非初始——余额不足禁用)/选中(已解锁——当前=禁用态标记) -func _forge_route_buttons(d: Dictionary, row: Dictionary, bal: int, line: HBoxContainer) -> Dictionary: - var buy_btn: Button = null - if d["unlocked"] and d["lvl"] < d["cap_v"]: - buy_btn = UiKit.button("锻造 %d" % d["next_price"], 0.0) # 行板紧凑钮豁免 - buy_btn.pressed.connect(forge_buy_requested.emit.bind(d["rid"])) - buy_btn.disabled = bal < d["next_price"] - line.add_child(buy_btn) - var unlock_btn: Button = null - if not d["unlocked"] and not d["initial"]: - unlock_btn = UiKit.button("解锁 %d" % d["unlock_price"], 0.0) # 行板紧凑钮豁免 - unlock_btn.pressed.connect(forge_unlock_requested.emit.bind(d["rid"])) - unlock_btn.disabled = bal < d["unlock_price"] - line.add_child(unlock_btn) - var select_btn: Button = null - if d["unlocked"]: - var sel := bool(row.get("selected", false)) - select_btn = UiKit.button("当前" if sel else "选中", 0.0) # 行板紧凑钮豁免 - select_btn.pressed.connect(forge_select_requested.emit.bind(d["rid"])) - select_btn.disabled = sel - line.add_child(select_btn) - return {"buy_btn": buy_btn, "unlock_btn": unlock_btn, "select_btn": select_btn} - - -# 打开营地(全量重建四柜台行 + 同调装配段 + 最近冒险;树段与钥匙门随 refresh_camp 重建—— -# 数据 = 组合根 _camp_data() 投影) -func open_camp(data: Dictionary) -> void: - _rebuild_workshop(data) # 段尾链 _rebuild_attune(同调装配段+Y 升级行随秘宝匣柜台内容) - _rebuild_forge(data) # 锻造工坊两区六路线行板 - _rebuild_recent(data) - refresh_camp(data) - visible = true - - -# 状态重刷(购买后调用——余额/树行板/钥匙门/工坊状态就地重建或改文案;树行板随刷重建=按钮 -# 禁用态与门槛/cap 同步,12 行重建开销可忽略) -func refresh_camp(data: Dictionary) -> void: - var balance := int(data.get("balance", 0)) - _balance_label.text = "钥石 %d" % balance - _rebuild_tree(data) - _refresh_key_gates(data) - _rebuild_forge(data) # 行板随刷重建(购买/解锁/选中后等级/价/禁用态同步) - _refresh_workshop_rows(data, balance) - _refresh_attune_segment(data, balance) - - -## 秘宝匣行板刷新域:进度列(解锁混合制三形态)与状态色(亮板双色墨制)与购买钮余额联动 -func _refresh_workshop_rows(data: Dictionary, balance: int) -> void: - var ws_by_id := {} - for row in data.get("workshop", []): - ws_by_id[String(row.get("item_id", ""))] = row - for card in _workshop_rows: - var row: Dictionary[Variant, Variant] = ws_by_id.get(String(card.item_id), {}) - if row.is_empty(): - continue - var unlocked := bool(row.get("unlocked", false)) - card.progress_l.text = _workshop_progress_text(row, unlocked) - # 状态色用亮板双色墨制(深墨族替代暖白族——亮板上暖白不可读) - var st_text := "未解锁" - var st_col: Color = UiKit.STONE_DEEP - if unlocked: - st_text = "已解锁" - st_col = UiKit.GOLD_INK # GOLD_DEEP 对行板 3.00:1 越带,换深金墨 5.84:1 - card.status_l.text = st_text - card.status_l.add_theme_color_override("font_color", st_col) - card.status_l.tooltip_text = st_text # 截断兜底(状态列 EXPAND_FILL 吸余宽——悬停看全文) - # 购买钮余额联动(禁用态=余额不足,常态防线;pattern 1 兜底拒绝可见归 presenter) - if card.get("buy_btn") != null: - card.buy_btn.disabled = balance < int(card.get("price", 0)) - - -## 进度列文案域(解锁混合制三形态:钥石购买价/条件进度拼接/已入手) -func _workshop_progress_text(row: Dictionary, unlocked: bool) -> String: - if unlocked: - return "已入手" - var mode := String(row.get("mode", "")) - if mode == "keystone": - return "钥石 %d" % int(row.get("price", 0)) - if mode == "condition": - var parts: Array[String] = [] - for req_v in (row.get("reqs", []) as Array): - var req: Dictionary[Variant, Variant] = req_v as Dictionary - var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件")) - parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))]) - return "、".join(parts) - return "" - - -## 同调装配段刷新域(Slots 点亮/满位禁选/Y 升级行余额联动——就地刷) -func _refresh_attune_segment(data: Dictionary, balance: int) -> void: - var att: Dictionary[Variant, Variant] = data.get("attune", {}) - if _attune._attune_header_l != null: - var slots_n := int(att.get("slots", 0)) - var eq: Array[Variant] = att.get("equipped", []) - _attune._attune_header_l.text = "── 同调装配 Slots %d/%d ──" % [eq.size(), slots_n] - var eq_set := {} - for v in eq: - eq_set[String(v)] = true - for card2 in _attune._attune_btns: - var on: bool = eq_set.has(String(card2.item_id)) - card2.btn.text = "卸下" if on else "装配" - # 满位禁选+未解锁禁装(已装件恒可卸下;unlocked 快照随 rebuild 刷新) - card2.btn.disabled = (not on and eq.size() >= slots_n) or (not on and not bool(card2.get("unlocked", false))) - if _side._refresh_l != null: - var rf: Dictionary[Variant, Variant] = att.get("refresh", {}) - _side._refresh_l.text = "三选一刷新 +%d 次/冒险 · Lv%d" % [int(rf.get("charges", 0)), int(rf.get("level", 0))] - _side._refresh_btn.text = "已满级" if bool(rf.get("maxed", false)) else "升级 %d 钥石" % int(rf.get("price", 0)) - _side._refresh_btn.disabled = bool(rf.get("maxed", false)) or balance < int(rf.get("price", 0)) - - -func _rebuild_workshop(data: Dictionary) -> void: - _workshop_rows.clear() - for child in _workshop_holder.get_children(): - child.queue_free() - _attune._attune_nodes.clear() - _attune._attune_btns.clear() - _attune._attune_header_l = null - _side._refresh_l = null - _side._refresh_btn = null - for row in data.get("workshop", []): - var card := _workshop_card(row) - _workshop_holder.add_child(card["card"]) - _workshop_rows.append(card) - _rebuild_attune(data) - - -## 秘宝匣行板构建域:件名(分档后缀+效果摘要悬停)+进度列+状态列+钥石购买钮(购买件未解锁在列) -func _workshop_card(row: Dictionary) -> Dictionary: - var card := PanelContainer.new() - card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4))) - var line := HBoxContainer.new() - line.add_theme_constant_override("separation", 12) - card.add_child(line) - var tier_name := String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "")) - var name_l := UiKit.label(String(row.get("display_name", "")) + ("·" + tier_name if tier_name != "" else ""),UiKit.FS_HINT, true, UiKit.INK) - name_l.custom_minimum_size = Vector2(132, 0) - name_l.clip_text = true - name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - name_l.tooltip_text = String(row.get("desc", "")) # 件效果摘要悬停可见(分档呈现契约——槽位档位=名后缀) - line.add_child(name_l) - var progress_l := UiKit.label("",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # 同行价格墨同案 - progress_l.custom_minimum_size = Vector2(150, 0) - progress_l.clip_text = true # 营地溢出修——文字全宽不撑柜 - progress_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - line.add_child(progress_l) - # 状态色用亮板双色墨制(深墨族替代暖白族)——色随刷新态条件赋值,非机械两行式 - var status_l := UiKit.label("",UiKit.FS_HINT, true, UiKit.STONE_DEEP) - status_l.clip_text = true # 同上——「已解锁 · 能力封存中」不撑柜(余宽吸收者) - status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL - line.add_child(status_l) - # 钥石购买钮(购买件且未解锁时在列;写口唯一=buy_treasure_unlock, - # 经 buy_requested(item_id) 信号走 presenter 分支;禁用态=余额不足) - var buy_btn: Button = null - if not bool(row.get("unlocked", false)) and String(row.get("mode", "")) == "keystone": - buy_btn = UiKit.button("购买 %d" % int(row.get("price", 0)), 0.0) # 行板紧凑钮豁免 - buy_btn.pressed.connect(_emit_buy_requested.bind(String(row.get("item_id", "")))) - line.add_child(buy_btn) - return {"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l, "buy_btn": buy_btn, "price": int(row.get("price", 0)), "unlocked": bool(row.get("unlocked", false)), "card": card} - - -# 同调装配段重建(秘宝匣柜台尾部段;Slots 点亮+ -# 九件行板+Y 升级行,程序占位面) -func _rebuild_attune(data: Dictionary) -> void: - var att: Dictionary[Variant, Variant] = data.get("attune", {}) - _attune._attune_header_l = UiKit.label("",UiKit.FS_HINT, true, UiKit.INK) - _workshop_holder.add_child(_attune._attune_header_l) - _attune._attune_nodes.append(_attune._attune_header_l) - for row in att.get("items", []): - var line := _attune_item_line(row) - _workshop_holder.add_child(line) - _attune._attune_nodes.append(line) - _refresh_attune_tail(att) - - -## 同调件行板构建域:件名+形态+档位+装配切换钮(未解锁禁装——toggle 写口同守卫) -func _attune_item_line(row: Dictionary) -> HBoxContainer: - var iid := String(row.get("item_id", "")) - var line := HBoxContainer.new() - line.add_theme_constant_override("separation", 6) - var name_l := UiKit.label(String(row.get("display_name", "")),UiKit.FS_HINT, true, UiKit.INK) - name_l.custom_minimum_size = Vector2(96, 0) - name_l.clip_text = true - name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - line.add_child(name_l) - var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) - form_l.custom_minimum_size = Vector2(58, 0) - line.add_child(form_l) - # 档位标签(槽位档位显示:件档=普通/优秀/稀有,装入须件档≤格档契约 2 的呈现半边) - var tier_l := UiKit.label(String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "—")),UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) - tier_l.custom_minimum_size = Vector2(36, 0) - line.add_child(tier_l) - var btn := UiKit.button("装配", 0.0) # 行板紧凑钮豁免 - btn.pressed.connect(attune_toggled.emit.bind(iid)) - btn.disabled = not bool(row.get("unlocked", false)) # 未解锁禁装(解锁为装配前置——toggle 写口同守卫) - line.add_child(btn) - _attune._attune_btns.append({"item_id": iid, "btn": btn, "unlocked": bool(row.get("unlocked", false)), "equipped": bool(row.get("equipped", false))}) - return line - - -## Y 升级尾行构建域(双入口之一——树上 ks_refresh 同账) -func _refresh_attune_tail(att: Dictionary) -> void: - var refresh_line := HBoxContainer.new() - refresh_line.add_theme_constant_override("separation", 6) - _side._refresh_l = UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) - refresh_line.add_child(_side._refresh_l) - _side._refresh_btn = UiKit.button("升级", 0.0) # 行板紧凑钮豁免 - _side._refresh_btn.pressed.connect(_emit_buy_requested.bind("attune_refresh")) - refresh_line.add_child(_side._refresh_btn) - _workshop_holder.add_child(refresh_line) - _attune._attune_nodes.append(refresh_line) - - -func _rebuild_recent(data: Dictionary) -> void: - var lines: Array[String] = [] - var recent: Array[Variant] = data.get("recent", []) - if recent.is_empty(): - lines.append(" 最近冒险:(暂无记录——完成或结束一次冒险后入账)") - for s in recent: - var verdict := "失败" - if String(s.get("result", "")) == "completed": - verdict = "完成" - elif String(s.get("result", "")) == "abandoned": - verdict = "放弃" - lines.append("最近冒险:%s · 完成 %d 关 · 钥石 +%d · 种子 %d" % [verdict, - int(s.get("levels_cleared", 0)), int(s.get("keystone_total", 0)), int(s.get("master_seed", 0))]) - _side._recent_list.text = " | ".join(lines) - - -func _emit_closed() -> void: - closed.emit() - - -func _emit_buy_requested(attr: String) -> void: - buy_requested.emit(attr) diff --git a/game/presentation/views/camp_overlay_view.gd.uid b/game/presentation/views/camp_overlay_view.gd.uid deleted file mode 100644 index eaa6d5de..00000000 --- a/game/presentation/views/camp_overlay_view.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dvg2wmjh87p8a diff --git a/game/presentation/views/camp_refresh_bus.gd b/game/presentation/views/camp_refresh_bus.gd new file mode 100644 index 00000000..c275264e --- /dev/null +++ b/game/presentation/views/camp_refresh_bus.gd @@ -0,0 +1,17 @@ +## 营地刷新单口:各写口尾→现开界面刷新+场景根 HUD 刷新一闸完成(卡⑤ HUD 同口挂接)。 +## 非 autoload、非信号广播——组合根单点推送(组合根建件绑册,presenter 经 context 持引用; +## 卡④ 契约 4:写口语义零改,只改路由形态)。 +class_name CampRefreshBus +extends RefCounted + +var _targets: Array[Control] = [] # 可刷面册(四子界面+卡⑤ HUD 栏——均带 refresh_camp(data)) + + +func bind_targets(targets: Array[Control]) -> void: + _targets = targets + + +func refresh(camp_data: Dictionary) -> void: + for t in _targets: + if t != null and t.visible and t.has_method("refresh_camp"): + t.refresh_camp(camp_data) # 现开界面就地重刷(未开的零开销——留底形态下场景根恒开) diff --git a/game/presentation/views/camp_refresh_bus.gd.uid b/game/presentation/views/camp_refresh_bus.gd.uid new file mode 100644 index 00000000..b953a12c --- /dev/null +++ b/game/presentation/views/camp_refresh_bus.gd.uid @@ -0,0 +1 @@ +uid://cdfmyxf87n6f6 diff --git a/game/presentation/views/camp_sub_view_base.gd b/game/presentation/views/camp_sub_view_base.gd new file mode 100644 index 00000000..b03b7384 --- /dev/null +++ b/game/presentation/views/camp_sub_view_base.gd @@ -0,0 +1,86 @@ +## 营地子界面模态壳基类(锻造工坊/秘宝匣/天赋树/奖励格图鉴四子界面公共形态): +## 实底全屏暖亮档+石纹暗角+顶栏(铭牌+钥石徽)+内容列+底栏返回钮。 +## 开合可见性归模态栈(登记层开合一律走栈);返回钮只发 closed——关层编排归 presenter(组合根单口)。 +## 余额徽随子类 refresh_camp 刷新(钥石读数=投影单口,禁第二账本)。 +class_name CampSubViewBase +extends Control + +signal closed + +var _balance_label: Label # 钥石徽数值行(探针直读面——ui_probe 余额文案断言改指新界面) + + +## 壳构建(子类 _init 调用一次):content 由子类先建好传入(列容器/滚动容器皆可) +func _build_shell(title: String, icon_path: String, content: Control) -> void: + # 实底暖亮档(半透 dim 会透出底下场景图——CampOverlay 同案;遮蔽语义由组合根输入锁兜底) + var dim := UiKit._modal_dim(self, UiKit.BG_BRIGHT) + var bd := UiKit.backdrop_tile() + if bd != null: + dim.add_child(bd) + dim.add_child(UiKit.vignette_rect()) + var margin := MarginContainer.new() + margin.set_anchors_preset(Control.PRESET_FULL_RECT) + margin.add_theme_constant_override("margin_left", 16) + margin.add_theme_constant_override("margin_right", 16) + margin.add_theme_constant_override("margin_top", 12) + margin.add_theme_constant_override("margin_bottom", 12) + dim.add_child(margin) + var col := VBoxContainer.new() + col.add_theme_constant_override("separation", 8) + margin.add_child(col) + _build_top_bar(col, title, icon_path) + content.size_flags_vertical = Control.SIZE_EXPAND_FILL + col.add_child(content) + _build_bottom_bar(col) + visible = false # 登记模态初始隐(开合走栈) + + +## 顶栏域:铭牌(标题)+钥石徽(余额徽单点——子类 refresh_camp 刷新数值行) +func _build_top_bar(col: VBoxContainer, title: String, icon_path: String) -> void: + var top := HBoxContainer.new() + top.add_theme_constant_override("separation", 10) + col.add_child(top) + var plaque := PanelContainer.new() + plaque.add_theme_stylebox_override("panel", + UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(14, 8, 14, 8))) + top.add_child(plaque) + var head := HBoxContainer.new() + head.add_theme_constant_override("separation", 6) + plaque.add_child(head) + if icon_path != "": + var ic := UiKit.icon_rect(icon_path, 32) # 头带图标缺位 null → 纯文本回退 + if ic != null: + head.add_child(ic) + head.add_child(UiKit.label(title, UiKit.FS_TITLE, true, UiKit.INK_TITLE)) + var spacer := Control.new() + spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL + top.add_child(spacer) + var badge := UiKit.keystone_badge("钥石 0") # UiKit 工厂单点(烛光黄底深墨字) + top.add_child(badge) + _balance_label = badge.value_label + + +## 底栏域:返回营地钮(closed 信号——关层编排归 presenter) +func _build_bottom_bar(col: VBoxContainer) -> void: + var bottom := HBoxContainer.new() + bottom.add_theme_constant_override("separation", 10) + col.add_child(bottom) + var back := UiKit.button("返回营地") + back.pressed.connect(_emit_closed) + bottom.add_child(back) + var spacer2 := Control.new() + spacer2.size_flags_horizontal = Control.SIZE_EXPAND_FILL + bottom.add_child(spacer2) + + +func open_camp(data: Dictionary) -> void: + refresh_camp(data) + visible = true # 开可见性由栈承载(本行为开层数据投喂的收尾拍) + + +func refresh_camp(data: Dictionary) -> void: + pass # 子类覆写(余额徽刷新+各自行板/门判据) + + +func _emit_closed() -> void: + closed.emit() diff --git a/game/presentation/views/camp_sub_view_base.gd.uid b/game/presentation/views/camp_sub_view_base.gd.uid new file mode 100644 index 00000000..851356aa --- /dev/null +++ b/game/presentation/views/camp_sub_view_base.gd.uid @@ -0,0 +1 @@ +uid://dv0gyjyrtoxkh diff --git a/game/presentation/views/codex_view.gd b/game/presentation/views/codex_view.gd new file mode 100644 index 00000000..dfdbc362 --- /dev/null +++ b/game/presentation/views/codex_view.gd @@ -0,0 +1,25 @@ +## 奖励格图鉴子界面(自 CampOverlay 图鉴柜台抽出)——占位壳: +## 内容保持挂起(用户令冻结,重启须用户另行发令——卡④ 契约 5),点进看占位、不是禁用态。 +## 锁文照录原柜台锁文并随形态订正『柜台』称谓(L2 记录——零新文案)。 +class_name CodexView +extends CampSubViewBase + + +func _init() -> void: + var holder := VBoxContainer.new() + holder.custom_minimum_size = Vector2(0, 336) + var lock_l := UiKit.label(" —— 结构位预留:内容排 Alpha(地图战斗资源差异化批)", UiKit.FS_HINT, false, UiKit.STONE_DEEP) + lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER + lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + lock_l.custom_minimum_size = Vector2(280, 0) + holder.add_child(lock_l) + var scroll := ScrollContainer.new() + scroll.custom_minimum_size = Vector2(300, 336) + scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL + scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + scroll.add_child(holder) + _build_shell("奖励格图鉴", UiKit.TEX_ICON_ATLAS, scroll) + + +func refresh_camp(data: Dictionary) -> void: + _balance_label.text = "钥石 %d" % int(data.get("balance", 0)) # 内容挂起——余额徽随刷(占位态无行板) diff --git a/game/presentation/views/codex_view.gd.uid b/game/presentation/views/codex_view.gd.uid new file mode 100644 index 00000000..597b330f --- /dev/null +++ b/game/presentation/views/codex_view.gd.uid @@ -0,0 +1 @@ +uid://m1bcfjiceyiq diff --git a/game/presentation/views/forge_view.gd b/game/presentation/views/forge_view.gd new file mode 100644 index 00000000..f47054a5 --- /dev/null +++ b/game/presentation/views/forge_view.gd @@ -0,0 +1,160 @@ +## 锻造工坊子界面(自 CampOverlay 锻造柜台抽出——承载面迁移,界面逻辑复用零重设计): +## 钥匙门(树上 forge_workshop 点亮前锁定态零行板零购买路径;点亮=两区六路线行板)。 +## UI 只发意图信号(写口唯一归 MetaProgression 锻造写口族);钮禁用=常态防线(余额/未解锁/满级/已选中)。 +class_name ForgeView +extends CampSubViewBase + +signal forge_buy_requested(route_id: String) ## 锻造路线购买意图(写口唯一归 MetaProgression.forge_buy_level) +signal forge_unlock_requested(route_id: String) ## 锻造路线解锁意图(写口唯一归 MetaProgression.forge_buy_unlock) +signal forge_select_requested(route_id: String) ## 锻造路线选中意图(写口唯一归 MetaProgression.forge_select) + +const HEADER_BAND_COL := UiKit.CAMP_HEADER_BAND # 方向稿取色(柜台头带深棕/行板暖米)——值单点收 UiKit +const ROW_PLATE_COL := UiKit.CAMP_ROW_PLATE + +var _forge_lock_l: Label # 锁定态行(钥匙未点亮) +var _forge_workbench: VBoxContainer # 工作间(钥匙点亮后展开——两区六路线行板) +var _forge_zone_weapon: VBoxContainer # 武器锻造区行板容器(三路线) +var _forge_zone_armor: VBoxContainer # 护甲锻造区行板容器(三路线) +var _forge_rows: Array[Dictionary] = [] # [{route_id, name_l, lvl_l, status_l, buy_btn, unlock_btn, select_btn, next_price, unlock_price}](open/refresh 重建;探针消费) + + +func _init() -> void: + var holder := VBoxContainer.new() + holder.add_theme_constant_override("separation", 6) + holder.custom_minimum_size = Vector2(0, 336) + _forge_lock_l = UiKit.label("🔒 锻造工坊未开放\n\n点亮树上「锻造工坊」钥匙节点后展开工作间。\n(钥匙免费、无前置——树上直接点亮)", UiKit.FS_HINT, false, UiKit.STONE_DEEP) + _forge_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER + _forge_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _forge_lock_l.custom_minimum_size = Vector2(280, 0) + _forge_workbench = VBoxContainer.new() + _forge_workbench.add_theme_constant_override("separation", 6) + _forge_workbench.visible = false # 默认锁定态(钥匙门未判前零行板——门开才建) + _forge_zone_weapon = VBoxContainer.new() + _forge_zone_weapon.add_theme_constant_override("separation", 4) + _forge_zone_armor = VBoxContainer.new() + _forge_zone_armor.add_theme_constant_override("separation", 4) + var wb_wh := UiKit.label("── 武器锻造区 ──", UiKit.FS_HINT, true, UiKit.GOLD_INK) + _forge_workbench.add_child(wb_wh) + _forge_workbench.add_child(_forge_zone_weapon) + var wb_ah := UiKit.label("── 护甲锻造区 ──", UiKit.FS_HINT, true, UiKit.GOLD_INK) + _forge_workbench.add_child(wb_ah) + _forge_workbench.add_child(_forge_zone_armor) + holder.add_child(_forge_lock_l) + holder.add_child(_forge_workbench) + var scroll := ScrollContainer.new() + scroll.custom_minimum_size = Vector2(300, 336) + scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL + scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + scroll.add_child(holder) + _build_shell("锻造工坊", UiKit.TEX_ICON_FORGE, scroll) + + +func refresh_camp(data: Dictionary) -> void: + _balance_label.text = "钥石 %d" % int(data.get("balance", 0)) + _refresh_key_gate(data) + _rebuild_forge(data) + + +# 工作间钥匙门(树上 forge_workshop 行 level 判据——零核心访问,纯视图读投影) +func _refresh_key_gate(data: Dictionary) -> void: + var lvl := 0 + for row in (data.get("tree", []) as Array): + if String(row.get("node_id", "")) == "forge_workshop": + lvl = int(row.get("level", 0)) + var open := lvl >= 1 + _forge_lock_l.visible = not open + _forge_workbench.visible = open + + +# 锻造行板重建(两区六路线;refresh_camp 调用。行结构仿秘宝匣行板:件名+等级+状态+购买/解锁/选中钮) +func _rebuild_forge(data: Dictionary) -> void: + for zone in [_forge_zone_weapon, _forge_zone_armor]: + for c in zone.get_children(): + c.queue_free() + _forge_rows.clear() + if not _forge_workbench.visible: + return # 锁定态(钥匙未点亮):零行板零购买路径——占位纪律延续 + var bal := int(data.get("balance", 0)) + for row in data.get("forge", []): + var kind := String(row.get("kind", "")) + var zone: VBoxContainer = _forge_zone_weapon if kind == "weapon" else _forge_zone_armor + var card := _forge_route_card(row, bal) + zone.add_child(card["card"]) + _forge_rows.append(card) + + +## 锻造行板构建域:单路线卡(件名+等级+状态+购买/解锁/选中钮——并联双通道呈现面) +func _forge_route_card(row: Dictionary, bal: int) -> Dictionary: + var d := _forge_route_scalars(row) + var card := PanelContainer.new() + card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4))) + var line := HBoxContainer.new() + line.add_theme_constant_override("separation", 6) + card.add_child(line) + var name_l := UiKit.label(String(row.get("display_name", "")), UiKit.FS_HINT, true, UiKit.INK) + name_l.custom_minimum_size = Vector2(104, 0) + name_l.clip_text = true + name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + line.add_child(name_l) + var lvl_l := UiKit.label("Lv%d/%d" % [d["lvl"], d["cap_v"]], 13, false, UiKit.INK_LABEL) + lvl_l.custom_minimum_size = Vector2(44, 0) + line.add_child(lvl_l) + var status_l := UiKit.label(_forge_status_text(d, row), UiKit.FS_HINT, false, UiKit.STONE_DEEP) + status_l.clip_text = true + status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL + line.add_child(status_l) + var btns := _forge_route_buttons(d, row, bal, line) + return {"card": card, "route_id": d["rid"], "name_l": name_l, "lvl_l": lvl_l, "status_l": status_l, + "buy_btn": btns["buy_btn"], "unlock_btn": btns["unlock_btn"], "select_btn": btns["select_btn"], + "next_price": d["next_price"], "unlock_price": d["unlock_price"]} + + +## 锻造行标量束:路线 id/解锁态/初始持有/等级/cap/几何价/解锁价一次取数 +func _forge_route_scalars(row: Dictionary) -> Dictionary: + return { + "rid": String(row.get("route_id", "")), + "unlocked": bool(row.get("unlocked", false)), + "initial": bool(row.get("initial_owned", false)), + "lvl": int(row.get("level", 0)), + "cap_v": int(row.get("cap", 0)), + "next_price": int(row.get("next_price", 0)), + "unlock_price": int(row.get("unlock_price", 0)), + } + + +## 状态文案域(并联双通道:练满前一档 or 一次性解锁价直购;已满级态) +func _forge_status_text(d: Dictionary, row: Dictionary) -> String: + var st_text := "已解锁" + if not d["unlocked"]: + if d["initial"]: + st_text = "未解锁" + else: + st_text = "未解锁 · 练满%s 或 %d 钥石直购" % [String(row.get("prereq_name", "")), d["unlock_price"]] + elif d["lvl"] >= d["cap_v"]: + st_text = "已满级" + return st_text + + +## 三钮域:购买(已解锁未满级——余额不足禁用)/解锁(待解锁非初始——余额不足禁用)/选中(已解锁——当前=禁用态标记) +func _forge_route_buttons(d: Dictionary, row: Dictionary, bal: int, line: HBoxContainer) -> Dictionary: + var buy_btn: Button = null + if d["unlocked"] and d["lvl"] < d["cap_v"]: + buy_btn = UiKit.button("锻造 %d" % d["next_price"], 0.0) # 行板紧凑钮豁免 + buy_btn.pressed.connect(forge_buy_requested.emit.bind(d["rid"])) + buy_btn.disabled = bal < d["next_price"] + line.add_child(buy_btn) + var unlock_btn: Button = null + if not d["unlocked"] and not d["initial"]: + unlock_btn = UiKit.button("解锁 %d" % d["unlock_price"], 0.0) # 行板紧凑钮豁免 + unlock_btn.pressed.connect(forge_unlock_requested.emit.bind(d["rid"])) + unlock_btn.disabled = bal < d["unlock_price"] + line.add_child(unlock_btn) + var select_btn: Button = null + if d["unlocked"]: + var sel := bool(row.get("selected", false)) + select_btn = UiKit.button("当前" if sel else "选中", 0.0) # 行板紧凑钮豁免 + select_btn.pressed.connect(forge_select_requested.emit.bind(d["rid"])) + select_btn.disabled = sel + line.add_child(select_btn) + return {"buy_btn": buy_btn, "unlock_btn": unlock_btn, "select_btn": select_btn} diff --git a/game/presentation/views/forge_view.gd.uid b/game/presentation/views/forge_view.gd.uid new file mode 100644 index 00000000..aa904b0e --- /dev/null +++ b/game/presentation/views/forge_view.gd.uid @@ -0,0 +1 @@ +uid://b77xh0e3txmh5 diff --git a/game/presentation/views/treasure_view.gd b/game/presentation/views/treasure_view.gd new file mode 100644 index 00000000..8dd45a12 --- /dev/null +++ b/game/presentation/views/treasure_view.gd @@ -0,0 +1,239 @@ +## 秘宝匣子界面(自 CampOverlay 秘宝匣柜台抽出——承载面迁移,界面逻辑复用零重设计): +## 钥匙门(树上 ks_workshop 点亮前锁定态;点亮=12 件行板+同调装配段+Y 刷新升级行整体迁入)。 +## UI 只发意图信号(购买写口唯一归 MetaProgression.buy_treasure_unlock / toggle_attunement / buy_attune_refresh)。 +class_name TreasureView +extends CampSubViewBase + +signal buy_requested(attr: String) ## 秘宝购买/Y 刷新升级意图(presenter 按 attr 分派) +signal attune_toggled(item_id: String) ## 同调装配切换意图(写口唯一归 MetaProgression.toggle_attunement) + +const HEADER_BAND_COL := UiKit.CAMP_HEADER_BAND +const ROW_PLATE_COL := UiKit.CAMP_ROW_PLATE + +var _ws_lock_l: Label # 锁定态行(ks_workshop 未点亮) +var _ws_scroll: ScrollContainer # 秘宝匣内容容器(钥匙点亮后可见) +var _workshop_holder: VBoxContainer # 秘宝匣行板(六行+同调装配段+Y 升级行;探针直读冻结) +var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l, buy_btn, price, unlocked, card}](探针消费) +var _attune_header_l: Label # 同调装配段(Slots 点亮呈现) +var _attune_btns: Array[Dictionary] = [] # [{item_id, btn, unlocked, equipped}]——装配/卸下切换钮(满位禁选随刷) +var _attune_nodes: Array[Control] = [] # 段内节点册(open 重建时定向清理,不扰工坊行板) +var _refresh_l: Label # Y 刷新升级行(双入口之一——树上 ks_refresh 同账) +var _refresh_btn: Button + + +func _init() -> void: + var holder := VBoxContainer.new() + holder.add_theme_constant_override("separation", 6) + holder.custom_minimum_size = Vector2(0, 336) + _ws_lock_l = UiKit.label("🔒 秘宝匣未开放\n\n点亮树上「秘宝匣」钥匙节点后展开。\n(前置=脊柱生命第 1 级)", UiKit.FS_HINT, false, UiKit.STONE_DEEP) + _ws_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER + _ws_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _ws_lock_l.custom_minimum_size = Vector2(280, 0) + _ws_scroll = ScrollContainer.new() + _ws_scroll.custom_minimum_size = Vector2(300, 336) + _ws_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + _ws_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + _workshop_holder = VBoxContainer.new() + _workshop_holder.add_theme_constant_override("separation", 6) + _ws_scroll.add_child(_workshop_holder) + holder.add_child(_ws_lock_l) + holder.add_child(_ws_scroll) + var scroll := ScrollContainer.new() + scroll.custom_minimum_size = Vector2(300, 336) + scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL + scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + scroll.add_child(holder) + _build_shell("秘宝匣", UiKit.TEX_ICON_WORKSHOP, scroll) + + +func open_camp(data: Dictionary) -> void: + _rebuild_workshop(data) # 行板+同调装配段+Y 尾行随开层全量重建(段尾链 _rebuild_attune) + super.open_camp(data) + + +func refresh_camp(data: Dictionary) -> void: + var balance := int(data.get("balance", 0)) + _balance_label.text = "钥石 %d" % balance + _refresh_key_gate(data) + _refresh_workshop_rows(data, balance) + _refresh_attune_segment(data, balance) + + +# 秘宝匣钥匙门(树上 ks_workshop 行 level 判据——零核心访问,纯视图读投影) +func _refresh_key_gate(data: Dictionary) -> void: + var lvl := 0 + for row in (data.get("tree", []) as Array): + if String(row.get("node_id", "")) == "ks_workshop": + lvl = int(row.get("level", 0)) + var open := lvl >= 1 + _ws_lock_l.visible = not open + _ws_scroll.visible = open + + +func _rebuild_workshop(data: Dictionary) -> void: + _workshop_rows.clear() + for child in _workshop_holder.get_children(): + child.queue_free() + _attune_nodes.clear() + _attune_btns.clear() + _attune_header_l = null + _refresh_l = null + _refresh_btn = null + for row in data.get("workshop", []): + var card := _workshop_card(row) + _workshop_holder.add_child(card["card"]) + _workshop_rows.append(card) + _rebuild_attune(data) + + +## 秘宝匣行板构建域:件名(分档后缀+效果摘要悬停)+进度列+状态列+钥石购买钮(购买件未解锁在列) +func _workshop_card(row: Dictionary) -> Dictionary: + var card := PanelContainer.new() + card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4))) + var line := HBoxContainer.new() + line.add_theme_constant_override("separation", 12) + card.add_child(line) + var tier_name := String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "")) + var name_l := UiKit.label(String(row.get("display_name", "")) + ("·" + tier_name if tier_name != "" else ""), UiKit.FS_HINT, true, UiKit.INK) + name_l.custom_minimum_size = Vector2(132, 0) + name_l.clip_text = true + name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + name_l.tooltip_text = String(row.get("desc", "")) # 件效果摘要悬停可见(分档呈现契约——槽位档位=名后缀) + line.add_child(name_l) + var progress_l := UiKit.label("", UiKit.FS_HINT, false, UiKit.STONE_DEEP) + progress_l.custom_minimum_size = Vector2(150, 0) + progress_l.clip_text = true # 营地溢出修——文字全宽不撑柜 + progress_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + line.add_child(progress_l) + # 状态色用亮板双色墨制(深墨族替代暖白族)——色随刷新态条件赋值,非机械两行式 + var status_l := UiKit.label("", UiKit.FS_HINT, true, UiKit.STONE_DEEP) + status_l.clip_text = true # 「已解锁 · 能力封存中」不撑柜(余宽吸收者) + status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL + line.add_child(status_l) + # 钥石购买钮(购买件且未解锁时在列;写口唯一=buy_treasure_unlock, + # 经 buy_requested(item_id) 信号走 presenter 分支;禁用态=余额不足) + var buy_btn: Button = null + if not bool(row.get("unlocked", false)) and String(row.get("mode", "")) == "keystone": + buy_btn = UiKit.button("购买 %d" % int(row.get("price", 0)), 0.0) # 行板紧凑钮豁免 + buy_btn.pressed.connect(_emit_buy_requested.bind(String(row.get("item_id", "")))) + line.add_child(buy_btn) + return {"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l, "buy_btn": buy_btn, "price": int(row.get("price", 0)), "unlocked": bool(row.get("unlocked", false)), "card": card} + + +# 同调装配段重建(秘宝匣尾部段:Slots 头行+九件行板+Y 升级尾行) +func _rebuild_attune(data: Dictionary) -> void: + var att: Dictionary[Variant, Variant] = data.get("attune", {}) + _attune_header_l = UiKit.label("", UiKit.FS_HINT, true, UiKit.INK) + _workshop_holder.add_child(_attune_header_l) + _attune_nodes.append(_attune_header_l) + for row in att.get("items", []): + var line := _attune_item_line(row) + _workshop_holder.add_child(line) + _attune_nodes.append(line) + _refresh_attune_tail() + + +## 同调件行板构建域:件名+形态+档位+装配切换钮(未解锁禁装——toggle 写口同守卫) +func _attune_item_line(row: Dictionary) -> HBoxContainer: + var iid := String(row.get("item_id", "")) + var line := HBoxContainer.new() + line.add_theme_constant_override("separation", 6) + var name_l := UiKit.label(String(row.get("display_name", "")), UiKit.FS_HINT, true, UiKit.INK) + name_l.custom_minimum_size = Vector2(96, 0) + name_l.clip_text = true + name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + line.add_child(name_l) + var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能", UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) + form_l.custom_minimum_size = Vector2(58, 0) + line.add_child(form_l) + # 档位标签(槽位档位显示:件档=普通/优秀/稀有,装入须件档≤格档契约 2 的呈现半边) + var tier_l := UiKit.label(String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "—")), UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) + tier_l.custom_minimum_size = Vector2(36, 0) + line.add_child(tier_l) + var btn := UiKit.button("装配", 0.0) # 行板紧凑钮豁免 + btn.pressed.connect(attune_toggled.emit.bind(iid)) + btn.disabled = not bool(row.get("unlocked", false)) # 未解锁禁装(解锁为装配前置——toggle 写口同守卫) + line.add_child(btn) + _attune_btns.append({"item_id": iid, "btn": btn, "unlocked": bool(row.get("unlocked", false)), "equipped": bool(row.get("equipped", false))}) + return line + + +## Y 升级尾行构建域(双入口之一——树上 ks_refresh 同账) +func _refresh_attune_tail() -> void: + var refresh_line := HBoxContainer.new() + refresh_line.add_theme_constant_override("separation", 6) + _refresh_l = UiKit.label("", UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) + refresh_line.add_child(_refresh_l) + _refresh_btn = UiKit.button("升级", 0.0) # 行板紧凑钮豁免 + _refresh_btn.pressed.connect(_emit_buy_requested.bind("attune_refresh")) + refresh_line.add_child(_refresh_btn) + _workshop_holder.add_child(refresh_line) + _attune_nodes.append(refresh_line) + + +## 秘宝匣行板刷新域:进度列(解锁混合制三形态)与状态色(亮板双色墨制)与购买钮余额联动 +func _refresh_workshop_rows(data: Dictionary, balance: int) -> void: + var ws_by_id := {} + for row in data.get("workshop", []): + ws_by_id[String(row.get("item_id", ""))] = row + for card in _workshop_rows: + var row: Dictionary[Variant, Variant] = ws_by_id.get(String(card.item_id), {}) + if row.is_empty(): + continue + var unlocked := bool(row.get("unlocked", false)) + card.progress_l.text = _workshop_progress_text(row, unlocked) + var st_text := "未解锁" + var st_col: Color = UiKit.STONE_DEEP + if unlocked: + st_text = "已解锁" + st_col = UiKit.GOLD_INK # GOLD_DEEP 对行板 3.00:1 越带,换深金墨 5.84:1 + card.status_l.text = st_text + card.status_l.add_theme_color_override("font_color", st_col) + card.status_l.tooltip_text = st_text # 截断兜底(状态列 EXPAND_FILL 吸余宽——悬停看全文) + # 购买钮余额联动(禁用态=余额不足,常态防线;pattern 1 兜底拒绝可见归 presenter) + if card.get("buy_btn") != null: + card.buy_btn.disabled = balance < int(card.get("price", 0)) + + +## 进度列文案域(解锁混合制三形态:钥石购买价/条件进度拼接/已入手) +func _workshop_progress_text(row: Dictionary, unlocked: bool) -> String: + if unlocked: + return "已入手" + var mode := String(row.get("mode", "")) + if mode == "keystone": + return "钥石 %d" % int(row.get("price", 0)) + if mode == "condition": + var parts: Array[String] = [] + for req_v in (row.get("reqs", []) as Array): + var req: Dictionary[Variant, Variant] = req_v as Dictionary + var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件")) + parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))]) + return "、".join(parts) + return "" + + +## 同调装配段刷新域(Slots 点亮/满位禁选/Y 升级行余额联动——就地刷) +func _refresh_attune_segment(data: Dictionary, balance: int) -> void: + var att: Dictionary[Variant, Variant] = data.get("attune", {}) + if _attune_header_l != null: + var slots_n := int(att.get("slots", 0)) + var eq: Array[Variant] = att.get("equipped", []) + _attune_header_l.text = "── 同调装配 Slots %d/%d ──" % [eq.size(), slots_n] + var eq_set := {} + for v in eq: + eq_set[String(v)] = true + for card2 in _attune_btns: + var on: bool = eq_set.has(String(card2.item_id)) + card2.btn.text = "卸下" if on else "装配" + # 满位禁选+未解锁禁装(已装件恒可卸下;unlocked 快照随 rebuild 刷新) + card2.btn.disabled = (not on and eq.size() >= slots_n) or (not on and not bool(card2.get("unlocked", false))) + if _refresh_l != null: + var rf: Dictionary[Variant, Variant] = att.get("refresh", {}) + _refresh_l.text = "三选一刷新 +%d 次/冒险 · Lv%d" % [int(rf.get("charges", 0)), int(rf.get("level", 0))] + _refresh_btn.text = "已满级" if bool(rf.get("maxed", false)) else "升级 %d 钥石" % int(rf.get("price", 0)) + _refresh_btn.disabled = bool(rf.get("maxed", false)) or balance < int(rf.get("price", 0)) + + +func _emit_buy_requested(attr: String) -> void: + buy_requested.emit(attr) diff --git a/game/presentation/views/treasure_view.gd.uid b/game/presentation/views/treasure_view.gd.uid new file mode 100644 index 00000000..b0dc8118 --- /dev/null +++ b/game/presentation/views/treasure_view.gd.uid @@ -0,0 +1 @@ +uid://dcakccwlacegq diff --git a/game/presentation/views/tree_view.gd b/game/presentation/views/tree_view.gd new file mode 100644 index 00000000..c9dfc9ba --- /dev/null +++ b/game/presentation/views/tree_view.gd @@ -0,0 +1,105 @@ +## 天赋树子界面(自 CampOverlay 树淬炼柜台抽出——承载面迁移,界面逻辑复用零重设计): +## 树脊柱界面形态=主界面(脉分组行板:节点名/等级/价/购买钮+Fork 双态切换钮段)。 +## UI 只发意图信号(写口唯一归 MetaProgression.tree_buy/tree_buy_fork/tree_toggle_fork)。 +class_name TreeView +extends CampSubViewBase + +signal tree_buy_requested(node_id: String) ## 天赋树购买意图(写口唯一归 MetaProgression.tree_buy) +signal tree_fork_toggled(group: String, side: String) ## 双态切换意图(一节点双态——零币切态) + +var _tree_holder: VBoxContainer # 树淬炼内容(脊柱两行+钥匙/socket 行板+Fork 双态钮段) +var _tree_buy_btns: Array[Dictionary] = [] # [{node_id, btn}]——树购买/解锁钮登记面(open/refresh 重建时刷新;探针消费) +var _tree_fork_btns: Array[Dictionary] = [] # [{group, side, btn}]——Fork 双态切换钮登记面(探针消费) + + +func _init() -> void: + _tree_holder = VBoxContainer.new() + _tree_holder.add_theme_constant_override("separation", 6) + var tree_scroll := ScrollContainer.new() + tree_scroll.custom_minimum_size = Vector2(300, 336) + tree_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + tree_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + tree_scroll.add_child(_tree_holder) + var content := VBoxContainer.new() + content.add_child(tree_scroll) + _build_shell("天赋树 · 淬炼", "", content) + + +func refresh_camp(data: Dictionary) -> void: + _balance_label.text = "钥石 %d" % int(data.get("balance", 0)) + _rebuild_tree(data) + + +# 天赋树段重建(脉分组行板:节点名/等级/价/购买钮 + Fork 双态切换钮; +# 树数据 = 组合根 _camp_data().tree 投影 [{node_id, display_name, vein, kind, level, price, can_buy, +# currency}...] + fork_groups [{group, node_id, active_side, owned, can_buy, price, sides:[{side, display_name, effects}]}]) +func _rebuild_tree(data: Dictionary) -> void: + for c in _tree_holder.get_children(): + c.queue_free() + _tree_buy_btns.clear() + _tree_fork_btns.clear() + var tree: Array[Variant] = data.get("tree", []) as Array + var forks: Array[Variant] = data.get("fork_groups", []) as Array + var vein_names := {"spine": "·脊柱淬炼·", "forge": "·锻造脉·", "keystone": "·钥石脉·", "attune": "·同调脉·"} + var last_vein := "" + for row in tree: + var vein := String(row.get("vein", "")) + if vein != last_vein: + last_vein = vein + var vh := UiKit.label(String(vein_names.get(vein, vein)), UiKit.FS_HINT, true, UiKit.GOLD_INK) # GOLD_DEEP 对柜台板 2.67:1 越带,换深金墨 5.21:1 + _tree_holder.add_child(vh) + if String(row.get("kind", "")) == "fork": + continue # Fork 行去行板化——由 fork_groups 段承载双态切换钮 + _tree_holder.add_child(_tree_row_line(row)) + for fg in forks: + _tree_holder.add_child(_tree_fork_line(fg)) + + +## 树行板构建域:节点名(含门槛标记)+购买钮(追加序与锚句随段整体搬迁) +func _tree_row_line(row: Dictionary) -> HBoxContainer: + var hb := HBoxContainer.new() + hb.add_theme_constant_override("separation", 6) + var lvl := int(row.get("level", 0)) + var cap := int(row.get("cap", 1)) + var name_l := UiKit.label("%s Lv%d/%d" % [String(row.get("display_name", "")), lvl, cap], 13, false, UiKit.INK_LABEL) + name_l.custom_minimum_size = Vector2(150, 0) + name_l.clip_text = true + var gate := String(row.get("gate_text", "")) + if gate != "": + name_l.text += " 🔒" # 等级阈值前置未达标标记(R1 机制位——悬停看门槛;达标后随 refresh 消失) + name_l.tooltip_text = "等级门槛:%s" % gate + hb.add_child(name_l) + var btn := UiKit.button("购买 %d 钥石" % int(row.get("price", 0)), 0.0) # 柜台行板紧凑钮豁免宽度基准 + btn.pressed.connect(func(): tree_buy_requested.emit(String(row.get("node_id", "")))) + btn.disabled = not bool(row.get("can_buy", false)) + hb.add_child(btn) + _tree_buy_btns.append({"node_id": String(row.get("node_id", "")), "btn": btn}) + return hb + + +## Fork 双态行板构建域:组状态文案+解锁钮或双态切换钮 +func _tree_fork_line(fg: Dictionary) -> HBoxContainer: + var group := String(fg.get("group", "")) + var active := String(fg.get("active_side", "")) + var owned := bool(fg.get("owned", false)) + var fb := HBoxContainer.new() + fb.add_theme_constant_override("separation", 6) + var state_txt := "未解锁" + if owned: + state_txt = "激活:%s" % active + var gl := UiKit.label("%s[%s]" % [String(fg.get("display_name", group)), state_txt], 13, false, UiKit.INK_LABEL) + fb.add_child(gl) + if not owned: + var bb := UiKit.button("解锁 %d 钥石" % int(fg.get("price", 0)), 0.0) # 行板紧凑钮豁免 + bb.pressed.connect(func(): tree_buy_requested.emit(String(fg.get("node_id", "")))) + bb.disabled = not bool(fg.get("can_buy", false)) + fb.add_child(bb) + _tree_buy_btns.append({"node_id": String(fg.get("node_id", "")), "btn": bb}) + else: + for side_row in (fg.get("sides", []) as Array): + var sd := String(side_row.get("side", "")) + var sb := UiKit.button(String(side_row.get("display_name", sd)), 0.0) # 行板紧凑钮豁免 + sb.pressed.connect(func(): tree_fork_toggled.emit(group, sd)) + fb.add_child(sb) + _tree_fork_btns.append({"group": group, "side": sd, "btn": sb}) + return fb diff --git a/game/presentation/views/tree_view.gd.uid b/game/presentation/views/tree_view.gd.uid new file mode 100644 index 00000000..825363bc --- /dev/null +++ b/game/presentation/views/tree_view.gd.uid @@ -0,0 +1 @@ +uid://mjjrdelhi7yg diff --git a/game/tests/system_test.gd b/game/tests/system_test.gd index 3cfe4ee4..439c7455 100644 --- a/game/tests/system_test.gd +++ b/game/tests/system_test.gd @@ -64,8 +64,8 @@ func _smoke_scene_split() -> void: var camp: Node = load("res://presentation/scenes/camp_screen.tscn").instantiate() root.add_child(camp) await process_frame - check(camp.camp_overlay != null, "V2 营地屏壳独立实例化(camp_overlay 在位)") - check(not camp.camp_overlay.visible, "V2 营地层默认隐藏(模态纪律)") + check(camp.camp_scene != null, "V2 营地屏壳独立实例化(camp_scene 在位)") + check(not camp.camp_scene.visible, "V2 营地层默认隐藏(模态纪律)") camp.queue_free() var adv: Node = load("res://presentation/scenes/adventure_screen.tscn").instantiate() root.add_child(adv) @@ -105,7 +105,7 @@ func _smoke_scene_split() -> void: chain_prev = node check(chain_ok, "V1c 合同树序链逐对保序(运行期树序==LAYER_CONTRACT 序;违例:%s)〔装配生命批 V1〕〔句柄子容器批 V2〕" % chain_detail) # ③ 探针挂载假设句柄在座(main.X 一级路径——探针迁移面收口证明) - check(main.ui.screens.start_overlay == main.ui.screens.menu_screen.start_overlay and main.ui.modals.camp.overlay == main.ui.screens.camp_screen.camp_overlay and main.ui.modals.level_screen == main.ui.screens.adventure_screen.level_screen and main.ui.screens.battle_root == main.ui.screens.adventure_screen.battle_root and main.ui.battle.board_zone.board_view == main.ui.screens.adventure_screen.board_view, + check(main.ui.screens.start_overlay == main.ui.screens.menu_screen.start_overlay and main.ui.modals.camp.scene == main.ui.screens.camp_screen.camp_scene and main.ui.modals.level_screen == main.ui.screens.adventure_screen.level_screen and main.ui.screens.battle_root == main.ui.screens.adventure_screen.battle_root and main.ui.battle.board_zone.board_view == main.ui.screens.adventure_screen.board_view, "V2 组合根句柄指向屏壳成员(探针挂载假设 main.X 零迁移)〔句柄子容器批 V1〕") check(main.run_scope != null and not main.run_scope.is_adventure, "V2 闲时会话自 _ready 即在(永非 null;迭代 27 T3——gp 影子账退役无账本成员)") @@ -118,7 +118,7 @@ func _smoke_scene_split() -> void: func _topmost_visible_layer(main: Node) -> Node: var candidates: Array[Node] = [main.ui.screens.start_overlay, main.ui.screens.battle_root, main.ui.screens.inter_overlay, main.ui.modals.legend_overlay, main.ui.modals.node_overlay, main.ui.modals.level_screen, main.ui.modals.curio_overlay, - main.ui.modals.end_overlay, main.ui.modals.shop_overlay, main.ui.modals.camp.overlay, main.ui.modals.settings_overlay, + main.ui.modals.end_overlay, main.ui.modals.shop_overlay, main.ui.modals.camp.scene, main.ui.modals.settings_overlay, main.ui.modals.status_panel, main.ui.modals.career_overlay, main.ui.modals.confirm_dialog, main.ui.modals.debug_overlay] var best: Node = null var best_x := -1 diff --git a/game/tests/unit/presentation/forge_workbench_test.gd b/game/tests/unit/presentation/forge_workbench_test.gd index cf7a9b62..fc05d97b 100644 --- a/game/tests/unit/presentation/forge_workbench_test.gd +++ b/game/tests/unit/presentation/forge_workbench_test.gd @@ -15,7 +15,7 @@ func _initialize() -> void: func _row_by_id(ov) -> Dictionary: var out := {} - for r in ov._forge._forge_rows: + for r in ov._forge_rows: out[String(r.route_id)] = r return out @@ -30,7 +30,7 @@ func _run() -> void: var sv = load("res://core/save_manager.gd").new(_TEST_SAVE) sv.load_from_disk() var mp = load("res://core/meta_progression.gd").new(db, sv) - var ov = load("res://presentation/views/camp_overlay_view.gd").new() + var ov = load("res://presentation/views/forge_view.gd").new() root.add_child(ov) var buys := [] var unlocks := [] @@ -42,16 +42,16 @@ func _run() -> void: # ① 锁定态:树钥匙未点亮 → 工作间不可见、两区零行板(无任何可交互购买路径) ov.open_camp({"balance": 999, "forge": mp.forge_shop_status(), "tree": []}) Kit.check("锁定态:工作间不可见(forge_workshop 钥匙未点亮)〔锻造工坊批 V13〕", - not ov._forge._forge_workbench.visible and ov._forge._forge_lock_l.visible) + not ov._forge_workbench.visible and ov._forge_lock_l.visible) Kit.check("锁定态:两区零行板(占位纪律——零购买路径)〔锻造工坊批 V13〕", - ov._forge._forge_zone_weapon.get_child_count() == 0 and ov._forge._forge_zone_armor.get_child_count() == 0) + ov._forge_zone_weapon.get_child_count() == 0 and ov._forge_zone_armor.get_child_count() == 0) # ② 解锁态:钥匙点亮 → 两区各三路线 ov.open_camp({"balance": 500, "forge": mp.forge_shop_status(), "tree": [{"node_id": "forge_workshop", "level": 1}]}) - Kit.check("解锁态:工作间可见(钥匙点亮展开)〔锻造工坊批 V13〕", ov._forge._forge_workbench.visible) + Kit.check("解锁态:工作间可见(钥匙点亮展开)〔锻造工坊批 V13〕", ov._forge_workbench.visible) Kit.check("两区各三路线(武器锻造区 3 + 护甲锻造区 3)〔锻造工坊批 V13〕", - ov._forge._forge_zone_weapon.get_child_count() == 3 and ov._forge._forge_zone_armor.get_child_count() == 3) + ov._forge_zone_weapon.get_child_count() == 3 and ov._forge_zone_armor.get_child_count() == 3) var rows: Dictionary = _row_by_id(ov) Kit.check("六路线行册恰全(sword/spear/staff/light/heavy/ward)〔锻造工坊批 V13〕", rows.has("sword") and rows.has("spear") and rows.has("staff") diff --git a/game/tests/unit/presentation/theme_projection_test.gd b/game/tests/unit/presentation/theme_projection_test.gd index 7c8a8d42..eddc7162 100644 --- a/game/tests/unit/presentation/theme_projection_test.gd +++ b/game/tests/unit/presentation/theme_projection_test.gd @@ -52,7 +52,6 @@ func _init() -> void: _smoke_diag_button_retired() _status_panel_projection() _career_overlay_projection() - _camp_recent_projection() quit(Kit.finish("theme_projection_test")) @@ -708,12 +707,6 @@ func _smoke_element_details() -> void: var shop_src: String = FileAccess.get_file_as_string("res://presentation/views/shop_overlay_view.gd") Kit.check("T5③ 商店卡片区垂直居中(ALIGNMENT_CENTER)", shop_src.contains("_cards_box.alignment = BoxContainer.ALIGNMENT_CENTER")) - var camp_src: String = FileAccess.get_file_as_string("res://presentation/views/camp_overlay_view.gd") - var camp_min := camp_src.count("custom_minimum_size = Vector2(0, 336)") - Kit.check("T5③ 营地三 holder 统一 min 高 336(实测 %d 处)" % camp_min, camp_min == 3) - var camp_fill := camp_src.count("size_flags_vertical = Control.SIZE_FILL") - Kit.check("T5③ 营地四栏面板纵向 FILL 等高(实测 %d 处)" % camp_fill, camp_fill == 4) - Kit.check("T5③ 三处锁文垂直居中在位", camp_src.count("SIZE_SHRINK_CENTER") >= 3) # ---- 诊断转储钮冒烟(角落钮不在玩家画面+转储入口在调试浮窗;V6)---- @@ -796,20 +789,6 @@ func _all_labels(root: Node) -> Array: return out -# ---- 营地最近冒险行投影(口径换代 V6:完成关数措辞钉子——open_camp 全量重建路径直调; -# 最小档 data=recent 单条 completed 局,其余键缺省走 .get 兜底——与组合根 _camp_data 投影同键面)---- -func _camp_recent_projection() -> void: - print("--- 营地·最近冒险行投影(open_camp 最小档)---") - var camp = load("res://presentation/views/camp_overlay_view.gd").new() - camp.open_camp({"balance": 0, "recent": [ - {"result": "completed", "levels_cleared": 2, "keystone_total": 5, "master_seed": 42}]}) - var recent_text: String = String(camp._side._recent_list.text) - Kit.check("营地最近冒险行完成关数措辞(实际「%s」)〔口径换代 V6〕" % recent_text, - recent_text.contains("完成 2 关") and not recent_text.contains("通关")) - camp.visible = false - camp.free() - - # ---- 生涯页只读投影一致性(汇总段六类九项逐行 == SaveManager 计数访问器/ # MetaProgression 访问器读数;投影拼装与 HudPresenter.career_data 同构(同访问器族同序)。 # 只读性连接巡检与零动效断言归 ui_struct_probe 呈现侧,本段只管数据面+旧档载入显示