- 四view新件(自CampOverlay承载面迁移,界面逻辑复用零重设计契约4): forge_view(钥匙门+两区六路线行板)/treasure_view(12件行板+同调装配段+Y升级行+钥匙门)/tree_view(脉分组行板+Fork双态)/codex_view(占位挂起壳,锁文随形态订正『柜台』称谓L2)——各带返回钮发closed(关层归presenter);公共壳CampSubViewBase(实底dim+铭牌+钥石徽+返回钮) - CampRefreshBus新件: 各写口尾→现开界面刷新+场景根HUD一闸(卡⑤HUD同口挂接;非autoload非信号广播——组合根单点推送);presenter八写口尾refresh_camp→bus.refresh(F9:七口+on_camp_buy双尾) - 承载形态(§9-2拍板): 单一内页切换=场景根留底——模态栈open只隐上方登记层(modal_stack.gd:27实测),四子界面登记于场景根之上且实底遮挡,双开互斥靠遮挡天然成立;V2断言按留底派生(T5落) - ui_registry: LAYER_CONTRACT camp_overlay槽→camp_scene(shell camp_screen)+forge/treasure/tree/codex_view四槽;node()改经modals.camp.X二层(卡⑥契约2);ui_camp_modals五枚成员(overlay枚随退役删除——卡⑥契约4) - camp_screen: 持camp_scene取代CampOverlay成员;main_ui_build: 场景装配(布局表ConfigDB注入)+四view实例化+信号接线(意图→presenter/closed bind携视图/object_pressed→组合根路由) - presenter承接面: 五目标改线(ctx.views束——成员预算12≤15)+open_camp_forge/treasure/tree/codex四开层口+on_camp_sub_closed+on_camp_closed关层收闭(营地层与选档层同收+开始层恢复);main_overlay_flow._on_camp_object_pressed五物件路由(gate/campfire归T4)+生涯营地语境关层分支(笔记本改挂——CareerOverlay本体零改);main.gd薄委托零新增成员 - 退役: camp_overlay_view.gd删除(吸收即死);旧形态断言同批摘除(test-conventions §5.5):theme_projection等高源码断言+最近冒险行投影段(卡面削弱申报授权:注销,条带Q4=甲撤下) - 同批迁移: forge_workbench_test承载类camp_overlay_view→forge_view+_forge._X扁平化(断言零改12项绿);system_test三处机械随迁(V2实例化/:108同一性链/:121候选清单——camp_scene句柄更名) - 验证: 冒烟boot PASS(15项0失败——装配全链通)+全流程孤检绿(12项:五句柄/铺层/点选路由四子界面/返回/生涯改挂/营地语境分支/关层收闭——临时件不入库);S9/S10/S13归基线(codex头注日期清偿/_build_shell超局部变量按顶栏底栏拆)
240 lines
12 KiB
GDScript
240 lines
12 KiB
GDScript
## 秘宝匣子界面(自 CampOverlay 秘宝匣柜台抽出——承载面迁移,界面逻辑复用零重设计):
|
||
## 钥匙门(树上 ks_workshop 点亮前锁定态;点亮=12 件行板+同调装配段+Y 刷新升级行整体迁入)。
|
||
## UI 只发意图信号(购买写口唯一归 MetaProgression.buy_treasure_unlock / toggle_attunement / buy_attune_refresh)。
|
||
class_name TreasureView
|
||
extends CampSubViewBase
|
||
|
||
signal buy_requested(attr: String) ## 秘宝购买/Y 刷新升级意图(presenter 按 attr 分派)
|
||
signal attune_toggled(item_id: String) ## 同调装配切换意图(写口唯一归 MetaProgression.toggle_attunement)
|
||
|
||
const HEADER_BAND_COL := UiKit.CAMP_HEADER_BAND
|
||
const ROW_PLATE_COL := UiKit.CAMP_ROW_PLATE
|
||
|
||
var _ws_lock_l: Label # 锁定态行(ks_workshop 未点亮)
|
||
var _ws_scroll: ScrollContainer # 秘宝匣内容容器(钥匙点亮后可见)
|
||
var _workshop_holder: VBoxContainer # 秘宝匣行板(六行+同调装配段+Y 升级行;探针直读冻结)
|
||
var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l, buy_btn, price, unlocked, card}](探针消费)
|
||
var _attune_header_l: Label # 同调装配段(Slots 点亮呈现)
|
||
var _attune_btns: Array[Dictionary] = [] # [{item_id, btn, unlocked, equipped}]——装配/卸下切换钮(满位禁选随刷)
|
||
var _attune_nodes: Array[Control] = [] # 段内节点册(open 重建时定向清理,不扰工坊行板)
|
||
var _refresh_l: Label # Y 刷新升级行(双入口之一——树上 ks_refresh 同账)
|
||
var _refresh_btn: Button
|
||
|
||
|
||
func _init() -> void:
|
||
var holder := VBoxContainer.new()
|
||
holder.add_theme_constant_override("separation", 6)
|
||
holder.custom_minimum_size = Vector2(0, 336)
|
||
_ws_lock_l = UiKit.label("🔒 秘宝匣未开放\n\n点亮树上「秘宝匣」钥匙节点后展开。\n(前置=脊柱生命第 1 级)", UiKit.FS_HINT, false, UiKit.STONE_DEEP)
|
||
_ws_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||
_ws_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
_ws_lock_l.custom_minimum_size = Vector2(280, 0)
|
||
_ws_scroll = ScrollContainer.new()
|
||
_ws_scroll.custom_minimum_size = Vector2(300, 336)
|
||
_ws_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
_ws_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||
_workshop_holder = VBoxContainer.new()
|
||
_workshop_holder.add_theme_constant_override("separation", 6)
|
||
_ws_scroll.add_child(_workshop_holder)
|
||
holder.add_child(_ws_lock_l)
|
||
holder.add_child(_ws_scroll)
|
||
var scroll := ScrollContainer.new()
|
||
scroll.custom_minimum_size = Vector2(300, 336)
|
||
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||
scroll.add_child(holder)
|
||
_build_shell("秘宝匣", UiKit.TEX_ICON_WORKSHOP, scroll)
|
||
|
||
|
||
func open_camp(data: Dictionary) -> void:
|
||
_rebuild_workshop(data) # 行板+同调装配段+Y 尾行随开层全量重建(段尾链 _rebuild_attune)
|
||
super.open_camp(data)
|
||
|
||
|
||
func refresh_camp(data: Dictionary) -> void:
|
||
var balance := int(data.get("balance", 0))
|
||
_balance_label.text = "钥石 %d" % balance
|
||
_refresh_key_gate(data)
|
||
_refresh_workshop_rows(data, balance)
|
||
_refresh_attune_segment(data, balance)
|
||
|
||
|
||
# 秘宝匣钥匙门(树上 ks_workshop 行 level 判据——零核心访问,纯视图读投影)
|
||
func _refresh_key_gate(data: Dictionary) -> void:
|
||
var lvl := 0
|
||
for row in (data.get("tree", []) as Array):
|
||
if String(row.get("node_id", "")) == "ks_workshop":
|
||
lvl = int(row.get("level", 0))
|
||
var open := lvl >= 1
|
||
_ws_lock_l.visible = not open
|
||
_ws_scroll.visible = open
|
||
|
||
|
||
func _rebuild_workshop(data: Dictionary) -> void:
|
||
_workshop_rows.clear()
|
||
for child in _workshop_holder.get_children():
|
||
child.queue_free()
|
||
_attune_nodes.clear()
|
||
_attune_btns.clear()
|
||
_attune_header_l = null
|
||
_refresh_l = null
|
||
_refresh_btn = null
|
||
for row in data.get("workshop", []):
|
||
var card := _workshop_card(row)
|
||
_workshop_holder.add_child(card["card"])
|
||
_workshop_rows.append(card)
|
||
_rebuild_attune(data)
|
||
|
||
|
||
## 秘宝匣行板构建域:件名(分档后缀+效果摘要悬停)+进度列+状态列+钥石购买钮(购买件未解锁在列)
|
||
func _workshop_card(row: Dictionary) -> Dictionary:
|
||
var card := PanelContainer.new()
|
||
card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4)))
|
||
var line := HBoxContainer.new()
|
||
line.add_theme_constant_override("separation", 12)
|
||
card.add_child(line)
|
||
var tier_name := String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), ""))
|
||
var name_l := UiKit.label(String(row.get("display_name", "")) + ("·" + tier_name if tier_name != "" else ""), UiKit.FS_HINT, true, UiKit.INK)
|
||
name_l.custom_minimum_size = Vector2(132, 0)
|
||
name_l.clip_text = true
|
||
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
name_l.tooltip_text = String(row.get("desc", "")) # 件效果摘要悬停可见(分档呈现契约——槽位档位=名后缀)
|
||
line.add_child(name_l)
|
||
var progress_l := UiKit.label("", UiKit.FS_HINT, false, UiKit.STONE_DEEP)
|
||
progress_l.custom_minimum_size = Vector2(150, 0)
|
||
progress_l.clip_text = true # 营地溢出修——文字全宽不撑柜
|
||
progress_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
line.add_child(progress_l)
|
||
# 状态色用亮板双色墨制(深墨族替代暖白族)——色随刷新态条件赋值,非机械两行式
|
||
var status_l := UiKit.label("", UiKit.FS_HINT, true, UiKit.STONE_DEEP)
|
||
status_l.clip_text = true # 「已解锁 · 能力封存中」不撑柜(余宽吸收者)
|
||
status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
line.add_child(status_l)
|
||
# 钥石购买钮(购买件且未解锁时在列;写口唯一=buy_treasure_unlock,
|
||
# 经 buy_requested(item_id) 信号走 presenter 分支;禁用态=余额不足)
|
||
var buy_btn: Button = null
|
||
if not bool(row.get("unlocked", false)) and String(row.get("mode", "")) == "keystone":
|
||
buy_btn = UiKit.button("购买 %d" % int(row.get("price", 0)), 0.0) # 行板紧凑钮豁免
|
||
buy_btn.pressed.connect(_emit_buy_requested.bind(String(row.get("item_id", ""))))
|
||
line.add_child(buy_btn)
|
||
return {"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l, "buy_btn": buy_btn, "price": int(row.get("price", 0)), "unlocked": bool(row.get("unlocked", false)), "card": card}
|
||
|
||
|
||
# 同调装配段重建(秘宝匣尾部段:Slots 头行+九件行板+Y 升级尾行)
|
||
func _rebuild_attune(data: Dictionary) -> void:
|
||
var att: Dictionary[Variant, Variant] = data.get("attune", {})
|
||
_attune_header_l = UiKit.label("", UiKit.FS_HINT, true, UiKit.INK)
|
||
_workshop_holder.add_child(_attune_header_l)
|
||
_attune_nodes.append(_attune_header_l)
|
||
for row in att.get("items", []):
|
||
var line := _attune_item_line(row)
|
||
_workshop_holder.add_child(line)
|
||
_attune_nodes.append(line)
|
||
_refresh_attune_tail()
|
||
|
||
|
||
## 同调件行板构建域:件名+形态+档位+装配切换钮(未解锁禁装——toggle 写口同守卫)
|
||
func _attune_item_line(row: Dictionary) -> HBoxContainer:
|
||
var iid := String(row.get("item_id", ""))
|
||
var line := HBoxContainer.new()
|
||
line.add_theme_constant_override("separation", 6)
|
||
var name_l := UiKit.label(String(row.get("display_name", "")), UiKit.FS_HINT, true, UiKit.INK)
|
||
name_l.custom_minimum_size = Vector2(96, 0)
|
||
name_l.clip_text = true
|
||
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
line.add_child(name_l)
|
||
var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能", UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)
|
||
form_l.custom_minimum_size = Vector2(58, 0)
|
||
line.add_child(form_l)
|
||
# 档位标签(槽位档位显示:件档=普通/优秀/稀有,装入须件档≤格档契约 2 的呈现半边)
|
||
var tier_l := UiKit.label(String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "—")), UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)
|
||
tier_l.custom_minimum_size = Vector2(36, 0)
|
||
line.add_child(tier_l)
|
||
var btn := UiKit.button("装配", 0.0) # 行板紧凑钮豁免
|
||
btn.pressed.connect(attune_toggled.emit.bind(iid))
|
||
btn.disabled = not bool(row.get("unlocked", false)) # 未解锁禁装(解锁为装配前置——toggle 写口同守卫)
|
||
line.add_child(btn)
|
||
_attune_btns.append({"item_id": iid, "btn": btn, "unlocked": bool(row.get("unlocked", false)), "equipped": bool(row.get("equipped", false))})
|
||
return line
|
||
|
||
|
||
## Y 升级尾行构建域(双入口之一——树上 ks_refresh 同账)
|
||
func _refresh_attune_tail() -> void:
|
||
var refresh_line := HBoxContainer.new()
|
||
refresh_line.add_theme_constant_override("separation", 6)
|
||
_refresh_l = UiKit.label("", UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)
|
||
refresh_line.add_child(_refresh_l)
|
||
_refresh_btn = UiKit.button("升级", 0.0) # 行板紧凑钮豁免
|
||
_refresh_btn.pressed.connect(_emit_buy_requested.bind("attune_refresh"))
|
||
refresh_line.add_child(_refresh_btn)
|
||
_workshop_holder.add_child(refresh_line)
|
||
_attune_nodes.append(refresh_line)
|
||
|
||
|
||
## 秘宝匣行板刷新域:进度列(解锁混合制三形态)与状态色(亮板双色墨制)与购买钮余额联动
|
||
func _refresh_workshop_rows(data: Dictionary, balance: int) -> void:
|
||
var ws_by_id := {}
|
||
for row in data.get("workshop", []):
|
||
ws_by_id[String(row.get("item_id", ""))] = row
|
||
for card in _workshop_rows:
|
||
var row: Dictionary[Variant, Variant] = ws_by_id.get(String(card.item_id), {})
|
||
if row.is_empty():
|
||
continue
|
||
var unlocked := bool(row.get("unlocked", false))
|
||
card.progress_l.text = _workshop_progress_text(row, unlocked)
|
||
var st_text := "未解锁"
|
||
var st_col: Color = UiKit.STONE_DEEP
|
||
if unlocked:
|
||
st_text = "已解锁"
|
||
st_col = UiKit.GOLD_INK # GOLD_DEEP 对行板 3.00:1 越带,换深金墨 5.84:1
|
||
card.status_l.text = st_text
|
||
card.status_l.add_theme_color_override("font_color", st_col)
|
||
card.status_l.tooltip_text = st_text # 截断兜底(状态列 EXPAND_FILL 吸余宽——悬停看全文)
|
||
# 购买钮余额联动(禁用态=余额不足,常态防线;pattern 1 兜底拒绝可见归 presenter)
|
||
if card.get("buy_btn") != null:
|
||
card.buy_btn.disabled = balance < int(card.get("price", 0))
|
||
|
||
|
||
## 进度列文案域(解锁混合制三形态:钥石购买价/条件进度拼接/已入手)
|
||
func _workshop_progress_text(row: Dictionary, unlocked: bool) -> String:
|
||
if unlocked:
|
||
return "已入手"
|
||
var mode := String(row.get("mode", ""))
|
||
if mode == "keystone":
|
||
return "钥石 %d" % int(row.get("price", 0))
|
||
if mode == "condition":
|
||
var parts: Array[String] = []
|
||
for req_v in (row.get("reqs", []) as Array):
|
||
var req: Dictionary[Variant, Variant] = req_v as Dictionary
|
||
var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件"))
|
||
parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))])
|
||
return "、".join(parts)
|
||
return ""
|
||
|
||
|
||
## 同调装配段刷新域(Slots 点亮/满位禁选/Y 升级行余额联动——就地刷)
|
||
func _refresh_attune_segment(data: Dictionary, balance: int) -> void:
|
||
var att: Dictionary[Variant, Variant] = data.get("attune", {})
|
||
if _attune_header_l != null:
|
||
var slots_n := int(att.get("slots", 0))
|
||
var eq: Array[Variant] = att.get("equipped", [])
|
||
_attune_header_l.text = "── 同调装配 Slots %d/%d ──" % [eq.size(), slots_n]
|
||
var eq_set := {}
|
||
for v in eq:
|
||
eq_set[String(v)] = true
|
||
for card2 in _attune_btns:
|
||
var on: bool = eq_set.has(String(card2.item_id))
|
||
card2.btn.text = "卸下" if on else "装配"
|
||
# 满位禁选+未解锁禁装(已装件恒可卸下;unlocked 快照随 rebuild 刷新)
|
||
card2.btn.disabled = (not on and eq.size() >= slots_n) or (not on and not bool(card2.get("unlocked", false)))
|
||
if _refresh_l != null:
|
||
var rf: Dictionary[Variant, Variant] = att.get("refresh", {})
|
||
_refresh_l.text = "三选一刷新 +%d 次/冒险 · Lv%d" % [int(rf.get("charges", 0)), int(rf.get("level", 0))]
|
||
_refresh_btn.text = "已满级" if bool(rf.get("maxed", false)) else "升级 %d 钥石" % int(rf.get("price", 0))
|
||
_refresh_btn.disabled = bool(rf.get("maxed", false)) or balance < int(rf.get("price", 0))
|
||
|
||
|
||
func _emit_buy_requested(attr: String) -> void:
|
||
buy_requested.emit(attr)
|