Files
gplan-2/game/presentation/views/camp_overlay_view.gd
T
DKChan a6a2bf885e 迭代11第①批:魔法道具同调实施批 T0-T10 全落(重闸 645/0 + MC off 逐位 PASS)
- T1 表与schema:attunement_growth.tres 谓词表(m1/m2/m3+Σgrant≤slot_cap 校验+slots() 求值口径)+道具表扩列(form/charges/slot_cost/unlock_requires/effective_rounds/charge_n+effect_kinds 九kind+KEY_WHITELIST 零裸flat护栏)+七同调行
- T2 save v4(boss_kill_count 收束入账+attunement_equipped+attune_refresh_level+断点 attunement_charges+迁移链 v3→v4)
- T3 Attunement 求值服务(纯函数零 await 零 RNG)
- T4 营地装配覆盖层(工坊扩容程序占位+Y 升级行 attune_refresh_upgrade 行族)
- T5 被动六件接线+装配注入链+刷新 X/Y 双源(BonusPickOverlay 刷新钮,autoplay 默认不刷)
- T6 掘进蓄能全链+摆钟能力面(骑库存面差额注入+断点充能键);炸格 P5 甲影响评估破面大→用户裁拆后批,blast_kit deferred 留位
- T7 退役面五件(BONUS_IDS 除名 aux_item+transitional_sync_slots 过渡键退役=迭代10欠账③清偿+授予路/四容量闸拆除+nest_pendulum 换装 active_charged)
- T8 冒烟六新断言(645/0)+MC attune 开关+贪心画像分层 off/on 各 N=60 双策略(on 画像功率显著带平移归调参批;off 新锚=对拍正主留盘主区 sim-data,81000 降批入口基准)
- T9 连带修订九件(魔法道具定位重写/元进度v1.6/地图战斗资源v0.16/行动经济v0.7/05不做v1.3/关卡节点地图v1.4/node-reward v2.2/registry 三处/06 收口+炸格拆后批新条)
- T10 重闸全绿:冒烟 645/0+熵增 PASS+doc 三闸零新增+MC off 复跑同种子逐位一致 PASS;出口人闸后移迭代 12(用户 2026-10-03 令)
2026-10-03 15:41:30 +08:00

