Files
gplan-2/game/presentation/ui_assembler.gd

238 lines
11 KiB
GDScript
Raw Permalink 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
## 视图构建装配器:只做 new/add_child/布局,静态无状态,返回句柄供组合根接线。
## 接线拓扑唯一归组合根——本类零信号连接。
## z 序契约:调用方必须按 build 返回序 addChild(登记序 = 模态遮挡序)。
extends RefCounted
# 战斗屏三区(B 战斗画面 / C·D 两列——节点链
# 进度由关卡画面本体(选路覆盖层整图)承载,28px 纵向空间拨给 C/D 底行 520→548;
# 无顶栏进度条、无道具面板区)。
# + 结算演出覆盖层(A–D 之上、模态之下——跨区弹道/光带载体)。
# 返回句柄字典:{stage, log_view, info_panel, board_zone, board_view, ap_bar, settlement_fx}
# (返回句柄不含 gp_label/keystone_label——GP/钥石不在战斗屏呈现)
static func build_battle_ui(root: Control, db: ConfigDb) -> Dictionary:
# set_anchors_and_offsets_preset(非 set_anchors_preset):后者会把控件重置回最小内容尺寸
# (实测 927×613——缩顶、棋盘 45px 格 ×10 行超出 D 区 333px 高被裁)
root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
root.add_theme_constant_override("separation", 0)
# B 区:战斗画面(全宽 × 252px——常驻双舞台,布局决策 D1)
var stage := BattleStageView.new(db)
root.add_child(stage)
# C/D 两列(548px——含顶栏让出的 28px 纵向空间)
var bottom := HBoxContainer.new()
bottom.size_flags_vertical = Control.SIZE_EXPAND_FILL
bottom.add_theme_constant_override("separation", 4)
root.add_child(bottom)
var info_panel := HeroInfoPanelView.new()
bottom.add_child(info_panel)
var board_zone := BoardZoneView.new()
bottom.add_child(board_zone)
board_zone.ap_bar.set_refresh_cost(int(db.run.action_economy.rap_refresh_cost)) # RAP 读数表值驱动
# 结算演出覆盖层(A–D 之上——z 序 = addChild 序,模态覆盖层组在 build_overlays 中更晚挂入)
var settlement_fx := SettlementFxView.new()
root.add_child(settlement_fx)
# CJK 字体随包(Fusion Pixel OFL):Control 侧默认主题字体走 project.godot
# gui/theme/custom_font;SubViewport 两世界不继承 Control 主题——显式注入挂点接线(C6 挂点链)。
# 缺位守卫(load_font 返回 null = 引擎回退字体,不炸)。
var ui_font: Font = UiKit.load_font(UiKit.FONT_UI)
if ui_font != null:
stage.set_world_font(ui_font)
board_zone.board_view.set_world_font(ui_font)
# GP/钥石余额位不在战斗屏(中/大循环资源迁出战斗屏——
# GP 去向 = 关卡画面+战斗结算画面、钥石去向 = 结算画面+营地,呈现归对应 UX spec)
return {
"stage": stage,
"log_view": stage.log_view,
"info_panel": info_panel,
"board_zone": board_zone,
"board_view": board_zone.board_view,
"ap_bar": board_zone.ap_bar,
"settlement_fx": settlement_fx,
}
# 模态覆盖层组(z 序 = 登记序:图例 < 节点 < 终局 < 商店)。
# 返回 {legend_overlay, node_overlay, end_overlay, shop_overlay};调用方按本序 addChild。
# (本组不含调试浮窗——由组合根直构挂树序最末,见 main._build_ui;
# 本组不含明细回看 overlay——冒险中回看归「生涯」页;
# 本组不含营地覆盖层——在 camp_screen 屏壳构建)
static func build_overlays(host: Control, db: ConfigDb) -> Dictionary:
var legend_overlay := LegendOverlay.new(int(db.run.action_economy.rap_refresh_cost)) # RAP 行表值注入
host.add_child(legend_overlay)
var node_overlay := NodeOverlay.new()
host.add_child(node_overlay)
var end_overlay := BattleEndOverlay.new()
host.add_child(end_overlay)
# 商店覆盖层(商店房节点兑付呈现;按钮接线归组合根,装配器零信号连接)
var shop_overlay := ShopOverlay.new()
host.add_child(shop_overlay)
return {
"legend_overlay": legend_overlay,
"node_overlay": node_overlay,
"end_overlay": end_overlay,
"shop_overlay": shop_overlay,
}
# 开始覆盖层(返回 {overlay, box, entersavebtn, settingsbtn, quitbtn}——按钮接线归组合根;
# 随机种子/数值调整板/两层调试三散件不在主按钮组,收进调试浮窗——
# 调试入口=UI 最顶层全局浮层角标(build_debug_badge,本层零调试入口)
# 布局=双列 VBox(左操作 56% + 右舞台);按钮动词化+悬停解释
#(解释全迁 tooltip,G5);entersavebtn 为主操作钮(烛光黄 CANDLE——主菜单三入口化唯一进度入口);右列=立绘看台。**界面禁玩法文字墙
#(教学内容归 Beta 帮助手册〔教学引导定稿 P4 丙〕)**;本层无玩法速览牌;
# tooltip 主题归全局主题单点(禁在 root 挂程序化 tooltip 主题)。
# 返回键集如首行所列,接线归组合根)
static func build_start_overlay(host: Control, db: ConfigDb = null) -> Dictionary:
var overlay := overlay_bg(host, UiKit.BG_BRIGHT)
# 石纹弱质感+斜光带+暗角(demo .tilebg 0.14/.beams/.vignette 三层;
# 石纹层用半透明贴图,非满铺不透。缺位不挂即回退平色)
var bd := UiKit.backdrop_tile()
if bd != null:
overlay.add_child(bd)
overlay.add_child(UiKit._beams_rect())
overlay.add_child(UiKit.vignette_rect())
# 双列骨架:左=操作列 56% / 右=舞台(立绘看台)
var cols := _start_columns(overlay)
var box := _start_left_column(cols)
var entersavebtn := UiKit.primary_button("进入存档") # 主操作钮(唯一进度入口)
entersavebtn.tooltip_text = "选择一个存档位进入(新档 / 进档 / 删档)"
box.add_child(entersavebtn)
var settingsbtn := UiKit.button("设置") # 最小设置面入口(行为零变化——契约 2)
settingsbtn.tooltip_text = "音量 / 全屏 / 减动效"
box.add_child(settingsbtn)
# 退出游戏与设置同层并行——只在一级菜单出现(行为零变化——契约 2)
var quitbtn := UiKit.button("退出游戏")
box.add_child(quitbtn)
# 调试入口收浮层角标;boot 菜单优先不开
# 浮窗,调试入口=UI 最顶层全局浮层角标(build_debug_badge——LAYER_CONTRACT 末位、
# ModalStack 登记外,任何 UI 不遮挡;本层不设屏内调试角标,全局浮层全场景承载)
_start_right_column(cols)
return {"overlay": overlay, "box": box,
"entersavebtn": entersavebtn,
"settingsbtn": settingsbtn, "quitbtn": quitbtn}
## 双列骨架域:左=操作列 56% / 右=舞台(立绘看台)——全屏锚+56px 四边衬距
static func _start_columns(overlay: Control) -> HBoxContainer:
var cols := HBoxContainer.new()
cols.set_anchors_preset(Control.PRESET_FULL_RECT)
cols.add_theme_constant_override("separation", 32)
var pad := MarginContainer.new()
pad.set_anchors_preset(Control.PRESET_FULL_RECT)
for side in ["margin_left", "margin_right", "margin_top", "margin_bottom"]:
pad.add_theme_constant_override(side, 56)
cols.add_child(pad)
overlay.add_child(cols)
return cols
## 左操作列域:标题+三入口(主菜单三入口化——营地交互重做卡②,用户裁决 1:
## 进入存档/设置/退出游戏;组帽双撤——冒险组帽组内零钮退化态不留、「营地与系统」帽指称失义,追认单留痕)
static func _start_left_column(cols: HBoxContainer) -> VBoxContainer:
var box := VBoxContainer.new()
box.name = "start_box"
box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
box.size_flags_stretch_ratio = 1.4
box.add_theme_constant_override("separation", 10)
(cols.get_child(0) as MarginContainer).add_child(box)
box.add_child(UiKit.label("Dungeon Sweeper",UiKit.FS_H1, true, UiKit.INK_ON_DEEP))
box.add_child(HSeparator.new())
return box
## 右列域:立绘看台(三张立绘 + 标题底座;界面禁玩法文字墙,故无速览牌)
static func _start_right_column(cols: HBoxContainer) -> void:
var right := VBoxContainer.new()
right.name = "start_right"
right.size_flags_horizontal = Control.SIZE_EXPAND_FILL
right.add_theme_constant_override("separation", 18)
right.alignment = BoxContainer.ALIGNMENT_CENTER
cols.add_child(right) # 右列挂 HBox 第二列(禁挂 pad——与左列同矩形叠放会破双列)
var stage := VBoxContainer.new()
stage.alignment = BoxContainer.ALIGNMENT_CENTER
var foe_row := HBoxContainer.new()
foe_row.alignment = BoxContainer.ALIGNMENT_CENTER
foe_row.add_theme_constant_override("separation", 36)
for cfg in [[UiKit.TEX_ENEMY_GOBLIN, 46, 96], [UiKit.TEX_HERO_PORTRAIT, 138, 144], [UiKit.TEX_ENEMY_OGRE_CHIEF, 80, 96]]:
foe_row.add_child(_start_portrait(cfg))
stage.add_child(foe_row)
var pedestal := Panel.new()
var ped_sb := UiKit.panel_flat(UiKit.TITLE_PEDESTAL, UiKit.BG_ROOT, 17, 2)
pedestal.add_theme_stylebox_override("panel", ped_sb)
pedestal.custom_minimum_size = Vector2(240, 22)
pedestal.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
stage.add_child(pedestal)
right.add_child(stage)
## 单立绘域(循环体提函数——像素过滤居中;敌影剪影退后 §6.1 立绘>背景)
static func _start_portrait(cfg: Array) -> TextureRect:
var tr := TextureRect.new()
var tex := UiKit.load_tex(cfg[0])
if tex != null:
tr.texture = tex
tr.custom_minimum_size = Vector2(cfg[1], cfg[2])
tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
if cfg[0] != UiKit.TEX_HERO_PORTRAIT:
tr.modulate = Color(0.7, 0.7, 0.7, 0.4)
return tr
# 调试浮层入口角标(UI 最顶层浮层,任何 UI 不遮挡)——main 直接子层、
# LAYER_CONTRACT 末位(float 类:只落树序、不注册 ModalStack——open() 遮挡记账只动登记层,
# 故永不被隐藏);右下角 16px(拇指区惯例);可见性归组合根(_debug_entry
# 门控落定后赋值);地图屏底栏 85px 常驻,offset 抬升本函数不处理(组合根 _sync_status_entry)。
static func build_debug_badge(host: Control) -> Button:
var badge := UiKit.button("调试", 0.0) # 角落小件豁免宽度基准(0=不设 min/seals——同状态角标)
badge.add_theme_font_size_override("font_size", UiKit.FS_HINT)
badge.tooltip_text = "打开调试浮窗(当局数据/种子/数值调整)"
badge.grow_horizontal = Control.GROW_DIRECTION_BEGIN # 向左上生长——尺寸变化向屏内
badge.grow_vertical = Control.GROW_DIRECTION_BEGIN
badge.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_RIGHT, Control.PRESET_MODE_MINSIZE, 16)
badge.visible = false # 显式初始化——门控落定后由组合根置位
return badge
# 调试两层模式的层间过渡覆盖层(历史入口专用)。返回 {overlay, label, next_btn}——接线归组合根
static func build_inter_overlay(host: Control) -> Dictionary:
var overlay := overlay_bg(host, Color(0, 0, 0, 0.6))
var box := overlay_box(overlay)
var label := UiKit.label("",UiKit.FS_TITLE, true, UiKit.WARM)
box.add_child(label)
var next_btn := UiKit.button("进入下一层")
box.add_child(next_btn)
overlay.visible = false
return {"overlay": overlay, "label": label, "next_btn": next_btn}
static func overlay_bg(host: Control, color: Color) -> ColorRect:
var dim := ColorRect.new()
dim.color = color
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
host.add_child(dim)
return dim
static func overlay_box(overlay: Control) -> VBoxContainer:
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
overlay.add_child(center)
var box := VBoxContainer.new()
box.add_theme_constant_override("separation", 12)
center.add_child(box)
return box