iter47-卡2:卡④(迭代47·营地场景图新建批): 十层素材落地+CampSceneView场景图+layout表fail-fast+四柜台拆解五子界面+大门篝火动线+断言迁移批+审计补差——CampOverlay退役全仓零残留

This commit is contained in:
DKChan committed 2026-11-01 23:44:29 +08:00
1 parent 0761071b37
commit bbeeea77e0
45 files changed
+1614 -822

No files matched your search

+238
View File
@@ -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)