iter47-卡2:卡④(迭代47·营地场景图新建批): 十层素材落地+CampSceneView场景图+layout表fail-fast+四柜台拆解五子界面+大门篝火动线+断言迁移批+审计补差——CampOverlay退役全仓零残留
This commit is contained in:
1 parent
0761071b37
commit
bbeeea77e0
45 files changed
+1614
-822
No files matched your search
@@ -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 束)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
# 调试浮窗(主菜单三散件收一:随机种子/数值调整/批量模拟/两层调试入口;
|
||||
|
||||
@@ -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)
|
||||
@@ -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 # 奖励格图鉴子界面(占位挂起态)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -1 +0,0 @@
|
||||
uid://dvg2wmjh87p8a
|
||||
@@ -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) # 现开界面就地重刷(未开的零开销——留底形态下场景根恒开)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cdfmyxf87n6f6
|
||||
@@ -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 = &""
|
||||
@@ -0,0 +1 @@
|
||||
uid://btsrctv5ddx16
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dv0gyjyrtoxkh
|
||||
@@ -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)) # 内容挂起——余额徽随刷(占位态无行板)
|
||||
@@ -0,0 +1 @@
|
||||
uid://m1bcfjiceyiq
|
||||
@@ -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()
|
||||
@@ -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}
|
||||
@@ -0,0 +1 @@
|
||||
uid://b77xh0e3txmh5
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dcakccwlacegq
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://mjjrdelhi7yg
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user