345 lines
16 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VERTICAL SLICE - NOT FOR PRODUCTION
# Date: 2026-09-21(元进度实现批——营地覆盖层呈现;2026-09-30 迭代 9 卡⑭ 视觉终稿——对照营地屏
# 方向稿 artwork/mockups/v3-draft-2026-09-29/4-camp.png 重排:石纹全屏底 + 顶栏铭牌/钥石徽 +
# 三柜台列(属性锻造/道具工坊/奖励格图鉴,头带深棕+米色板身+内嵌行板,暗底→亮板双色墨制)+
# 底栏关闭钮/操作注记;最近冒险收底栏条带。信号面/投影契约零变化:buy_requested(attr)/closed、
# open_camp(data)/refresh_camp(data)、_forge_rows 行结构 {attr,level_l,price_l,btn}(探针依赖)。
# 挂接方式:开合与开始层互斥仿 TunePanel 先例(开始覆盖层非模态且树序在后——打开本层前由组合根
# 隐藏开始层,规范 2.3 遮挡纪律)。
# 高内聚边界:纯视图 setter 面——账本快照经组合根喂入(MetaProgression 只读投影),购买/关闭
# 意图经信号外发——零核心写调用、零 MetaProgression 持有。
class_name CampOverlay
extends Control
signal buy_requested(attr: String)
signal closed
signal attune_toggled(item_id: String) # 同调装配切换意图(迭代 11 第①批 T4——写口唯一归 MetaProgression.toggle_attunement)
const ATTR_LABELS := {"atk": "攻击", "max_hp": "生命上限", "armor_class": "护甲"} # 档位文案轴(数据契约 attr 词表,与 ShopOverlay 同款)
# 方向稿取色(4-camp.png——柜台头带深棕/行板暖米;双色墨=亮板配深墨)——值单点收 UiKit(T4 裸色收口批)
const HEADER_BAND_COL := UiKit.CAMP_HEADER_BAND
const ROW_PLATE_COL := UiKit.CAMP_ROW_PLATE
const BADGE_PANEL_COL := UiKit.CAMP_BADGE_PANEL
var _balance_label: Label
var _forge_holder: VBoxContainer
var _workshop_holder: VBoxContainer
var _forge_rows: Array[Dictionary] = [] # [{attr, level_l, price_l, btn}]——refresh_camp 就地重刷(零重建回流)
var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l}]
var _attune_header_l: Label # 同调装配段(迭代 11 第①批 T4 工坊扩容——Slots 点亮呈现)
var _attune_btns: Array[Dictionary] = [] # [{item_id, btn}]——装配/卸下切换钮(满位禁选随刷)
var _attune_nodes: Array[Control] = [] # 段内节点册(open_camp 重建时定向清理,不扰工坊六行)
var _refresh_l: Label # Y 刷新升级行(P1 甲——每冒险开局给 Y 次、冒险周期池)
var _refresh_btn: Button
var _recent_list: Label # 最近冒险(逐行拼接 label——只读账面,随 open_camp 重建)
func _init() -> void:
var dim := UiKit.modal_dim(self, Color(0, 0, 0, 0.45)) # 方向稿=石纹底透出(浅 dim——石纹全屏底担当氛围)
# 石纹底噪(样张 v2 冻结件——start overlay 同款;缺位不挂即回退平 dim)
var bd_tex := UiKit.load_tex(UiKit.TEX_BACKDROP_STONE)
if bd_tex != null:
var bd := TextureRect.new()
bd.texture = bd_tex
bd.stretch_mode = TextureRect.STRETCH_TILE
bd.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
bd.set_anchors_preset(Control.PRESET_FULL_RECT)
bd.mouse_filter = Control.MOUSE_FILTER_IGNORE
dim.add_child(bd)
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)
# ── 顶栏:左铭牌(标题+副题)/ 右钥石徽 ──
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, 2, 1, Vector4(14, 8, 14, 8)))
top.add_child(plaque)
var plaque_col := VBoxContainer.new()
plaque.add_child(plaque_col)
var title := UiKit.label("营地 · 永久强化", 22, true, UiKit.INK_TITLE)
plaque_col.add_child(title)
var sub := UiKit.label("跨局成长账本——钥石换永久属性", 13, false, UiKit.STONE)
plaque_col.add_child(sub)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
top.add_child(spacer)
var badge := PanelContainer.new()
badge.add_theme_stylebox_override("panel",
UiKit.panel_flat(BADGE_PANEL_COL, UiKit.PANEL_BORDER, 2, 1, Vector4(14, 8, 14, 8)))
top.add_child(badge)
var bal_row := HBoxContainer.new()
bal_row.add_theme_constant_override("separation", 6)
badge.add_child(bal_row)
var keystone_icon := UiKit.icon_rect(UiKit.TEX_ICON_KEYSTONE, 18) # 缺位 null → 纯文本回退
if keystone_icon != null:
bal_row.add_child(keystone_icon)
_balance_label = UiKit.label("钥石 0", 18, true, UiKit.GOLD)
bal_row.add_child(_balance_label)
# ── 三柜台列(方向稿同构:头带深棕+米色板身)──
var mid := HBoxContainer.new()
mid.add_theme_constant_override("separation", 18)
mid.alignment = BoxContainer.ALIGNMENT_CENTER
mid.size_flags_vertical = Control.SIZE_EXPAND_FILL
col.add_child(mid)
_forge_holder = VBoxContainer.new()
_forge_holder.add_theme_constant_override("separation", 6)
mid.add_child(_counter_panel("属性锻造", UiKit.TEX_ICON_FORGE, _forge_holder, 356))
# 迭代 11 第①批 T4 工坊扩容(P2 甲程序占位——同调装配段+Y 升级行入工坊柜台,行板复用;行数增长以
# 柜台内滚承载,冻结三柜台版式与另两柜台零触碰,视觉终稿归营地批)
var ws_scroll := ScrollContainer.new()
ws_scroll.custom_minimum_size = Vector2(340, 336)
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)
mid.add_child(_counter_panel("道具工坊", UiKit.TEX_ICON_WORKSHOP, ws_scroll, 356))
var atlas_holder := VBoxContainer.new()
mid.add_child(_counter_panel("奖励格图鉴", UiKit.TEX_ICON_ATLAS, atlas_holder, 356))
var lock_l := UiKit.label(" —— 柜台结构位预留:内容排 Alpha(地图战斗资源差异化批)", 13, false, UiKit.STONE)
lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
lock_l.custom_minimum_size = Vector2(300, 0)
atlas_holder.add_child(lock_l)
# ── 底栏:关闭钮(左)/ 操作注记(右)+ 最近冒险条带 ──
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("每购得一件永久强化,对应柜台亮起一格暖光 · 价格为钥石", 13, false, UiKit.WARM_SOFT)
bottom.add_child(caption)
col.add_child(bottom)
_recent_list = UiKit.label("", 13, false, UiKit.WARM_SOFT)
_recent_list.clip_text = true
_recent_list.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
col.add_child(_recent_list)
visible = false
# 柜台列面板(方向稿同构):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, 3, 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, 2, 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, 16) # 缺位 null → 纯文本回退
if ic != null:
head.add_child(ic)
var l := UiKit.label(title_text, 16, true, UiKit.WARM)
head.add_child(l)
vbox.add_child(holder)
return panel
# ---- 柜台行构建(open_camp 重建;行结构仿 ShopOverlay 商品卡——亮板双色墨制)----
# 打开营地(全量重建三柜台行 + 同调装配段 + 最近冒险——数据 = 组合根 _camp_data() 投影)
func open_camp(data: Dictionary) -> void:
_rebuild_forge(data)
_rebuild_workshop(data)
_rebuild_attune(data)
_rebuild_recent(data)
refresh_camp(data)
visible = true
# 状态重刷(购买后调用——余额/价格/禁用态/工坊状态就地改文案,零重建)
func refresh_camp(data: Dictionary) -> void:
var balance := int(data.get("balance", 0))
_balance_label.text = "钥石 %d" % balance
var forge_by_attr := {}
for row in data.get("forge", []):
forge_by_attr[String(row.get("attr", ""))] = row
for card in _forge_rows:
var row: Dictionary = forge_by_attr.get(String(card.attr), {})
if row.is_empty():
continue
card.level_l.text = "Lv %d" % int(row.get("level", 0))
card.price_l.text = "%d 钥石" % int(row.get("price", 0))
card.btn.disabled = balance < int(row.get("price", 0)) # 余额不足禁用置灰
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 = ws_by_id.get(String(card.item_id), {})
if row.is_empty():
continue
var th := int(row.get("threshold", 0))
var prog := int(row.get("progress", 0))
var unlocked := bool(row.get("unlocked", false))
card.progress_l.text = "累计通关 %d / %d 次" % [prog, th]
# 迭代 7 T5——失义件(显影尘/地穴全图卷 Alpha 重定)与退役件(探宝罗盘 2026-09-29 拍板①乙)呈现口径;
# 状态色随卡⑭ 亮板双色墨制(深墨族替代暖白族——亮板上暖白不可读)
var st_text := "未解锁"
var st_col: Color = UiKit.STONE
if bool(row.get("retired", false)):
st_text = "已退役"
st_col = UiKit.STONE
elif bool(row.get("deferred", false)) and unlocked:
st_text = "已解锁 · 能力封存中"
st_col = UiKit.GOLD_DEEP
elif unlocked:
st_text = "已解锁"
st_col = UiKit.GOLD_DEEP
card.status_l.text = st_text
card.status_l.add_theme_color_override("font_color", st_col)
# 同调装配段状态就地刷(迭代 11 第①批 T4——Slots 点亮/满位禁选/Y 升级行余额联动)
var att: Dictionary = data.get("attune", {})
if _attune_header_l != null:
var slots_n := int(att.get("slots", 0))
var eq: Array = 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 "装配"
card2.btn.disabled = (not on) and eq.size() >= slots_n # 满位禁选
if _refresh_l != null:
var rf: Dictionary = 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 _rebuild_forge(data: Dictionary) -> void:
_forge_rows.clear()
for child in _forge_holder.get_children():
child.queue_free()
for row in data.get("forge", []):
_forge_rows.append(_build_forge_row(_forge_holder, row))
func _build_forge_row(holder: VBoxContainer, row: Dictionary) -> Dictionary:
var card := PanelContainer.new()
card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER, 2, 1, Vector4(10, 4, 10, 4)))
var line := HBoxContainer.new()
line.add_theme_constant_override("separation", 12)
card.add_child(line)
var attr := String(row.get("attr", ""))
var name_l := UiKit.label("%s %+d/级" % [String(row.get("display_name", "")), int(row.get("flat", 0))], 14, true, UiKit.INK)
name_l.custom_minimum_size = Vector2(132, 0)
line.add_child(name_l)
var level_l := UiKit.label("", 14, false, UiKit.INK)
level_l.custom_minimum_size = Vector2(44, 0)
line.add_child(level_l)
var price_l := UiKit.label("", 14, false, UiKit.STONE)
price_l.custom_minimum_size = Vector2(64, 0)
line.add_child(price_l)
var btn := UiKit.button("锻造 +1 级")
btn.pressed.connect(_emit_buy_requested.bind(attr))
line.add_child(btn)
holder.add_child(card)
return {"attr": attr, "level_l": level_l, "price_l": price_l, "btn": btn}
func _rebuild_workshop(data: Dictionary) -> void:
_workshop_rows.clear()
for child in _workshop_holder.get_children():
child.queue_free()
for row in data.get("workshop", []):
var card := PanelContainer.new()
card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER, 2, 1, Vector4(10, 4, 10, 4)))
var line := HBoxContainer.new()
line.add_theme_constant_override("separation", 12)
card.add_child(line)
var name_l := UiKit.label(String(row.get("display_name", "")), 14, true, UiKit.INK)
name_l.custom_minimum_size = Vector2(132, 0)
line.add_child(name_l)
var progress_l := UiKit.label("", 13, false, UiKit.STONE)
progress_l.custom_minimum_size = Vector2(110, 0)
line.add_child(progress_l)
# 状态色随卡⑭ 亮板双色墨制(深墨族替代暖白族)——色随刷新态条件赋值,非机械两行式(T4 收口保留)
var status_l := UiKit.label("", 13, true, UiKit.STONE)
line.add_child(status_l)
_workshop_holder.add_child(card)
_workshop_rows.append({"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l})
# 同调装配段重建(迭代 11 第①批 T4——工坊柜台尾部追加;Slots 点亮+九件行板+Y 升级行,程序占位面)
func _rebuild_attune(data: Dictionary) -> void:
for n in _attune_nodes:
n.queue_free()
_attune_nodes.clear()
_attune_btns.clear()
var att: Dictionary = data.get("attune", {})
_attune_header_l = UiKit.label("", 13, true, UiKit.INK)
_workshop_holder.add_child(_attune_header_l)
_attune_nodes.append(_attune_header_l)
for row in att.get("items", []):
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", "")), 13, true, UiKit.INK)
name_l.custom_minimum_size = Vector2(96, 0)
line.add_child(name_l)
var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能", 12, false, UiKit.STONE)
form_l.custom_minimum_size = Vector2(58, 0)
line.add_child(form_l)
var btn := UiKit.button("装配")
btn.pressed.connect(attune_toggled.emit.bind(iid))
line.add_child(btn)
_workshop_holder.add_child(line)
_attune_nodes.append(line)
_attune_btns.append({"item_id": iid, "btn": btn})
var refresh_line := HBoxContainer.new()
refresh_line.add_theme_constant_override("separation", 6)
_refresh_l = UiKit.label("", 12, false, UiKit.STONE)
refresh_line.add_child(_refresh_l)
_refresh_btn = UiKit.button("升级")
_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 _rebuild_recent(data: Dictionary) -> void:
var lines: Array[String] = []
var recent: Array = 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))])
_recent_list.text = " | ".join(lines)
func _emit_closed() -> void:
closed.emit()
func _emit_buy_requested(attr: String) -> void:
buy_requested.emit(attr)