diff --git a/game/core/config_db.gd b/game/core/config_db.gd index 4a6d4f8d..de14a9b3 100644 --- a/game/core/config_db.gd +++ b/game/core/config_db.gd @@ -30,12 +30,13 @@ const TABLE_MANIFEST := { "feats": "feats.tres", # FeatTable 规则改写型特性(定义表 Rule 4 豁免——TreasureItemTable 先例;空表合法态=主时间线零扰动) "forge": "forge.tres", # ForgeTable——两根类型轴默认值+六路线行族;攻防所有权切分唯一数据面 "battle_layout": "battle_layout.tres", # BattleLayoutTable——战斗屏分区几何与明细带高度单行 + "camp_scene_layout": "camp_scene_layout.tres", # CampSceneLayoutTable——营地场景 10 层贴图/命中矩形与门洞缺口单行 } var combat: ConfigDomainCombat # 战斗编排域七表(board_gen/variation/status_effects/enemy/skill/hero/settlement) var run: ConfigDomainRun # 冒险 runner 域七表(level/difficulty/level_gen_params/reward/adventure/action_economy/shop) var crossrun: ConfigDomainCrossrun # 跨局元进度域五表(meta/meta_tree/treasure_items/feats/forge) -var presentation: ConfigDomainPresentation # 表现层布局域一表(battle_layout) +var presentation: ConfigDomainPresentation # 表现层布局域两表(battle_layout/camp_scene_layout) var is_ready := false var _diagnostics: Array[Dictionary] = [] # {severity, category, message} @@ -149,6 +150,9 @@ func _assign_loaded(loaded: Dictionary) -> void: crossrun.feats = loaded.get("feats") crossrun.forge = loaded.get("forge") presentation.battle_layout = loaded.get("battle_layout") + presentation.camp_scene_layout = loaded.get("camp_scene_layout") + for problem: String in presentation.camp_scene_layout.validate(): + _fatal("camp_scene_layout", problem) # 布局几何红线(命中区越画面/相交/侵顶栏)=数据致命 # resolver 协作件注入三表(resolve 委托承载——resolver 文件与键零搬移) combat.board_gen._resolver = _resolver combat.enemy._resolver = _resolver diff --git a/game/core/config_domain_presentation.gd b/game/core/config_domain_presentation.gd index b3cf13b3..3846979d 100644 --- a/game/core/config_domain_presentation.gd +++ b/game/core/config_domain_presentation.gd @@ -1,6 +1,7 @@ -## 表现层布局域容器(D2 域容器——config_db 表持有按域收敛):战斗屏布局一表类型化持有。 +## 表现层布局域容器(D2 域容器——config_db 表持有按域收敛):战斗屏与营地场景布局两表类型化持有。 ## 零方法零行为——纯持有结构;「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88 定稿注记)。 class_name ConfigDomainPresentation extends Resource var battle_layout: BattleLayoutTable +var camp_scene_layout: CampSceneLayoutTable diff --git a/game/data/schemas/camp_scene_layer_spec.gd b/game/data/schemas/camp_scene_layer_spec.gd new file mode 100644 index 00000000..152b8621 --- /dev/null +++ b/game/data/schemas/camp_scene_layer_spec.gd @@ -0,0 +1,13 @@ +## 营地场景单层规格(camp_scene_layout.tres 行件):层键+贴图矩形+命中矩形+命中键。 +## z 序=表内数组序(视图子节点序同源派生);坐标一律终稿画布 1280×800 像素坐标。 +class_name CampSceneLayerSpec +extends Resource + +@export var key: StringName = &"" # 层键(对齐 UiKit.TEX_CAMP_* 常量后缀) +@export var texture_rect: Rect2 = Rect2() # 贴图矩形(1:1 像素贴图落位) +@export var hotspot: Rect2 = Rect2() # 命中矩形(零矩形=不可点层——bg/wall/tent) +@export var hotspot_key: StringName = &"" # object_pressed 载荷键(可点层必填) + + +func is_clickable() -> bool: + return hotspot.size.x > 0.0 and hotspot.size.y > 0.0 diff --git a/game/data/schemas/camp_scene_layer_spec.gd.uid b/game/data/schemas/camp_scene_layer_spec.gd.uid new file mode 100644 index 00000000..2234c8c1 --- /dev/null +++ b/game/data/schemas/camp_scene_layer_spec.gd.uid @@ -0,0 +1 @@ +uid://bll8d0s1nmarf diff --git a/game/data/schemas/camp_scene_layout_table.gd b/game/data/schemas/camp_scene_layout_table.gd new file mode 100644 index 00000000..2137967d --- /dev/null +++ b/game/data/schemas/camp_scene_layout_table.gd @@ -0,0 +1,67 @@ +## 营地场景布局表 schema(camp_scene_layout.tres):10 层贴图矩形/命中矩形/门洞缺口/围墙带包络/顶栏禁区。 +## 坐标数值唯一事实源=数据行本表——视图、探针、单测一律读表取值,禁另写坐标字面量。 +class_name CampSceneLayoutTable +extends Resource + +@export var canvas := Vector2(1280, 800) # 终稿画布(工程档同幅面) +@export var hud_band_h := 48 # 顶栏禁区高度(命中不侵) +@export var gate_gap := Vector2i(520, 760) # 门洞缺口 x 范围(透出背景层小路,禁填死) +@export var wall_band := Rect2(0, 240, 1280, 70) # 围墙带包络(门柱顶 240—桩底 310;围墙贴图须落在其内) +@export var layers: Array[CampSceneLayerSpec] = [] + + +## 加载期 fail-fast 校验(ConfigDB 装载即查,问题非空=数据致命)。 +## 校验面按件拆分:层身份与贴图矩形/命中区边界/命中区互斥/围墙带包络。 +func validate() -> Array[String]: + var problems: Array[String] = [] + var clickables: Array[CampSceneLayerSpec] = _collect_clickables(problems) + _check_hotspot_bounds(clickables, problems) + _check_hotspot_disjoint(clickables, problems) + _check_wall_band(problems) + return problems + + +func _collect_clickables(problems: Array[String]) -> Array[CampSceneLayerSpec]: + var seen: Dictionary[StringName, bool] = {} + var clickables: Array[CampSceneLayerSpec] = [] + for layer in layers: + if layer.key == &"": + problems.append("层键为空") + continue + if seen.has(layer.key): + problems.append("层键重复: %s" % layer.key) + seen[layer.key] = true + if _out_of_canvas(layer.texture_rect): + problems.append("贴图矩形越画面: %s %s" % [layer.key, layer.texture_rect]) + if layer.is_clickable(): + if layer.hotspot_key == &"": + problems.append("可点层缺命中键: %s" % layer.key) + clickables.append(layer) + elif layer.hotspot_key != &"": + problems.append("不可点层带命中键: %s" % layer.key) + return clickables + + +func _check_hotspot_bounds(clickables: Array[CampSceneLayerSpec], problems: Array[String]) -> void: + for h in clickables: + if h.hotspot.position.x < 0.0 or h.hotspot.position.y < hud_band_h \ + or _out_of_canvas(h.hotspot): + problems.append("命中区越画面或侵顶栏禁区: %s %s" % [h.key, h.hotspot]) + + +func _check_hotspot_disjoint(clickables: Array[CampSceneLayerSpec], problems: Array[String]) -> void: + for i in clickables.size(): + for j in range(i + 1, clickables.size()): + if clickables[i].hotspot.intersects(clickables[j].hotspot): + problems.append("命中区相交: %s × %s" % [clickables[i].key, clickables[j].key]) + + +func _check_wall_band(problems: Array[String]) -> void: + for layer in layers: + if layer.key == &"wall" and not wall_band.encloses(layer.texture_rect): + problems.append("围墙贴图越围墙带包络: %s" % layer.texture_rect) + + +func _out_of_canvas(rect: Rect2) -> bool: + return rect.position.x < 0.0 or rect.position.y < 0.0 \ + or rect.end.x > canvas.x or rect.end.y > canvas.y diff --git a/game/data/schemas/camp_scene_layout_table.gd.uid b/game/data/schemas/camp_scene_layout_table.gd.uid new file mode 100644 index 00000000..6740c88f --- /dev/null +++ b/game/data/schemas/camp_scene_layout_table.gd.uid @@ -0,0 +1 @@ +uid://c6p8wyvcc5upp diff --git a/game/data/tables/camp_scene_layout.tres b/game/data/tables/camp_scene_layout.tres new file mode 100644 index 00000000..c811e355 --- /dev/null +++ b/game/data/tables/camp_scene_layout.tres @@ -0,0 +1,82 @@ +[gd_resource type="Resource" script_class="CampSceneLayoutTable" load_steps=13 format=3] + +[ext_resource type="Script" path="res://data/schemas/camp_scene_layout_table.gd" id="1"] +[ext_resource type="Script" path="res://data/schemas/camp_scene_layer_spec.gd" id="2"] + +[sub_resource type="Resource" id="1"] +script = ExtResource("2") +key = &"bg" +texture_rect = Rect2(0, 0, 1280, 800) +hotspot = Rect2(0, 0, 0, 0) +hotspot_key = &"" + +[sub_resource type="Resource" id="2"] +script = ExtResource("2") +key = &"wall" +texture_rect = Rect2(0, 261, 1280, 49) +hotspot = Rect2(0, 0, 0, 0) +hotspot_key = &"" + +[sub_resource type="Resource" id="3"] +script = ExtResource("2") +key = &"board" +texture_rect = Rect2(845, 268, 130, 121) +hotspot = Rect2(845, 268, 130, 140) +hotspot_key = &"board" + +[sub_resource type="Resource" id="4"] +script = ExtResource("2") +key = &"gate" +texture_rect = Rect2(520, 250, 240, 211) +hotspot = Rect2(520, 250, 240, 220) +hotspot_key = &"gate" + +[sub_resource type="Resource" id="5"] +script = ExtResource("2") +key = &"dummy" +texture_rect = Rect2(340, 520, 120, 180) +hotspot = Rect2(340, 520, 120, 180) +hotspot_key = &"dummy" + +[sub_resource type="Resource" id="6"] +script = ExtResource("2") +key = &"forge" +texture_rect = Rect2(40, 467, 240, 193) +hotspot = Rect2(40, 420, 240, 240) +hotspot_key = &"forge" + +[sub_resource type="Resource" id="7"] +script = ExtResource("2") +key = &"tent" +texture_rect = Rect2(982, 520, 251, 170) +hotspot = Rect2(0, 0, 0, 0) +hotspot_key = &"" + +[sub_resource type="Resource" id="8"] +script = ExtResource("2") +key = &"chest" +texture_rect = Rect2(1097, 700, 106, 90) +hotspot = Rect2(1085, 700, 130, 90) +hotspot_key = &"chest" + +[sub_resource type="Resource" id="9"] +script = ExtResource("2") +key = &"notebook" +texture_rect = Rect2(818, 704, 100, 66) +hotspot = Rect2(818, 690, 100, 80) +hotspot_key = &"notebook" + +[sub_resource type="Resource" id="10"] +script = ExtResource("2") +key = &"campfire" +texture_rect = Rect2(560, 571, 160, 149) +hotspot = Rect2(560, 640, 160, 80) +hotspot_key = &"campfire" + +[resource] +script = ExtResource("1") +canvas = Vector2(1280, 800) +hud_band_h = 48 +gate_gap = Vector2i(520, 760) +wall_band = Rect2(0, 240, 1280, 70) +layers = [SubResource("1"), SubResource("2"), SubResource("3"), SubResource("4"), SubResource("5"), SubResource("6"), SubResource("7"), SubResource("8"), SubResource("9"), SubResource("10")] 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..8d10c7ae 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -123,19 +123,37 @@ func _start_adventure_from_menu() -> void: # 确认弹窗分派(卡③ 契约 3):按 _confirm_consumer 路由——"delete_slot"=卡③ 删档确认 -#(确认才删、取消只关窗——裁决 3)。"new_adventure" 消费方已随卡② 主菜单确认链摘除退役 -#(mn6/V5——主菜单零确认残留),分派机制保留(卡④ 到位接入第三消费方不返工)。 +#(确认才删、取消只关窗——裁决 3)。"gate"/"gate_new"=卡④ 大门两分支消费方(有/无断点), +# "campfire_leave"=篝火二次确认(卡④ T4 接入——分派机制扩展第三/第四消费方)。 func _on_confirm_accepted() -> void: ui.modals.flow.close(ui.modals.confirm_dialog) # 模态栈单口 match _confirm_consumer: "delete_slot": save_manager.slots.delete_slot(_confirm_consumer_slot) ui.modals.save_select_overlay.open_save_select(HudPresenter.save_select_data(save_manager)) # 就地刷新(该槽回空态) + "gate": + _camp_pres.on_camp_closed() # 冒险发起后营地场景屏隐藏(选档层同收——卡④ T4 关层收闭) + _resume_from_menu() # 继续上一段冒险(现役续玩编排同源——卡④ 契约 4) + "gate_new": + _camp_pres.on_camp_closed() + _start_adventure() # 开始新冒险(现役开局编排同源) + "campfire_leave": + _camp_pres.on_camp_closed() # 篝火确认离场=营地与选档层同收+开始层恢复(回主菜单) + _confirm_consumer = "" + + +# 次确认分派(三钮形态中钮——大门「开始新冒险」出口;卡④ T4) +func _on_confirm_secondary() -> void: + ui.modals.flow.close(ui.modals.confirm_dialog) # 模态栈单口 + match _confirm_consumer: + "gate": + _camp_pres.on_camp_closed() + _start_adventure() _confirm_consumer = "" func _on_confirm_declined() -> void: - ui.modals.flow.close(ui.modals.confirm_dialog) # 只关窗(取消无状态变更——裁决 3:取消不删);关侧恢复归 ModalFlow + ui.modals.flow.close(ui.modals.confirm_dialog) # 只关窗(取消无状态变更——裁决 3:取消不删/卡④:退回营地);关侧恢复归 ModalFlow _confirm_consumer = "" @@ -500,12 +518,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..bf8d5a4d 100644 --- a/game/presentation/main_overlay_flow.gd +++ b/game/presentation/main_overlay_flow.gd @@ -40,7 +40,43 @@ 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/T4——场景根 object_pressed 组合根分派)---- +# 大门两分支(裁决 5,Q5 甲):点大门永远先出确认不直接进冒险——有断点=三出口弹窗 +#(继续上一段冒险/开始新冒险/取消),无断点=是否开始新冒险(是/否);cancelled 恒=无变更退回营地。 +# 篝火回主菜单必须二次确认(camp.md v2.2 裁定);确认后营地层与选档层同收+开始层恢复(关层收闭)。 + + +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) + &"gate": + _open_gate_confirm(root) + &"campfire": + root._confirm_consumer = "campfire_leave" + root.ui.modals.confirm_dialog.open("确定离开营地回到主菜单?", "离开", "留下") + + +static func _open_gate_confirm(root: Variant) -> void: + if root.save_manager.has_breakpoint(): + root._confirm_consumer = "gate" + root.ui.modals.confirm_dialog.open_three("有未完成的冒险——继续上一段冒险,还是开始新冒险?", + "继续上一段冒险", "开始新冒险", "取消") + return + root._confirm_consumer = "gate_new" + root.ui.modals.confirm_dialog.open("是否开始新冒险?", "是", "否") # 生涯页快照投影 —— 承接体见 HudPresenter.career_data(开层调用点直接改引) @@ -65,7 +101,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 +109,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..15172e8d 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) # 退出游戏提级一级菜单 @@ -194,6 +224,7 @@ static func _build_debug_and_resolvers(root: Variant) -> void: # 消费方编排(卡③ 契约 3/sl8——禁双常驻静态接线):open 前设 _confirm_consumer, # confirmed/cancelled 后按该状态量分派;分派面可容纳后续消费方(卡④ 到位直接接入不返工) root.ui.modals.confirm_dialog.confirmed.connect(root._on_confirm_accepted) + root.ui.modals.confirm_dialog.secondary_confirmed.connect(root._on_confirm_secondary) # 三钮次确认(卡④ 大门「开始新冒险」出口) root.ui.modals.confirm_dialog.cancelled.connect(root._on_confirm_declined) # 调试浮窗(主菜单三散件收一:随机种子/数值调整/批量模拟/两层调试入口; 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_scene_view.gd b/game/presentation/views/camp_scene_view.gd new file mode 100644 index 00000000..eb0f563b --- /dev/null +++ b/game/presentation/views/camp_scene_view.gd @@ -0,0 +1,307 @@ +## 营地场景图视图(交互式营地):10 层贴图铺底+七物件命中区+点选反馈四态+顶栏预留位。 +## 布局唯一事实源 = db.presentation.camp_scene_layout(构造注入,本视图禁自载表路径); +## 子节点序 = 表内数组序 = z 序(Control 根+TextureRect 层——禁 CanvasLayer/z_index/Sprite2D)。 +## 输入单点:根 _gui_input 集中命中判定(层节点全 IGNORE),点击只发 object_pressed 信号,路由归组合根。 +## 零核心写调用:视图不读 MetaProgression,禁用态由组合根经 set_feedback_state 推送。 +class_name CampSceneView +extends Control + +signal object_pressed(key: StringName) ## 命中区点击确认(press+release 同区且非禁用;组合根路由开界面/动线) + +## 名牌文案(camp.md §场景构成绑定表——七可点件按界面名逐字) +const _HOTSPOT_NAMES := { + &"gate": "冒险", &"forge": "锻造工坊", &"chest": "秘宝匣", &"dummy": "天赋树", + &"board": "奖励格图鉴", &"notebook": "生涯记录", &"campfire": "离开营地", +} +## 层键→纹理路径(单点 = UiKit.TEX_CAMP_*,本表只引用不自写路径;缺位 load_tex→null→程序化占位) +const _TEX_BY_KEY := { + &"bg": UiKit.TEX_CAMP_BG, &"wall": UiKit.TEX_CAMP_WALL, &"board": UiKit.TEX_CAMP_BOARD, + &"gate": UiKit.TEX_CAMP_GATE, &"dummy": UiKit.TEX_CAMP_DUMMY, &"forge": UiKit.TEX_CAMP_FORGE, + &"tent": UiKit.TEX_CAMP_TENT, &"chest": UiKit.TEX_CAMP_CHEST, + &"notebook": UiKit.TEX_CAMP_NOTEBOOK, &"campfire": UiKit.TEX_CAMP_CAMPFIRE, +} +## 禁用态去饱和着色器(modulate 只能调明度——降明度+去饱和走 canvas_item shader,零新素材) +const _DESAT_SHADER_SRC := " +shader_type canvas_item; +uniform float strength : hint_range(0.0, 1.0) = 1.0; +void fragment() { + vec4 tex = texture(TEXTURE, UV); + float g = dot(tex.rgb, vec3(0.299, 0.587, 0.114)); + tex.rgb = mix(tex.rgb, vec3(g) * 0.72, strength); + COLOR = tex * COLOR; +} +" + +enum FeedbackState { NORMAL, HOVER, PRESSED, DISABLED } ## 点选反馈四态(camp.md §点选反馈) + +var _layout: CampSceneLayoutTable # 布局表(坐标唯一事实源) +var _layer_nodes: Dictionary[StringName, Control] = {} # 层键→TextureRect/ColorRect 占位(子节点序=z 序) +var _hotspot_rects: Dictionary[StringName, Rect2] = {} # 命中键→Rect2(视图局部坐标=表坐标) +var _hotspot_layer: Dictionary[StringName, StringName] = {} # 命中键→层键 +var _base_states: Dictionary[StringName, int] = {} # 命中键→FeedbackState 值(持久基态——组合根置 DISABLED/NORMAL) +var _outline: Panel # 悬停轮廓亮边 +var _name_label: Label # 物件名牌(下方 8px 居中,画面内钳制) +var _hover_key := &"" # 当前悬停命中键(&""=无) +var _press_key := &"" # 当前按住命中键 +var _float_tween: Tween # 悬停浮动补间(逐级整像素吸附,禁 _process 逐帧移 UI) +var _desat_mat: ShaderMaterial # 禁用态共享材质(strength 恒 1,材质挂/摘承载开关) +var _hud_slot: Control # 顶栏预留位(卡⑤ set_hud_band 填值) + + +func _init() -> void: + visible = false # 登记模态初始隐(开合走栈——模态纪律;boot 态场景根不得盖菜单) + + +func setup_layers(layout: CampSceneLayoutTable) -> void: + if layout == null: + push_error("[CampSceneView] 布局表缺位——组合根须注入 db.presentation.camp_scene_layout") + return + _layout = layout + mouse_filter = Control.MOUSE_FILTER_STOP # 场景图=营地语境主交互面(模态层在兄弟位先截事件) + _build_desat_material() + for spec in layout.layers: + var node: Control = _build_layer_node(spec) + add_child(node) + _layer_nodes[spec.key] = node + _build_outline() + _build_name_label() + _build_hud_slot(layout) + mouse_exited.connect(_on_mouse_exited) # 悬出根控件=清 hover/press(防态悬挂) + + +func bind_hotspots(layout: CampSceneLayoutTable) -> void: + if layout == null: + return + for spec in layout.layers: + if spec.is_clickable(): + _hotspot_rects[spec.hotspot_key] = spec.hotspot + _hotspot_layer[spec.hotspot_key] = spec.key + _base_states[spec.hotspot_key] = FeedbackState.NORMAL + + +func get_hotspot_rect(key: StringName) -> Rect2: + var rect: Rect2 = _hotspot_rects.get(key, Rect2()) + return rect + + +func set_feedback_state(key: StringName, state: FeedbackState) -> void: + if not _base_states.has(key): + return + _base_states[key] = state # 持久基态只收 NORMAL/DISABLED;HOVER/PRESSED 为视图内瞬态 + _apply_state(key, _base_state(key)) + + +func set_hud_band(band: Control) -> void: + if _hud_slot != null: + _hud_slot.add_child(band) + + +func _gui_input(event: InputEvent) -> void: + if event is InputEventMouseMotion: + _handle_motion(event as InputEventMouseMotion) + elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: + _handle_button(event as InputEventMouseButton) + accept_event() + + +func _handle_motion(event: InputEventMouseMotion) -> void: + var key: StringName = _topmost_hotspot_at(event.position) + if key != _hover_key: + if _hover_key != &"": + _apply_state(_hover_key, _base_state(_hover_key)) + _hover_key = key + if key != &"": + _apply_state(key, FeedbackState.HOVER) + if _press_key != &"" and key != _press_key: + _apply_state(_press_key, _base_state(_press_key)) # 按住拖出=取消按压 + _press_key = &"" + + +func _handle_button(event: InputEventMouseButton) -> void: + var hit: StringName = _topmost_hotspot_at(event.position) + if event.pressed: + if hit != &"": + _press_key = hit + _apply_state(hit, FeedbackState.PRESSED) + return + if _press_key == &"": + return + if hit == _press_key and not _is_disabled(_press_key): + object_pressed.emit(_press_key) + _apply_state(_press_key, _base_state(_press_key)) + _press_key = &"" + + +func _build_layer_node(spec: CampSceneLayerSpec) -> Control: + var node: Control = null + var tex: Texture2D = UiKit.load_tex(_TEX_BY_KEY.get(spec.key, "")) + if tex != null: + var tr := TextureRect.new() + tr.texture = tex + tr.stretch_mode = TextureRect.STRETCH_SCALE + node = tr + else: + var ph := ColorRect.new() # 程序化占位(素材缺位=冷检出半同步态,结构在位冒烟安全) + ph.color = Color(1.0, 1.0, 1.0, 0.06) + node = ph + node.name = "Layer_%s" % spec.key + node.position = spec.texture_rect.position + node.size = spec.texture_rect.size + node.mouse_filter = Control.MOUSE_FILTER_IGNORE + return node + + +func _build_desat_material() -> void: + _desat_mat = ShaderMaterial.new() + var shader := Shader.new() + shader.code = _DESAT_SHADER_SRC + _desat_mat.shader = shader + + +func _build_outline() -> void: + _outline = Panel.new() + var sb := StyleBoxFlat.new() + sb.draw_center = false + sb.border_color = UiKit.CANDLE + sb.set_border_width_all(2) + sb.set_corner_radius_all(4) + _outline.add_theme_stylebox_override("panel", sb) + _outline.mouse_filter = Control.MOUSE_FILTER_IGNORE + _outline.visible = false + add_child(_outline) + + +func _build_name_label() -> void: + _name_label = UiKit.label("", UiKit.FS_HINT, false, UiKit.INK_ON_DEEP) + var plate := StyleBoxFlat.new() + plate.bg_color = UiKit.BG_ROOT + plate.set_corner_radius_all(4) + plate.content_margin_left = 8.0 + plate.content_margin_right = 8.0 + plate.content_margin_top = 2.0 + plate.content_margin_bottom = 2.0 + _name_label.add_theme_stylebox_override("normal", plate) + _name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE # 名牌不挡命中区 + _name_label.visible = false + add_child(_name_label) + + +func _build_hud_slot(layout: CampSceneLayoutTable) -> void: + _hud_slot = Control.new() + _hud_slot.name = "HudBandSlot" + _hud_slot.set_anchors_preset(Control.PRESET_TOP_WIDE) + _hud_slot.custom_minimum_size = Vector2(0, layout.hud_band_h) # 48px 浮层覆盖,不挤压画面 + _hud_slot.mouse_filter = Control.MOUSE_FILTER_IGNORE + add_child(_hud_slot) + + +func _topmost_hotspot_at(pos: Vector2) -> StringName: + if _layout == null: + return &"" + var idx: int = _layout.layers.size() - 1 # 逆序=自最上 z 向下 + while idx >= 0: + var spec: CampSceneLayerSpec = _layout.layers[idx] + if spec.is_clickable() and _hotspot_rects.get(spec.hotspot_key, Rect2()).has_point(pos): + return spec.hotspot_key + idx -= 1 + return &"" + + +func _base_state(key: StringName) -> FeedbackState: + var st: int = _base_states.get(key, FeedbackState.NORMAL) + return FeedbackState.DISABLED if st == FeedbackState.DISABLED else FeedbackState.NORMAL + + +func _is_disabled(key: StringName) -> bool: + return _base_state(key) == FeedbackState.DISABLED + + +func _apply_state(key: StringName, state: FeedbackState) -> void: + var layer_key: StringName = _hotspot_layer.get(key, &"") + var node: Control = _layer_nodes.get(layer_key, null) + if node == null: + return + var rect: Rect2 = _hotspot_rects.get(key, Rect2()) + var base_y: float = node.position.y - _float_offset_of(key) # 还原到基准位再施新态 + _set_float_offset(key, 0) + if _float_tween != null: + _float_tween.kill() + _float_tween = null + match state: + FeedbackState.NORMAL: + node.material = null + _outline.visible = false + _name_label.visible = false + FeedbackState.HOVER: + _apply_hover(key, node, rect) + FeedbackState.PRESSED: + node.position.y = base_y + 1.0 # 按下=位移 1px + FeedbackState.DISABLED: + _apply_disabled(node) + + +func _apply_hover(key: StringName, node: Control, rect: Rect2) -> void: + if _is_disabled(key): + _apply_disabled(node) + return + node.material = null + _outline.position = rect.position + _outline.size = rect.size + _outline.visible = true + _show_name_plate(key, rect) + _run_float(key) # 悬停浮动(整像素逐级吸附) + + +func _apply_disabled(node: Control) -> void: + node.material = _desat_mat # 降明度+去饱和(shader;仅锻炉/宝箱会有此态) + _outline.visible = false + _name_label.visible = false + + +func _run_float(key: StringName) -> void: + _float_tween = create_tween() + for step in 4: + var off := -1 - step + _float_tween.tween_callback(_set_float_offset.bind(key, off)).set_delay(UiKit.motion(0.03)) + # 悬离时由 _apply_state(NORMAL) 先归零位——本补间只承载升浮 + + +func _set_float_offset(key: StringName, off: int) -> void: + var layer_key: StringName = _hotspot_layer.get(key, &"") + var node: Control = _layer_nodes.get(layer_key, null) + if node == null or _layout == null: + return + for spec in _layout.layers: + if spec.key == layer_key: + node.position.y = spec.texture_rect.position.y + float(off) + return + + +func _float_offset_of(key: StringName) -> float: + var layer_key: StringName = _hotspot_layer.get(key, &"") + var node: Control = _layer_nodes.get(layer_key, null) + if node == null or _layout == null: + return 0.0 + for spec in _layout.layers: + if spec.key == layer_key: + return node.position.y - spec.texture_rect.position.y + return 0.0 + + +func _show_name_plate(key: StringName, rect: Rect2) -> void: + _name_label.text = String(_HOTSPOT_NAMES.get(key, "")) + _name_label.visible = true + _name_label.reset_size() + var pos := Vector2.ZERO + pos.x = clampf(rect.get_center().x - _name_label.size.x / 2.0, 0.0, _layout.canvas.x - _name_label.size.x) + pos.y = minf(rect.end.y + 8.0, _layout.canvas.y - _name_label.size.y) # 贴底物件防出画(画面内钳制) + _name_label.position = pos + + +func _on_mouse_exited() -> void: + if _hover_key != &"": + _apply_state(_hover_key, _base_state(_hover_key)) + _hover_key = &"" + if _press_key != &"": + _apply_state(_press_key, _base_state(_press_key)) + _press_key = &"" diff --git a/game/presentation/views/camp_scene_view.gd.uid b/game/presentation/views/camp_scene_view.gd.uid new file mode 100644 index 00000000..90ca8b4a --- /dev/null +++ b/game/presentation/views/camp_scene_view.gd.uid @@ -0,0 +1 @@ +uid://btsrctv5ddx16 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..1cc78727 --- /dev/null +++ b/game/presentation/views/camp_sub_view_base.gd @@ -0,0 +1,92 @@ +## 营地子界面模态壳基类(锻造工坊/秘宝匣/天赋树/奖励格图鉴四子界面公共形态): +## 实底全屏暖亮档+石纹暗角+顶栏(铭牌+钥石徽)+内容列+底栏返回钮。 +## 开合可见性归模态栈(登记层开合一律走栈);返回钮只发 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 + # 内容米板身(柜台行板对比底=PanelData——旧柜台面板形态延续,行板墨色对比表不脱锚) + var body := PanelContainer.new() + body.add_theme_stylebox_override("panel", + UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 10, 10, 10))) + body.size_flags_vertical = Control.SIZE_EXPAND_FILL + body.add_child(content) + col.add_child(body) + _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/confirm_dialog.gd b/game/presentation/views/confirm_dialog.gd index 5025b5b0..6ce39f15 100644 --- a/game/presentation/views/confirm_dialog.gd +++ b/game/presentation/views/confirm_dialog.gd @@ -1,14 +1,18 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION -## 通用确认弹窗:open(text, confirm_label, cancel_label) 双钮外发 confirmed/cancelled。 +## 通用确认弹窗:open(text, confirm_label, cancel_label) 双钮外发 confirmed/cancelled; +## open_three(text, confirm_label, secondary_label, cancel_label) 三钮增设 secondary_confirmed +##(营地大门两分支——卡④ T4;双钮调用方零改动:次钮随 open 隐藏)。 ## 文案与按钮语义装配归调用方——本类零业务触碰。 class_name ConfirmDialog extends Control signal confirmed +signal secondary_confirmed ## 次确认(三钮形态中钮——营地大门「开始新冒险」出口;卡④ T4) signal cancelled var _text: Label var _confirm_btn: Button +var _secondary_btn: Button var _cancel_btn: Button @@ -27,6 +31,9 @@ func _init() -> void: _cancel_btn = UiKit.button("取消") _cancel_btn.pressed.connect(_emit_cancelled) btn_row.add_child(_cancel_btn) + _secondary_btn = UiKit.button("次选项") + _secondary_btn.pressed.connect(_emit_secondary) + btn_row.add_child(_secondary_btn) _confirm_btn = UiKit.button("确认") _confirm_btn.pressed.connect(_emit_confirmed) btn_row.add_child(_confirm_btn) @@ -37,12 +44,20 @@ func open(text: String, confirm_label: String, cancel_label: String = "取消") _text.text = text _confirm_btn.text = confirm_label _cancel_btn.text = cancel_label + _secondary_btn.visible = false # 双钮形态(次钮隐藏——既有调用方零改动) # z 序纪律:层序唯一事实源=main.LAYER_CONTRACT(树序与登记序同源派生);开合单口 # 归组合根 ModalFlow(挂起兜底/遮挡记账/开侧同步)——本类只管文案与显示,不自行抬层 - + visible = true +## 三钮形态(营地大门有断点分支——继续上一段冒险/开始新冒险/取消;Q5 裁甲) +func open_three(text: String, confirm_label: String, secondary_label: String, cancel_label: String) -> void: + open(text, confirm_label, cancel_label) + _secondary_btn.text = secondary_label + _secondary_btn.visible = true + + func close() -> void: visible = false @@ -55,5 +70,9 @@ func _emit_confirmed() -> void: confirmed.emit() +func _emit_secondary() -> void: + secondary_confirmed.emit() + + func _emit_cancelled() -> void: cancelled.emit() diff --git a/game/presentation/views/forge_view.gd b/game/presentation/views/forge_view.gd new file mode 100644 index 00000000..7375bd83 --- /dev/null +++ b/game/presentation/views/forge_view.gd @@ -0,0 +1,159 @@ +## 锻造工坊子界面(自 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 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..14ed556f --- /dev/null +++ b/game/presentation/views/treasure_view.gd @@ -0,0 +1,238 @@ +## 秘宝匣子界面(自 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 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/presentation/views/ui_kit.gd b/game/presentation/views/ui_kit.gd index 0f8b3ec8..74110af8 100644 --- a/game/presentation/views/ui_kit.gd +++ b/game/presentation/views/ui_kit.gd @@ -247,6 +247,19 @@ const TEX_ENEMY_HOOK_HORROR := "res://assets/art/character/character_enemy_hook_ const TEX_ENEMY_DUERGAR_KING := "res://assets/art/character/character_enemy_duergar_king.png" # 灰矮人石君王 # 近景地台独立件(§6.1——源 48×12 九宫格横向拉伸、顶缘受光线 2px 不随拉伸变形;跨三关恒定不吃 wall_ambient tint) const TEX_STAGE_PLAZA := "res://assets/art/scene/scene_stage_plaza.png" +# 营地场景十层(art-bible §6.2——z0 bg 不透明全幅+z1 围墙透明细条+八物件透明层;布局矩形/命中矩形/z 序 +# 唯一事实源 = data/tables/camp_scene_layout.tres;素材名跟本组常量走(先定常量防断链); +# 缺位 load_tex→null→程序化占位,冒烟安全) +const TEX_CAMP_BG := "res://assets/art/scene/scene_camp_bg.png" # z0 背景层(不透明全幅 1280×800) +const TEX_CAMP_WALL := "res://assets/art/scene/scene_camp_wall.png" # z1 围墙层(透明细条,门洞留缺口) +const TEX_CAMP_BOARD := "res://assets/art/scene/scene_camp_board.png" # z2 布告板 +const TEX_CAMP_GATE := "res://assets/art/scene/scene_camp_gate.png" # z3 门扇 +const TEX_CAMP_DUMMY := "res://assets/art/scene/scene_camp_dummy.png" # z4 木桩 +const TEX_CAMP_FORGE := "res://assets/art/scene/scene_camp_forge.png" # z4 锻炉铁砧 +const TEX_CAMP_TENT := "res://assets/art/scene/scene_camp_tent.png" # z4 帐篷(承载件不可点) +const TEX_CAMP_CHEST := "res://assets/art/scene/scene_camp_chest.png" # z5 宝箱 +const TEX_CAMP_NOTEBOOK := "res://assets/art/scene/scene_camp_notebook.png" # z5 笔记本 +const TEX_CAMP_CAMPFIRE := "res://assets/art/scene/scene_camp_campfire.png" # z6 篝火(命中只取下半火堆) # 护甲类型图标三键(art-bible §5.3 工单:对象键 = 甲档变体;源 24×24 显示 24px = 1× 甲类整数倍; # 缺位 load_tex→null→字形回退,冒烟安全) const TEX_ARMOR_LIGHT := "res://assets/ui/icons/ui_armor_light.png" diff --git a/game/probe/screen_flow_probe.gd b/game/probe/screen_flow_probe.gd index d90071e5..1f939d5e 100644 --- a/game/probe/screen_flow_probe.gd +++ b/game/probe/screen_flow_probe.gd @@ -51,7 +51,7 @@ func _ready() -> void: func _screen_seals() -> Dictionary: return { "菜单": main.ui.screens.start_overlay != null and main.ui.screens.start_overlay.visible, - "营地": main.ui.modals.camp.overlay != null and main.ui.modals.camp.overlay.visible, + "营地": main.ui.modals.camp.scene != null and main.ui.modals.camp.scene.visible, "冒险": main.run_scope != null and main.run_scope.is_adventure, "终局": main.ui.modals.end_overlay != null and main.ui.modals.end_overlay.visible, } @@ -115,14 +115,14 @@ func _step_menu_to_camp() -> void: _expect_exclusive("②菜单→营地", "营地") -# ---- ③ 营地 → 菜单(回退路径:营地关闭发自覆盖层自身信号)---- +# ---- ③ 营地 → 菜单(回退路径:篝火二次确认动线——卡④ 营地唯一回菜单出口;选档层随关层收闭同收)---- func _step_camp_back_to_menu() -> void: - if main.ui.modals.camp.overlay == null: - _problems.append("③营地→菜单:营地覆盖层句柄缺失〔句柄子容器批 V1〕") + if main.ui.modals.camp.scene == null: + _problems.append("③营地→菜单:营地场景根句柄缺失〔句柄子容器批 V1〕") return - main.ui.modals.camp.overlay.closed.emit() + main.ui.modals.camp.scene.object_pressed.emit(&"campfire") await _frames() - main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点) + main.ui.modals.confirm_dialog.confirmed.emit() # 二次确认离场(回主菜单——卡④ T4) await _frames() _expect_exclusive("③营地→菜单(回退)", "菜单") diff --git a/game/probe/shot_probe.gd b/game/probe/shot_probe.gd index 0fe9021d..90184d93 100644 --- a/game/probe/shot_probe.gd +++ b/game/probe/shot_probe.gd @@ -53,7 +53,7 @@ func _ready() -> void: var top_ry := -1 for c: Node in [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]: if c == null or not (c is CanvasItem) or not (c as CanvasItem).visible: continue @@ -119,17 +119,15 @@ func _ready() -> void: await get_tree().process_frame if main.ui.modals.career_overlay.visible or not main.ui.screens.start_overlay.visible: _problems.append("生涯页关闭后页未隐藏或开始层未恢复") - # ② 营地(进档开层新路径——卡② mn9;槽模板注入隔离命名空间) + # ② 营地场景图(进档开层新路径——卡② mn9;截图=可点击场景图形态——卡④ 契约 9 V1 验收载体) if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_shot_probe_camp"): _problems.append("营地入口缺失(选档层新档钮——新路径驱动中断)〔句柄子容器批 V1〕") else: await get_tree().process_frame await _snap("camp") - var close_btn: Button = ProbeUiUtil.find_button(main.ui.modals.camp.overlay, "关闭营地", "contains") - if close_btn != null: - close_btn.pressed.emit() - await get_tree().process_frame - main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态) + main.ui.modals.camp.scene.object_pressed.emit(&"campfire") # 收闭改接篝火动线(营地根态无关闭钮——卡④ T5) + await get_tree().process_frame + main.ui.modals.confirm_dialog.confirmed.emit() # 二次确认离场(关层收闭——选档层随收) await get_tree().process_frame # ③ 关卡画面(开始冒险→停点出图——入口停点语义;函数直调中间态 mn10) main._start_adventure_from_menu() diff --git a/game/probe/ui_click_probe.gd b/game/probe/ui_click_probe.gd index 6cef204f..309e8526 100644 --- a/game/probe/ui_click_probe.gd +++ b/game/probe/ui_click_probe.gd @@ -65,17 +65,19 @@ func _ready() -> void: _problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)〔句柄子容器批 V1〕") else: await get_tree().process_frame - if not main.ui.modals.camp.overlay.visible: - _problems.append("营地层未打开(进档开层新路径)〔句柄子容器批 V1〕") - elif main.ui.modals.camp.overlay._tree_buy_btns.is_empty(): - _problems.append("树淬炼柜台购买钮缺失(点击路径——四柜台重排后树=主界面)〔句柄子容器批 V1〕") + if not main.ui.modals.camp.scene.visible: + _problems.append("营地场景根未打开(进档开层新路径)〔句柄子容器批 V1〕") + main.ui.modals.camp.scene.object_pressed.emit(&"dummy") # 场景点选真实路由(木桩→天赋树子界面——卡④ T5) + await get_tree().process_frame + if main.ui.modals.camp.tree._tree_buy_btns.is_empty(): + _problems.append("树淬炼购买钮缺失(四柜台拆解后树=独立子界面)〔句柄子容器批 V1〕") else: # 树钮句柄用后即取(refresh_camp 随刷重建树行板——旧钮 queue_free,跨刷新句柄必失效) iso_c.meta_state._add_keystone(9999) - main.ui.modals.camp.overlay.refresh_camp(main._camp_data()) + main.ui.modals.camp.tree.refresh_camp(main._camp_data()) await get_tree().process_frame var buy_btn: Button = null - for card in main.ui.modals.camp.overlay._tree_buy_btns: + for card in main.ui.modals.camp.tree._tree_buy_btns: if String(card.node_id) == "sp_hp": buy_btn = card.btn if buy_btn == null: @@ -93,16 +95,16 @@ func _ready() -> void: _problems.append("营地树购买未持久(reload 后级 != 1)〔句柄子容器批 V1〕") # Fork 双态切换点击路径(V20——购=解锁默认 A,钮切 B;前置=同调位 1,先点亮) var socket_btn: Button = null - for card in main.ui.modals.camp.overlay._tree_buy_btns: + for card in main.ui.modals.camp.tree._tree_buy_btns: if String(card.node_id) == "att_socket1": socket_btn = card.btn if socket_btn != null: socket_btn.pressed.emit() await get_tree().process_frame - main.ui.modals.camp.overlay.refresh_camp(main._camp_data()) + main.ui.modals.camp.tree.refresh_camp(main._camp_data()) await get_tree().process_frame var fork_btn: Button = null - for card in main.ui.modals.camp.overlay._tree_buy_btns: + for card in main.ui.modals.camp.tree._tree_buy_btns: if String(card.node_id) == "fork_posture": fork_btn = card.btn if fork_btn == null: @@ -110,10 +112,10 @@ func _ready() -> void: else: fork_btn.pressed.emit() await get_tree().process_frame - main.ui.modals.camp.overlay.refresh_camp(main._camp_data()) + main.ui.modals.camp.tree.refresh_camp(main._camp_data()) await get_tree().process_frame var side_b: Button = null - for fk in main.ui.modals.camp.overlay._tree_fork_btns: + for fk in main.ui.modals.camp.tree._tree_fork_btns: if String(fk.group) == "fg_posture" and String(fk.side) == "b": side_b = fk.btn if side_b == null: @@ -125,29 +127,23 @@ func _ready() -> void: iso_fk.load_from_disk() if String(iso_fk.meta_state.tree_state().fork_sides.get("fg_posture", "")) != "b": _problems.append("Fork 切态未持久(reload 后激活态 != b)〔句柄子容器批 V1〕") - # ---- 营地六处灰字取色断言(迭代36 wc4——真实入口开营地后按文本锚取 label, - # font_color 逐处 == STONE_DEEP;六源位 :167 图鉴锁文/:417+:476 行状态字/:509 同调形态/:513 档位标签/:525 刷新行)---- - var camp_labels: Array = [] - _collect_labels_by_text(main.ui.modals.camp.overlay, camp_labels) - var atlas_locks := _labels_with_text(camp_labels, "柜台结构位预留") - if atlas_locks.is_empty(): - _problems.append("营地灰字:图鉴柜台锁文 label 未找到(wc4 定位锚失守)〔句柄子容器批 V1〕") - for l in atlas_locks: - if l.get_theme_color("font_color") != UiKit.STONE_DEEP: - _problems.append("营地灰字:图鉴锁文未用 STONE_DEEP(wc4 :167)〔句柄子容器批 V1〕") - # 「未解锁」锚收窄到秘宝匣行容器子树(树行/锻造行也有「未解锁」态字样——非 wc4 目标位) - var locked_status: Array = [] - if main.ui.modals.camp.overlay.get("_workshop_holder") != null: - var ws_labels: Array = [] - _collect_labels_by_text(main.ui.modals.camp.overlay._workshop_holder, ws_labels) - locked_status = _labels_with_text(ws_labels, "未解锁") + # ---- 营地灰字取色断言(迭代36 wc4——四子界面拆分后按视图分扫;卡④ T5 迁移): + # 图鉴锁文→codex 视图(锚随形态订正去『柜台』);秘宝行未解锁/同调形态/档位/刷新行→treasure 视图 + # _workshop_holder 子树。font_color 逐处 == STONE_DEEP;树链段已闭树界面。 + main.ui.modals.camp.tree.closed.emit() # 树界面返回(关层归 presenter 单口) + await get_tree().process_frame + main.ui.modals.camp.scene.object_pressed.emit(&"chest") # 宝箱→秘宝匣(灰字四源位宿主) + await get_tree().process_frame + var ws_labels: Array = [] + _collect_labels_by_text(main.ui.modals.camp.treasure._workshop_holder, ws_labels) + var locked_status: Array = _labels_with_text(ws_labels, "未解锁") if locked_status.is_empty(): _problems.append("营地灰字:秘宝匣行「未解锁」态 label 未找到(wc4 夹具前提失守——新档应有锁定秘宝行)〔句柄子容器批 V1〕") for l in locked_status: if l.get_theme_color("font_color") != UiKit.STONE_DEEP: _problems.append("营地灰字:秘宝行「未解锁」态未用 STONE_DEEP(wc4 :417/:476)〔句柄子容器批 V1〕") var form_found := false - for l in camp_labels: + for l in ws_labels: if l.text == "被动" or l.text == "主动·充能": form_found = true if l.get_theme_color("font_color") != UiKit.STONE_DEEP: @@ -158,7 +154,7 @@ func _ready() -> void: for v in UiKit.TREASURE_TIER_LABELS.values(): tier_vals[String(v)] = true var tier_found := false - for l in camp_labels: + for l in ws_labels: if tier_vals.has(l.text): tier_found = true if l.get_theme_color("font_color") != UiKit.STONE_DEEP: @@ -166,18 +162,30 @@ func _ready() -> void: if not tier_found: _problems.append("营地灰字:同调档位标签未找到(wc4 定位锚失守)〔句柄子容器批 V1〕") var refresh_inked := false - for l in camp_labels: + for l in ws_labels: if String(l.text).contains("三选一刷新"): refresh_inked = true if l.get_theme_color("font_color") != UiKit.STONE_DEEP: _problems.append("营地灰字:刷新行未用 STONE_DEEP(wc4 :525)〔句柄子容器批 V1〕") if not refresh_inked: _problems.append("营地灰字:刷新行 label 未找到(wc4 定位锚失守)〔句柄子容器批 V1〕") - main._on_camp_closed() + main.ui.modals.camp.treasure.closed.emit() + await get_tree().process_frame + main.ui.modals.camp.scene.object_pressed.emit(&"board") # 布告板→图鉴占位(锁文宿主) + await get_tree().process_frame + var codex_labels: Array = [] + _collect_labels_by_text(main.ui.modals.camp.codex, codex_labels) + var atlas_locks := _labels_with_text(codex_labels, "结构位预留") + if atlas_locks.is_empty(): + _problems.append("营地灰字:图鉴锁文 label 未找到(wc4 定位锚失守——锚随形态订正去『柜台』)〔句柄子容器批 V1〕") + for l in atlas_locks: + if l.get_theme_color("font_color") != UiKit.STONE_DEEP: + _problems.append("营地灰字:图鉴锁文未用 STONE_DEEP(wc4 :167)〔句柄子容器批 V1〕") + main.ui.modals.camp.codex.closed.emit() + await get_tree().process_frame + main._on_camp_closed() # 关层收闭(营地与选档层同收+开始层恢复——卡④ T3) await get_tree().process_frame - main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点) - await get_tree().process_frame - if main.ui.modals.camp.overlay.visible or not main.ui.screens.start_overlay.visible: + if main.ui.modals.camp.scene.visible or not main.ui.screens.start_overlay.visible: _problems.append("营地关闭后未恢复开始层〔句柄子容器批 V1〕") # ---- 选档界面行为走查(迭代 33 卡③):sl5 空槽新档链/sl6 有档槽进档链/sl7 删档二次确认双路径 # /sl8 消费方分派单口/sl12 动线序不回闪。开层=函数直调(「进入存档」钮归卡② mn3——中间态在案); @@ -208,7 +216,7 @@ func _ready() -> void: new_btn.pressed.emit() # 真实按钮信号路径 → _on_save_select_new → create_slot+open_camp for fi in 3: await get_tree().process_frame - samples.append([main.ui.modals.save_select_overlay.visible, main.ui.modals.camp.overlay.visible, main.ui.screens.start_overlay.visible]) + samples.append([main.ui.modals.save_select_overlay.visible, main.ui.modals.camp.scene.visible, main.ui.screens.start_overlay.visible]) if samples[0][0]: _problems.append("[savesel] sl12 新档后选档层仍可见(落点开层未发生)") elif not samples[0][1]: @@ -217,7 +225,7 @@ func _ready() -> void: _problems.append("[savesel] sl12 动线序违约:开始层在落点帧恢复(回闪一帧)") if main.save_manager.slots.active_slot() != 1 or not FileAccess.file_exists(csel_tpl % 1): _problems.append("[savesel] sl5 新档链失守(active=%d 应=1 且槽1 白板应落盘)" % main.save_manager.slots.active_slot()) - var occluded: Array = main.ui.modals.flow.stack._occluded.get(main.ui.modals.camp.overlay, []) + var occluded: Array = main.ui.modals.flow.stack._occluded.get(main.ui.modals.camp.scene, []) if not occluded.has(main.ui.modals.save_select_overlay): _problems.append("[savesel] sl12 选档层被关闭而非栈记账挂起(先关后开形态——遮挡挂起中间态钉死)") if int((JSON.parse_string(FileAccess.get_file_as_string(csel_tpl % 1)) as Dictionary).get("meta", {}).get("keystone_balance", -1)) != 0: @@ -235,7 +243,7 @@ func _ready() -> void: await get_tree().process_frame if main.save_manager.slots.active_slot() != 2: _problems.append("[savesel] sl6 进档链失守(active=%d 应=2——激活该槽)" % main.save_manager.slots.active_slot()) - if not main.ui.modals.camp.overlay.visible or main.ui.modals.save_select_overlay.visible: + if not main.ui.modals.camp.scene.visible or main.ui.modals.save_select_overlay.visible: _problems.append("[savesel] sl6 落点未开营地层或选档未隐藏") main._on_camp_closed() await get_tree().process_frame @@ -668,6 +676,51 @@ func _ready() -> void: # 卡④大门 T5 接收,两头登记禁静默删除;续玩钮已退役,主菜单确认链已摘 mn6)。 # ① 战斗收口断点写盘断言保留(上方——冒险流直调中间态 mn10)。 + # ---- 大门两分支断言段(卡④ T5 接收——卡② 移交落点;V3 断言面)---- + # 驱动=营地场景根点选信号(真实路由)→组合根 _on_camp_object_pressed→确认弹窗。 + # 无断点=两出口(是/否);有断点=三出口(set_breakpoint 注入选路停点形——同下方开场行③先例)。 + # 判据绑可观测提交面:点大门不直接进冒险(is_adventure 翻转只在 confirmed 后);cancelled 恒= + # 无变更退回营地(写口零调用——余额读数不变+营地根 visible);开局后营地场景屏隐藏。 + ProbeUiUtil.isolate_save(main, "smoke_click_probe_gate", true, false, true) + main._camp_pres.open_camp() + await get_tree().process_frame + var bal_gate: int = main._camp_data()["balance"] + main.ui.modals.camp.scene.object_pressed.emit(&"gate") + await get_tree().process_frame + if not main.ui.modals.confirm_dialog.is_open(): + _problems.append("大门无断点:确认弹窗未开(点大门应先出确认不直接进冒险)〔营地图批 V3〕") + if main.run_scope.is_adventure: + _problems.append("大门无断点:未确认即进冒险〔营地图批 V3〕") + if main.ui.modals.confirm_dialog._secondary_btn.visible: + _problems.append("大门无断点:次钮应隐藏(两出口形态)〔营地图批 V3〕") + main.ui.modals.confirm_dialog.cancelled.emit() + await get_tree().process_frame + if not main.ui.modals.camp.scene.visible or main.run_scope.is_adventure or main._camp_data()["balance"] != bal_gate: + _problems.append("大门取消:未无变更退回营地(写口零调用/营地根 visible 失守)〔营地图批 V3〕") + main.ui.modals.camp.scene.object_pressed.emit(&"gate") + await get_tree().process_frame + main.ui.modals.confirm_dialog.confirmed.emit() # 「是」→开始新冒险 + await get_tree().process_frame + if not main.run_scope.is_adventure or main.ui.modals.camp.scene.visible: + _problems.append("大门确认:冒险未发起或营地场景屏未隐藏〔营地图批 V3〕") + # 有断点三出口(选路停点形注入——同开场行③ set_breakpoint 先例) + main.save_manager.set_breakpoint({ + "master_seed": 42, "rng_counter": 0, "hero_hp": 30, "gp": 10, "keystone": 0, + "level_index": 1, "bonus": {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}, + "shop_purchased": {}, "relics": [], "keystone_drops": [], "bonus_taken": {}, + "node_id": -1, "visited_ids": [], "graph_gen_v2": 1, + }) + main._camp_pres.open_camp() + await get_tree().process_frame + main.ui.modals.camp.scene.object_pressed.emit(&"gate") + await get_tree().process_frame + if not main.ui.modals.confirm_dialog.is_open() or not main.ui.modals.confirm_dialog._secondary_btn.visible: + _problems.append("大门有断点:三出口形态失守(次钮应现形)〔营地图批 V3〕") + main.ui.modals.confirm_dialog.secondary_confirmed.emit() # 「开始新冒险」出口 + await get_tree().process_frame + if not main.run_scope.is_adventure or main.ui.modals.camp.scene.visible: + _problems.append("大门有断点次确认:冒险未发起或营地未隐藏〔营地图批 V3〕") + # ---- 开场行三路径段(迭代36 wc1/wc2——真实入口驱动;隔离新档)---- # 驱动设计(0/0 白板局生存性不成立的实证处置——L2 记录进实现报告): # ① 菜单开新局·白板反向=独立短局:0/0 滑条开局断言 L1 开场行零后缀(不开驱动—— diff --git a/game/probe/ui_contrast_probe.gd b/game/probe/ui_contrast_probe.gd index 652fb04e..eaae9ffb 100644 --- a/game/probe/ui_contrast_probe.gd +++ b/game/probe/ui_contrast_probe.gd @@ -45,15 +45,11 @@ func _pairs() -> Array: ["商店·爬价规则墨/暖亮页底", UiKit.WARM_SOFT, UiKit.BG_BRIGHT, 1.0],# 货架面爬价规则行(page_shell 亮底) ["商店·货架卡名墨/货架板", UiKit.INK_TITLE, UiKit.PANEL_DATA, 1.0], # bcard 货架卡商品名(换 TG 带后未机检位) ["商店·货架价签墨/货架板", UiKit.GOLD_INK, UiKit.PANEL_DATA, 1.0], # bcard 价签「N ◈」(深金墨 5.21:1) - ["营地·等级章字/头带深棕", UiKit.INK_ON_DEEP, UiKit.CAMP_HEADER_BAND, 1.0],# 锻造行 Lv 章(木底小章亮墨) - ["营地·柜台头带字/头带深棕", UiKit.WARM, UiKit.CAMP_HEADER_BAND, 1.0], # 三柜台头带标题(暖白 bold) - ["营地·铭牌副题墨/铭牌板", UiKit.STONE_DEEP, UiKit.PANEL_DATA, 1.0],# 营地铭牌副题「跨局成长账本…」(灰石深化 5.98:1) ["营地·行名墨/行板", UiKit.INK, UiKit.CAMP_ROW_PLATE, 1.0], # 锻造/工坊行名(墨族压暖米行板) ["营地·行价格墨/行板", UiKit.STONE_DEEP, UiKit.CAMP_ROW_PLATE, 1.0],# 锻造行「N 钥石」价签/工坊进度行(灰石深化 6.70:1) ["营地·树分组头墨/柜台板", UiKit.GOLD_INK, UiKit.PANEL_DATA, 1.0],# 天赋树脉分组头「·锻造脉·」(深金墨 5.21:1) ["营地·树行名墨/柜台板", UiKit.INK_LABEL, UiKit.PANEL_DATA, 1.0], # 天赋树行「名 Lv0/1」(标签深墨) ["营地·工坊状态墨/行板", UiKit.GOLD_INK, UiKit.CAMP_ROW_PLATE, 1.0],# 工坊状态字「已解锁」(深金墨 5.84:1·最不利现役态) - ["营地·最近冒险墨/暖亮底", UiKit.WARM_SOFT, UiKit.BG_BRIGHT, 1.0], # 底栏最近冒险条带(暖白次级) ["设置·警示文/设置板", UiKit.INK_HINT, UiKit.PANEL_DATA, 1.0], # 设置警示文/后果说明(暖棕提示墨) ["设置·选项标签/设置板", UiKit.INK_LABEL, UiKit.PANEL_DATA, 1.0], # 设置行标签/状态字(标签深墨) ["三选一·停点头部墨/暗页底", UiKit.WARM_SOFT, UiKit.BG_ROOT, 1.0], # 三选一停点头部/回血反馈行(page_shell 根底档) diff --git a/game/probe/ui_probe.gd b/game/probe/ui_probe.gd index 22b3790d..65d5b429 100644 --- a/game/probe/ui_probe.gd +++ b/game/probe/ui_probe.gd @@ -28,30 +28,40 @@ func _ready() -> void: problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)〔句柄子容器批 V1〕") else: await get_tree().process_frame - if not main.ui.modals.camp.overlay.visible: - problems.append("营地层未打开(进档开层新路径)〔句柄子容器批 V1〕") + if not main.ui.modals.camp.scene.visible: + problems.append("营地场景根未打开(进档开层新路径)〔句柄子容器批 V1〕") elif main.ui.screens.start_overlay.visible: problems.append("营地层打开时开始层未隐藏(规范 2.3 遮挡纪律)〔句柄子容器批 V1〕") else: - if not String(main.ui.modals.camp.overlay._balance_label.text).begins_with("钥石"): - problems.append("营地余额文案缺失(%s)〔句柄子容器批 V1〕" % main.ui.modals.camp.overlay._balance_label.text) - if main.ui.modals.camp.overlay._tree_buy_btns.is_empty(): - problems.append("树淬炼柜台购买钮缺失(四柜台重排后树=主界面)〔句柄子容器批 V1〕") + # 天赋树子界面(场景点选真实路由——木桩命中区进树界面;卡④ T5 深员改指) + main.ui.modals.camp.scene.object_pressed.emit(&"dummy") + await get_tree().process_frame + if not main.ui.modals.camp.tree.visible: + problems.append("天赋树子界面未打开(场景点选路由中断)〔营地图批 V2〕") + if not String(main.ui.modals.camp.tree._balance_label.text).begins_with("钥石"): + problems.append("营地余额文案缺失(%s)〔句柄子容器批 V1〕" % main.ui.modals.camp.tree._balance_label.text) + if main.ui.modals.camp.tree._tree_buy_btns.is_empty(): + problems.append("树淬炼购买钮缺失(四柜台拆解后树=独立子界面)〔句柄子容器批 V1〕") else: var btn_by_id := {} - for card2 in main.ui.modals.camp.overlay._tree_buy_btns: + for card2 in main.ui.modals.camp.tree._tree_buy_btns: btn_by_id[String(card2.node_id)] = card2.btn if not btn_by_id.has("sp_hp") or bool(btn_by_id["sp_hp"].disabled): problems.append("脊柱生命 L1 白板可购钮未启用(无门槛——can_buy 不含余额面语义)〔句柄子容器批 V1〕") if not btn_by_id.has("att_socket2") or not bool(btn_by_id["att_socket2"].disabled): problems.append("同调位 2 前置未亮应禁用(点亮前置语义)〔句柄子容器批 V1〕") - if main.ui.modals.camp.overlay._workshop_rows.size() != 12: - problems.append("秘宝匣柜台行数错误(%d ≠ 12——迭代 22 秘宝批换代)〔句柄子容器批 V1〕" % main.ui.modals.camp.overlay._workshop_rows.size()) - main._on_camp_closed() + main.ui.modals.camp.tree.closed.emit() + await get_tree().process_frame + # 秘宝匣子界面(场景点选真实路由——宝箱命中区) + main.ui.modals.camp.scene.object_pressed.emit(&"chest") + await get_tree().process_frame + if main.ui.modals.camp.treasure._workshop_rows.size() != 12: + problems.append("秘宝匣行数错误(%d ≠ 12——迭代 22 秘宝批换代)〔句柄子容器批 V1〕" % main.ui.modals.camp.treasure._workshop_rows.size()) + main.ui.modals.camp.treasure.closed.emit() + await get_tree().process_frame + main._on_camp_closed() # 关层收闭(营地与选档层同收+开始层恢复——卡④ T3) await get_tree().process_frame - main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点) - await get_tree().process_frame - if main.ui.modals.camp.overlay.visible or not main.ui.screens.start_overlay.visible: + if main.ui.modals.camp.scene.visible or not main.ui.screens.start_overlay.visible: problems.append("营地关闭后未恢复开始层(开合往返失守)〔句柄子容器批 V1〕") # 换回全新零档(营地段购买态不泄入后续冒险段——确定性;先删后建防陈旧档泄入) ProbeUiUtil.isolate_save(main, "smoke_ui_probe_camp", true, false, true) diff --git a/game/probe/ui_struct_probe.gd b/game/probe/ui_struct_probe.gd index 7f800104..bf485e30 100644 --- a/game/probe/ui_struct_probe.gd +++ b/game/probe/ui_struct_probe.gd @@ -42,33 +42,70 @@ func _ready() -> void: problems.append("[camp] 营地入口缺失(选档层新档钮——mn9 新路径驱动中断)〔句柄子容器批 V1〕") else: _settle() - await _audit("camp", [main.ui.modals.camp.overlay]) - # 营地灰字源 token 扫描(迭代36 wc4 静态半——源内零裸 UiKit.STONE;判别式= - # STONE 出现次数 == STONE_DEEP 出现次数,即每处 STONE 都带 _DEEP 后缀) - var camp_src := FileAccess.get_file_as_string("res://presentation/views/camp_overlay_view.gd") - if camp_src.count("UiKit.STONE") != camp_src.count("UiKit.STONE_DEEP"): - problems.append("[camp] camp_overlay_view.gd 存在裸 UiKit.STONE(非 _DEEP 后缀——wc4 源扫描)〔句柄子容器批 V1〕") - main._on_camp_closed() + await _audit("camp", [main.ui.modals.camp.scene]) # 审计根=营地场景图(卡④ T5——CampOverlay 退役) + # 营地灰字源 token 扫描(迭代36 wc4 静态半——营地域视图源内零裸 UiKit.STONE;判别式= + # STONE 出现次数 == STONE_DEEP 出现次数;卡④ T5 改扫场景根+四子界面+公共壳六件) + var camp_stone := 0 + var camp_stone_deep := 0 + for vf in ["camp_scene_view", "forge_view", "treasure_view", "tree_view", "codex_view", "camp_sub_view_base"]: + var vf_src := FileAccess.get_file_as_string("res://presentation/views/%s.gd" % vf) + camp_stone += vf_src.count("UiKit.STONE") + camp_stone_deep += vf_src.count("UiKit.STONE_DEEP") + if camp_stone != camp_stone_deep: + problems.append("[camp] 营地域视图存在裸 UiKit.STONE(非 _DEEP 后缀——wc4 源扫描)〔句柄子容器批 V1〕") + # ---- 营地场景层序段(卡④ V1①——子节点序=layout 表 z 序逐层相等、各层纹理矩形与表逐值一致; + # 锻炉可达(V2 场景点选路由)+顶栏位在座(V8 结构)+根态反参照(V9——根态零文字按钮))---- + var layout: CampSceneLayoutTable = main.db.presentation.camp_scene_layout + var order_ok := true + var rect_ok := true + for i in layout.layers.size(): + var layer_node: Control = main.ui.modals.camp.scene.get_child(i) + if String(layer_node.name) != "Layer_%s" % layout.layers[i].key: + order_ok = false + if layer_node.position != layout.layers[i].texture_rect.position or layer_node.size != layout.layers[i].texture_rect.size: + rect_ok = false + if not order_ok: + problems.append("[camp] 场景子节点序≠layout 表 z 序(逐层相等失守)〔营地图批 V1〕") + if not rect_ok: + problems.append("[camp] 层贴图矩形≠layout 表(逐值一致失守)〔营地图批 V1〕") + main.ui.modals.camp.scene.object_pressed.emit(&"forge") _settle() - main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点) + if not main.ui.modals.camp.forge.visible: + problems.append("[camp] 点选锻炉→锻造子界面未开(场景点选路由中断)〔营地图批 V2〕") + var hud_slot: Control = main.ui.modals.camp.scene.get_node_or_null("HudBandSlot") + if hud_slot == null or hud_slot.custom_minimum_size.y != 48: + problems.append("[camp] 顶栏 HUD 预留位缺失或高≠48(浮层覆盖不挤压画面)〔营地图批 V8〕") + var scene_buttons := 0 + for n in main.ui.modals.camp.scene.get_children(): + if n is Button: + scene_buttons += 1 + if scene_buttons != 0: + problems.append("[camp] 场景根态存在文字按钮(反参照纪律——禁文字按钮列表形态)〔营地图批 V9〕") + main.ui.modals.camp.forge.closed.emit() + _settle() + main._on_camp_closed() # 关层收闭(营地与选档层同收+开始层恢复——卡④ T3) _settle() - # ---- 生涯页:入口点击链(合成点击真路径=pressed.emit→组合根 _open_career)→ + # ---- 生涯页:入口=帐篷旁笔记本(卡④ T3 改挂——场景点选信号真实路由→组合根 _open_career)→ # 结构断言族→只读巡检(契约 3)+零动效契约+零跳转契约(提纲 P4 甲)→组合根真实路径关闭。 #(登记序/树序契约统一在全模态 _modal_registry_contract——boot 段跑)---- - # 生涯页驱动=函数直调中间态(卡② mn9/mn10——careerbtn 已撤,卡④ 营地笔记本改挂前无 UI 路径) - main._open_career() + # 营地语境:开始层全程隐藏(笔记本入口在营地场景根——关生涯回营地不回开始层,卡④ T3 语境分支) + main._camp_pres.open_camp() # 重开营地场景根(笔记本入口语境) + _settle() + main.ui.modals.camp.scene.object_pressed.emit(&"notebook") _settle() if not main.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible: - problems.append("[career] 生涯页未打开或开始层未隐藏(函数直调中间态断裂)") + problems.append("[career] 生涯页未打开或开始层未隐藏(笔记本改挂路径断裂)〔营地图批 V5〕") await _audit("career", [main.ui.modals.career_overlay]) _career_readonly_contract(main) _career_zero_motion_contract(main) _career_nojump_contract(main) main._on_career_closed() _settle() - if main.ui.modals.career_overlay.visible or not main.ui.screens.start_overlay.visible: - problems.append("[career] 关闭后页未隐藏或开始层未恢复(模态栈单口失守)") + if main.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible: + problems.append("[career] 关闭后页未隐藏或开始层回弹(营地语境分支失守)〔营地图批 V5〕") + main._on_camp_closed() # 关营地复归菜单语境(后续选档段前提) + _settle() # ---- 选档界面(迭代 33 卡③):函数直调开层(「进入存档」钮归卡② mn3——点击链在卡② 承接), # sl1 三槽呈现/sl2 摘要三项逐槽读数/sl3 摘要禁项/sl4 操作集二分/sl9 重启链不预选。 @@ -394,7 +431,7 @@ func _career_zero_motion_contract(main: Node) -> void: # ⑫模态登记/树序全量合同(V1——career 专属登记序契约推广到全部模态+屏壳; # 判据=main.ui.LAYER_CONTRACT 单一事实源:登记册序==合同模态序、直接子层树序==合同序逐对相等。 # 两特例按合同数据表达:level_screen 宿住冒险屏壳(树序随宿主=全模态之下,遮挡语义归模态栈 -# screen_state 登记)、camp_overlay 成员与 camp_screen 屏壳同槽(屏壳树序代言成员登记位)。 +# screen_state 登记)、camp_scene 成员与 camp_screen 屏壳同槽(屏壳树序代言成员登记位)。 # _career_registry_contract 的断言(career=状态面板后/确认弹窗前、树序同步)由本全量合同覆盖) # ⑭选档界面契约(卡③ sl1/sl2/sl3/sl4/sl9——呈现面全部来自喂入数据,视图零自判) func _save_select_contract(main: Node, tpl: String) -> void: @@ -516,7 +553,7 @@ func _modal_registry_contract(main: Node) -> void: elif lv.get_parent().get_index() >= main.ui.modals.legend_overlay.get_index(): problems.append("[registry] 冒险屏壳应在全模态之下(宿主 idx=%d legend idx=%d)〔装配生命批 V1〕" % [ lv.get_parent().get_index(), main.ui.modals.legend_overlay.get_index()]) - # ④ 营地同槽特例:camp_screen 屏壳树序夹商店层与设置层之间(成员 camp_overlay 登记位代言) + # ④ 营地同槽特例:camp_screen 屏壳树序夹商店层与设置层之间(成员 camp_scene 登记位代言) var cs: Node = main.ui.screens.camp_screen if cs.get_index() <= main.ui.modals.shop_overlay.get_index() or cs.get_index() >= main.ui.modals.settings_overlay.get_index(): problems.append("[registry] camp_screen 屏壳树序应夹商店与设置层之间(idx=%d shop=%d settings=%d)〔装配生命批 V1〕" % [ diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index adc03204..447d1448 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -192,6 +192,7 @@ func _suites() -> Array: {"path": "res://tests/unit/presentation/reward_overlay_test.gd", "expect": 0}, # 奖励节点 UI 直验(盲区补漏) {"path": "res://tests/unit/presentation/level_map_info_fog_test.gd", "expect": 0}, # 信息迷雾——呈现显隐/Boss 豁免/详情零显示/大图拟合 {"path": "res://tests/unit/presentation/forge_workbench_test.gd", "expect": 0}, # 工作间两区六路线 UI 直验(V13 自动化前置面) + {"path": "res://tests/unit/presentation/camp_scene_layout_test.gd", "expect": 0}, # 营地场景布局数据件校验(V14——fail-fast 判据+七命中区几何红线) {"path": "res://tests/unit/battle_settlement/damage_math_test.gd", "expect": 0}, {"path": "res://tests/unit/battle_settlement/damage_controller_test.gd", "expect": 0}, {"path": "res://tests/unit/battle_settlement/roll_multiplier_test.gd", "expect": 0}, # 掷点与乘区宿主(敌攻击类型轴/护甲掷点行为面/乘区接线/伤害类型克制与标错/fresh 池攻击链——批 B 自 smoke_all 迁入) 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/camp_scene_layout_test.gd b/game/tests/unit/presentation/camp_scene_layout_test.gd new file mode 100644 index 00000000..8605dc48 --- /dev/null +++ b/game/tests/unit/presentation/camp_scene_layout_test.gd @@ -0,0 +1,89 @@ +extends SceneTree +# 营地场景布局数据件校验套件(V14——camp_scene_layout.tres 加载期 fail-fast 判据+七命中区几何红线机检) +# 覆盖:校验函数通过(键唯一/贴图在画布/命中成对/围墙带包络)· 七可点件键集 · 两两不相交 · +# 不越画面 · 不侵顶栏禁区 · 篝火命中只取下半(关系谓词)· 门扇对齐门洞缺口 · 布告板挂围墙带。 +# 断言全部为表内关系谓词——禁坐标字面量(数值单一事实源 = 数据行本表)。 +# 运行:godot --headless --path game --script res://tests/unit/presentation/camp_scene_layout_test.gd(expect 0) +const Kit := preload("res://tests/unit/test_kit.gd") + +const _EXPECTED_KEYS: Array[StringName] = [&"gate", &"forge", &"dummy", &"chest", &"notebook", &"board", &"campfire"] + + +func _initialize() -> void: + call_deferred("_test_layout_hotspot_geometry") + + +func _test_layout_hotspot_geometry() -> void: + var table: CampSceneLayoutTable = load("res://data/tables/camp_scene_layout.tres") + Kit.check("V14 布局表加载且 schema 类在位〔营地图批 V14〕", table is CampSceneLayoutTable and table != null) + if table == null: + quit(Kit.finish("camp_scene_layout_test")) + return + var problems: Array[String] = table.validate() + Kit.check("V14 校验函数通过(问题清单空,实得 %s)〔营地图批 V14〕" % [problems], problems.is_empty()) + var clickables: Array[CampSceneLayerSpec] = [] + for spec in table.layers: + if spec.is_clickable(): + clickables.append(spec) + Kit.check("V14 可点件数=7(实得 %d)〔营地图批 V14〕" % clickables.size(), clickables.size() == 7) + var got_keys: Array[StringName] = [] + for spec in clickables: + got_keys.append(spec.hotspot_key) + got_keys.sort() + var want_keys: Array[StringName] = [] + for k in _EXPECTED_KEYS: + want_keys.append(k) + want_keys.sort() + Kit.check("V14 命中键集=七物件绑定表(实得 %s)〔营地图批 V14〕" % [got_keys], got_keys == want_keys) + # 两两不相交(Rect2.intersects——贴边相触不算相交) + var disjoint := true + var pair := "" + for i in clickables.size(): + for j in range(i + 1, clickables.size()): + if clickables[i].hotspot.intersects(clickables[j].hotspot): + disjoint = false + pair = "%s×%s" % [clickables[i].key, clickables[j].key] + Kit.check("V14 七命中区两两不相交(首违例 %s)〔营地图批 V14〕" % pair, disjoint) + # 不越画面+不侵顶栏禁区 + var in_bounds := true + var offender := "" + for spec in clickables: + if spec.hotspot.position.x < 0.0 or spec.hotspot.position.y < table.hud_band_h \ + or spec.hotspot.end.x > table.canvas.x or spec.hotspot.end.y > table.canvas.y: + in_bounds = false + offender = String(spec.key) + Kit.check("V14 命中区不越画面且不侵顶栏禁区(y≥%d,违例 %s)〔营地图批 V14〕" % [table.hud_band_h, offender], in_bounds) + # 篝火命中只取下半:命中区贴贴图底、严格小于贴图、起点在贴图内(火焰上段不可点) + var fire: CampSceneLayerSpec = null + var gate: CampSceneLayerSpec = null + var board: CampSceneLayerSpec = null + var wall: CampSceneLayerSpec = null + for spec in table.layers: + match String(spec.key): + "campfire": fire = spec + "gate": gate = spec + "board": board = spec + "wall": wall = spec + var fire_ok := fire != null and fire.hotspot.end.y == fire.texture_rect.end.y \ + and fire.hotspot.size.y < fire.texture_rect.size.y \ + and fire.hotspot.position.y > fire.texture_rect.position.y + Kit.check("V14 篝火命中只取下半(火堆贴底、火焰不点)〔营地图批 V14〕", fire_ok) + # 门扇命中对齐门洞缺口(表内 gate_gap 字段关系谓词) + var gate_ok := gate != null and gate.hotspot.position.x == float(table.gate_gap.x) \ + and gate.hotspot.end.x == float(table.gate_gap.y) + Kit.check("V14 门扇命中对齐门洞缺口 %s〔营地图批 V14〕" % [table.gate_gap], gate_ok) + # 布告板挂围墙带:命中顶落在围墙贴图带内 + var board_ok := board != null and wall != null \ + and board.hotspot.position.y >= wall.texture_rect.position.y \ + and board.hotspot.position.y <= wall.texture_rect.end.y + Kit.check("V14 布告板命中挂围墙带〔营地图批 V14〕", board_ok) + # 层序结构:10 层且 bg/wall 压前两位(z0/z1——子节点序=表序先验) + var order_ok := table.layers.size() == 10 \ + and String(table.layers[0].key) == "bg" and String(table.layers[1].key) == "wall" + Kit.check("V14 层数=10 且 bg/wall 占 z0/z1〔营地图批 V14〕", order_ok) + # ConfigDB 装载链:表经单点注册进表现域容器且装载期校验过(fail-fast 钩子在位) + var db = load("res://core/config_db.gd").new() + var load_ok: bool = db.load_tables("res://data/tables", true) + Kit.check("V14 ConfigDB 装载含营地布局表且校验过(is_ready=%s)〔营地图批 V14〕" % db.is_ready, + load_ok and db.is_ready and db.presentation.camp_scene_layout != null) + quit(Kit.finish("camp_scene_layout_test")) diff --git a/game/tests/unit/presentation/camp_scene_layout_test.gd.uid b/game/tests/unit/presentation/camp_scene_layout_test.gd.uid new file mode 100644 index 00000000..0ea0b2dd --- /dev/null +++ b/game/tests/unit/presentation/camp_scene_layout_test.gd.uid @@ -0,0 +1 @@ +uid://ckyd57l11408 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 呈现侧,本段只管数据面+旧档载入显示