Files
gplan-2/game/presentation/views/ui_kit.gd
T

554 lines
32 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-11(2026-09-20 样式收敛批:全站色板与 StyleBox 工厂单点化)
# 呈现层共享小部件工厂 + 全站取色/描边唯一出口——各视图禁再内联 Color8/StyleBoxFlat
# 字面量;美术批换皮只改本文件(值 = 收敛时各视图现值快照,本批零视觉变化)。
class_name UiKit
extends RefCounted
# ── 色板(语义族;值 = 2026-09-20 收敛时现值) ──
const INK := Color8(51, 38, 27) # 墨族——正文/数字(对格底 ≥4.5:1)
const INK_COLD := Color8(19, 46, 54) # 青墨 1-3 #132E36(数字墨三档·冷=稀疏——样张 v2 定稿 2026-09-28;亮瓦自适应深墨族档兼色牌后备路径)
const INK_OCHRE := Color8(60, 37, 12) # 赭墨 4-5 #3C250C(数字墨三档·中;亮瓦自适应深墨族档兼色牌后备路径)
const INK_RUST := Color8(74, 30, 20) # 锈墨 6-8 #4A1E14(数字墨三档·暖=密集;亮瓦自适应深墨族档兼色牌后备路径)
# ── 亮墨三档(圣经 v3.5 冻结④ 2026-09-30——数字直画已开格底:色温承袭密度语义,冷亮/暖白/金亮;
# 实测对比 6.0–6.3:1(A3a);金亮低饱和避奖励金语义;色牌后备组用深墨族上牌〔12:1〕。
# 2026-10-01 修订:亮墨族限暗纹理板(dungeon),亮筋纹板按 _digit_ink_mode 自适应切深墨三档)──
const INK_L_COLD := Color8(216, 236, 240) # 冷亮 1-3 #D8ECF0
const INK_L_WARM := Color8(240, 226, 200) # 暖白 4-5 #F0E2C8(关卡屏 WARM 同族)
const INK_L_GOLD := Color8(248, 227, 168) # 金亮 6-8 #F8E3A8
const INK_LABEL := Color8(46, 34, 24) # 标签深墨
const INK_TITLE := Color8(30, 26, 22) # 亮底标题墨
const INK_ROUTE := Color8(24, 20, 16) # 节点图最深墨
const INK_ON_GOLD := Color8(60, 44, 20) # 金底上的深墨
const PANEL_DATA := Color8(226, 210, 182) # 面板底·数据框(AP 条/状态槽)
const PANEL_BORDER := Color8(120, 106, 88) # 面板细线框;兼熄灭点阵(DOT_OFF 别名)
const GOLD := Color8(240, 168, 40) # 奖励金
const GOLD_DEEP := Color8(176, 122, 20) # 深金(金框描边/达意金)
const GOLD_INK := Color8(116, 80, 11) # 深金墨(意图蓄力徽标墨——PANEL_DATA 垫牌上 4.9:1,UI 设计对齐批 N1 2026-10-06)
const GOLD_LIT := Color8(230, 190, 90) # 亮金(候选高亮)
const GOLD_DIM := Color8(140, 116, 60) # 暗金(已过节点)
const GOLD_DONE := Color8(104, 86, 44) # 已过节点暗金·关卡画面呈现态(关卡画面样张 v1 冻结新值 #68562C——旧 GOLD_DIM #8C743C 对双色墨均 <4.5:1 对比硬指标逼守;GOLD_DIM 他用不动)
const SEAL_BLACK := Color8(0, 0, 0) # 封印黑 #000000——封印(插旗)宣告色(2026-09-28
# 用户拍板旗朱→封印紫→封印黑:紫色与微光蓝族色相冲突,黑色无色相天然避开禁入带
# 妨害紫 281° 各留 12-14° 间距;候选值样张批冻结;旗朱 #C63D22 退役)
const RED_DEEP := Color8(150, 60, 48) # 深朱(敌卡框/敌像)
const RED_MINE := Color8(150, 44, 36) # 引爆雷字朱(已开雷格"雷"字)
const RED_FORBID := Color8(120, 60, 50) # 禁止框朱
const RED_ALERT := Color8(200, 60, 48) # 报错朱
const HERO_BLUE := Color8(46, 134, 222) # 小人蓝 #2E86DE(样张 v2 定稿 2026-09-28——圣经 §4.1 候选值冻结;旧 #2E6E8E 收敛期快照退役)
const HERO_BLUE_DEEP := Color8(30, 90, 150) # 小人蓝同族深档(意图增益徽标墨——PANEL_DATA 垫牌上 4.8:1;命名同 CONTACT_GLOW_DEEP 规则,UI 设计对齐批 N1 2026-10-06)
const BOSS_PURPLE := Color8(143, 47, 106) # 紫赤(Boss 专属)
const DEBUFF_PURPLE := Color8(122, 66, 148) # 妨害紫(意图 ☠)
# ── 类型色五族(UI 设计对齐批 N4c 2026-10-06——值 = demo --tg-* 冻结抄录〔任务书 §2.2 抄录表〕;
# 前四族 = 实机现值搬运、第五族群伤 demo 值补族——实机原完全缺位,消费面 = tg_band() 同源判定)──
const TG_BUFF := Color8(194, 178, 100) # 通用增益/默认带
const TG_HEAL := Color8(102, 164, 87) # 回复
const TG_MAGIC := Color8(138, 114, 196) # 魔法
const TG_PIERCE := Color8(87, 160, 160) # 穿甲
const TG_AOE := Color8(226, 86, 118) # 群伤/AOE #E25676(demo --tg-aoe)
# 类型色带分族判定(N4c/Q4 甲——三选一与商店货架同源同判:id/kind 小写子串匹配,默认=通用增益带;
# blast=爆裂群伤归 TG_AOE(reward.tres 现成 clear_blast/spark_aoe 消费面);max_hp 含 hp 归回复族)
static func tg_band(id_lower: String) -> Color:
if id_lower.contains("heal") or id_lower.contains("hp"):
return TG_HEAL
if id_lower.contains("aoe") or id_lower.contains("blast"):
return TG_AOE
if id_lower.contains("magic"):
return TG_MAGIC
if id_lower.contains("pierce"):
return TG_PIERCE
return TG_BUFF
# ── 呈现散件裸色搬运(UI 设计对齐批 N4a 2026-10-06——原值搬运零观感;本节立后 ui_kit 之外
# 呈现层裸 Color8 = 0,守卫 = ui_contrast_probe 静态段)──
const INK_HINT := Color8(110, 69, 52) # 提示/警示暖棕墨(设置警示文/地图底栏组标)
const INK_ON_DEEP := Color8(246, 238, 218) # 深底标题亮墨(主菜单标题/营地柜台等级章字)
const TITLE_PEDESTAL := Color8(90, 74, 56) # 标题屏底座石台面(椭圆 17 底座;描边=BG_ROOT)
const MAP_BAR_PLATE := Color8(240, 228, 200) # 关卡地图底栏亮板
const MAP_VDIV := Color8(201, 184, 142) # 关卡地图底栏竖分隔线(1px)
const HINT_STRIP_EDGE := Color8(74, 62, 48) # 棋盘底提示条描边(半透明罩层板边)
const RAP_DOT_CORE_WARM := Color8(248, 226, 168) # AP 点阵亮心(烛光黄族)
const RAP_DOT_LIT_BLUE := Color8(105, 161, 232) # RAP 点阵亮面(小人蓝族)
const RAP_DOT_CORE_BLUE := Color8(156, 194, 240) # RAP 点阵亮心(小人蓝族)
const E_FACADE_FOOT_COL := Color8(52, 42, 32) # 墙脚阴线(方向稿同色——原 board_cell_unit 局部)
const FOG_MEMORY_PLATE := Color8(58, 58, 58) # 记忆板 #3A3A3A(§6.5——原 board_cell_unit 局部)
const FOG_MEMORY_INK := Color8(196, 196, 196) # 记忆墨 #C4C4C4(对记忆板 ≈6.3:1——原局部)
const FOG_OUTLINE := Color8(30, 24, 18) # 雾面轮廓墨(原 board_cell_unit _draw 局部 oc)
const ICON_TINT_ON_GOLD := Color(24.0 / 240.0, 20.0 / 226.0, 16.0 / 200.0) # 当前金底图标墨 = WARM 分量乘法调制(双色墨制——原 level_map_screen 局部)
const DUSK := Color8(90, 108, 140) # 暮蓝(全作唯一暮蓝档——失败终局)
const WARM := Color8(240, 226, 200) # 暖白(暗底正文)
const WARM_SOFT := Color8(232, 213, 168) # 暖白次级(说明文)#E8D5A8(M0 换代——灰米 #C8BCA6 退役,配色总纲「米色去灰」)
const WARM_FAINT := Color8(205, 184, 140) # 暖白弱化(帮助文)#CDB88C(M0 换代,同上)
const STONE := Color8(120, 110, 98) # 灰石(通道/箭头/失效)
const STONE_FAINT := Color8(140, 124, 100) # 灰石弱化(占位提示文)
const STONE_DEEP := Color8(85, 77, 65) # 灰石深化(「已用」槽位墨——PANEL_DATA 槽牌 5.6:1 / 石皮 5.0:1,UI 设计对齐批 N1 2026-10-06;旧 STONE_FAINT 对亮底双向 <4.5:1)
const DARK_UPCOMING := Color8(64, 58, 52) # 未达暗字(底/文同值)
const BRICK := Color8(201, 175, 131) # 棋盘·未开掩尘地砖 #C9AF83
const FLOOR := Color8(179, 154, 120) # 棋盘·已开裸土 #B39A78
const CONTACT_GLOW := Color8(214, 236, 255) # 可挖域接触带微光(蓝族浅一档·近白蓝——art-bible §3.2 v2.8 定稿;
# 旧绿相 DIG_EDGE 随画法切换退役 2026-09-26 批)
const CONTACT_GLOW_DEEP := Color8(150, 190, 230) # 接触带呼吸暗端(lerp 目标)
const DIGIT_PLATE := Color8(245, 234, 211) # 数字色牌(素色暖纸白直角牌——art-bible §4.1;数字一律垫牌,
# 可读不依赖格底——原则二;2026-09-26 呈现修复批落码)
const DIGIT_PLATE_EDGE := Color8(210, 198, 172) # 色牌牌缘线
const SLOT_BACKDROP := Color8(52, 44, 36) # B 区状态图标槽实底(深墨棕——白图标对比承载,2026-09-26 反馈④)
const BG_ROOT := Color8(58, 46, 34) # 屏幕根底 #3A2E22(UI 落码批 M0 2026-10-04——配色总纲「提暖提亮禁近黑」,旧近黑 #1E1914 退役)
const BG_OVERLAY := Color8(54, 43, 32) # 模态遮罩底 #362B20(同批换代,旧 #1C1612 退役)
const BG_BRIGHT := Color8(69, 55, 35) # 暖亮档底 #453723(M0 新增——标题/营地/商店等暖亮屏根底)
const CANDLE := Color8(246, 198, 103) # 烛光黄 #F6C667(M0 新增——主操作色/候选节点/钥石徽章底;
# 与 §4.1 奖励金 #F0A828 分轨——圣经 §7.2 琥珀双岗注记)
const DIM := Color(0.08, 0.06, 0.05) # 遮罩基色(用时乘 alpha)
const TOAST_BG := Color(0.94, 0.91, 0.84, 0.95) # toast 面底
const TIP_BG := Color(0.16, 0.12, 0.09, 0.96) # tooltip 深底
# 呈现杂色档(T4 裸色收口批 2026-10-02——各视图裸 Color8 字面量单点化;值 = 收口时各来源行现值快照)
const STAGE_BG := Color8(62, 52, 42) # B 区舞台暗底(方向稿 render_battle_screen——battle_stage_view 面板底)
const BEAM_LIGHT := Color8(255, 236, 200) # B 区顶光柱暖光(用时乘 alpha 渐隐——battle_stage_view 光柱层)
const STAND_STONE := Color8(150, 130, 100) # 站桩石板地台·板身(standee_body 方向稿取色)
const STAND_STONE_EDGE := Color8(90, 76, 58) # 站桩石板地台·描边
const STAND_STONE_RIM := Color8(196, 172, 132) # 站桩石板地台·顶缘受光线(board_cell_unit 口沿高光已并档至此——清扫批 2026-10-02)
const CAMP_HEADER_BAND := Color8(122, 82, 50) # 营地柜台头带深棕(4-camp.png 方向稿取色)
const CAMP_ROW_PLATE := Color8(240, 230, 206) # 营地行板暖米(同上)
const CAMP_BADGE_PANEL := Color8(40, 32, 24) # 营地钥石徽暗底(同上)
const HP_BAR_BACK := Color8(40, 34, 28) # 迷你血条深底(关卡画面样张 v1 冻结——对暗面板可辨)
# BBCode 十六进制(RichTextLabel 用;与上同值)
const HEX_GOLD := "f0a828"
const HEX_INK := "33261b"
const HEX_ON_GOLD := "3a2a08"
# 结算行/叙事行语义档(T4 收口批——battle_log_view 行格式化与 narrator 横幅行的裸 hex 单点化;
# 名随呈现语义、值 = 2026-09 起各行现值快照,色值逐字节不变)
const HEX_LOG_ROUND := "9a8a72" # 轮次前缀/零值行暖灰石
const HEX_LOG_GAIN := "e0b860" # 收益金(GP 袋/商店行/败判定/节点横幅)
const HEX_LOG_LOSS := "c47c7c" # 损失红(GP 流失/负效果)
const HEX_LOG_HEAL := "7cc47c" # 治疗绿(效果正值/治疗行/恢复点行)
const HEX_LOG_DMG_NUM := "d9402a" # 伤害数字朱(放大加色——数字即信息主角)
const HEX_LOG_DMG_KILL := "e07050" # 击杀行正文橙红
const HEX_LOG_DMG_BODY := "e0a050" # 伤害行正文橙
const HEX_LOG_VOIDED := "b08060" # 未生效行暗橙(⚠ 划线)
const HEX_LOG_CAST := "b09a72" # 施放行暖石(✦ 具名技能)
const HEX_LOG_DECAY := "8a9ab0" # 状态衰减蓝灰
const HEX_LOG_WIN := "80d080" # 胜利判定绿(模拟报告通关率同档)
const HEX_ROUTE_NOTE := "d0c090" # 奖励点预告暗金(narrator 行)
const HEX_SEED_NOTE := "c8a860" # 种子/开场行暗金(narrator 行)
# 语义别名(同值异名——语义站点用别名,值仍单点)
const DOT_OFF := PANEL_BORDER
# ── 减动效(battle.md §Transitions 内建承诺:开启时时长减半;结算逐行兑现为信息载体降速不跳过)──
# 唯一入口 = 设置面开关(迭代 8 卡④ 转正;切片期调整板开关已随其退役,调整板本体 14.5 收进调试浮窗)
static var motion_scale := 1.0 # 1.0 常态 / 0.5 减动效
static var victory_fx_style := "crest" # 胜利演出风格(P-C5 两套并存——"crest"碎砖拼纹章[默认冻结件]/"dust"尘金粒子;切换=调试浮窗,会话级)
# 全部呈现层 tween 时长经本函数包装——新增动效一律 UiKit.motion(秒),禁再写裸时长字面量
static func motion(dur: float) -> float:
return dur * motion_scale
# ── 单位/特效纹理路径单点(棋盘小人贴图批 2026-09-25)──
# 素材 PNG 不入 git(用户裁定)——装载必须经 load_tex(exists 守卫),缺位返回 null 走各消费点
# 的字形/无演出回退路径;禁 preload(素材缺位树上直接断言炸脚本,冒烟安全红线)
const TEX_HERO_IDLE := "res://assets/art/character/character_hero_idle.png"
const TEX_HERO_WALK := "res://assets/art/character/character_hero_walk.png"
const TEX_HERO_PORTRAIT := "res://assets/art/character/character_hero_portrait.png" # B 区站桩立绘·normal 态(48px 档,迭代 6 T3 换装=new-normal 源)
const TEX_HERO_ATTACK := "res://assets/art/character/character_hero_attack.png" # B 区站桩立绘·attack 态(迭代 6 T3 三态切换)
const TEX_HERO_HIT := "res://assets/art/character/character_hero_hit.png" # B 区站桩立绘·hit 态(迭代 6 T3 三态切换)
# 敌种立绘(B 区站桩 2026-09-26 槽接线批)——文件键 = 数据表主键 enemy_id(圣经 §8.3 命名规范),
# 键集 = art-bible §4.6.4 八敌种;素材批入库前全部缺位(load_tex→null→字形回退),入库即显示零代码换装
const TEX_ENEMY_GOBLIN := "res://assets/art/character/character_enemy_goblin.png" # 地精
const TEX_ENEMY_BUGBEAR := "res://assets/art/character/character_enemy_bugbear.png" # 熊地精
const TEX_ENEMY_OGRE_CHIEF := "res://assets/art/character/character_enemy_ogre_chief.png" # 食人魔酋长
const TEX_ENEMY_GIANT_SPIDER := "res://assets/art/character/character_enemy_giant_spider.png" # 巨蜘蛛
const TEX_ENEMY_DRIDER := "res://assets/art/character/character_enemy_drider.png" # 蛛化卓尔精灵
const TEX_ENEMY_DUERGAR := "res://assets/art/character/character_enemy_duergar.png" # 灰矮人
const TEX_ENEMY_HOOK_HORROR := "res://assets/art/character/character_enemy_hook_horror.png" # 钩爪恐魔
const TEX_ENEMY_DUERGAR_KING := "res://assets/art/character/character_enemy_duergar_king.png" # 灰矮人石君王
# 框材组纹理槽(样张 v2 冻结批 T2 2026-09-28——♻ 复用件量化改色入包,产物 = tools/quantize_ui_skin.py,
# 全 CC0;缺位 = load_tex null → 程序化回退原路径,冒烟安全)
const TEX_PANEL_STONE := "res://assets/art/ui/panel_stone.png" # 石板亮面板九宫格 48×48
const TEX_BUTTON_STONE := "res://assets/art/ui/button_stone.png" # 按钮九宫格·常态(可按的砖)
const TEX_BUTTON_STONE_PRESSED := "res://assets/art/ui/button_stone_pressed.png" # 按钮九宫格·按压(下沉一档)
const TEX_BACKDROP_STONE := "res://assets/art/ui/backdrop_stone.png" # 石纹底噪 256×256 无缝
const TEX_BORDER_STONE := "res://assets/art/ui/border_stone.png" # 仪式石框(空心角括框)
const TEX_ICON_KEYSTONE := "res://assets/art/ui/icon_keystone.png" # 钥石图标(奖励金)
const TEX_ICON_FORGE := "res://assets/art/ui/icon_forge.png" # 柜台一·属性锻造
const TEX_ICON_WORKSHOP := "res://assets/art/ui/icon_workshop.png" # 柜台二·道具工坊
const TEX_ICON_ATLAS := "res://assets/art/ui/icon_atlas.png" # 柜台三·奖励格图鉴
# 节点图标五键(关卡画面样张 v1 冻结批 M-2 迭代 4 T2——20px 烘焙 1:1 消费,WARM 单色;
# 对象键 = node_type 文件侧延伸 §8.3;缺位 = load_tex null → 字形回退)
const TEX_NODE_BATTLE := "res://assets/art/ui/node_battle.png" # 节点图标·战斗房
const TEX_NODE_REWARD := "res://assets/art/ui/node_reward.png" # 节点图标·奖励房
const TEX_NODE_REST := "res://assets/art/ui/node_rest.png" # 节点图标·恢复房
const TEX_NODE_BOSS := "res://assets/art/ui/node_boss.png" # 节点图标·Boss 房
const TEX_NODE_SHOP := "res://assets/art/ui/node_shop.png" # 节点图标·商店房
# 手绘功能图标十键(UI 落码批 M1 2026-10-04——20×20 像素 1:1/整数倍消费〔§8.2 甲类纪律〕;
# 源=demo 原型 src 盘上件搬运;缺位 = load_tex null → 字形回退不炸)
const TEX_ICON_SWORD := "res://assets/art/ui/icon_sword.png" # 图标·攻击系(锻造行/攻击强化卡)
const TEX_ICON_SHIELD := "res://assets/art/ui/icon_shield.png" # 图标·护甲系
const TEX_ICON_HEART := "res://assets/art/ui/icon_heart.png" # 图标·生命系
const TEX_ICON_SHOVEL := "res://assets/art/ui/icon_shovel.png" # 图标·铲(挖掘系商品/锻造)
const TEX_ICON_MEDKIT := "res://assets/art/ui/icon_medkit.png" # 图标·医疗包
const TEX_ICON_POUCH := "res://assets/art/ui/icon_pouch.png" # 图标·弹药袋/行动点
const TEX_ICON_BLAST := "res://assets/art/ui/icon_blast.png" # 图标·工兵爆破(秘宝)
const TEX_ICON_COINBAG := "res://assets/art/ui/icon_coinbag.png" # 图标·金币袋
const TEX_ICON_KEYSHARD := "res://assets/art/ui/icon_keyshard.png" # 图标·钥石残片
const TEX_ICON_REFRESH := "res://assets/art/ui/icon_refresh.png" # 图标·刷新(货架/候选)
# 素材纹理守卫装载(exists 检查避开缺位树上 ResourceLoader 报错刷屏——冒烟/无素材检出静默回退)
static func load_tex(path: String) -> Texture2D:
if ResourceLoader.exists(path):
return load(path)
return null
# CJK 随包字体(2026-09-27 用户拍板 Fusion Pixel OFL——CJK 覆盖+授权干净;命名遵 art-bible §8.3
# 全小写 snake_case 域前缀)。守卫装载同 load_tex 纪律:缺位返回 null(引擎回退字体接手,不炸)。
const FONT_UI := "res://assets/fonts/ui_font_fusion_pixel_zh_hans.ttf"
static func load_font(path: String) -> Font:
if ResourceLoader.exists(path):
var res: Resource = load(path)
return res as Font
return null
# ── StyleBox 工厂 ──
# margin = (左, 上, 右, 下);border_w = 0 即无边框(根底/遮罩用)
static func panel_flat(bg: Color, border: Color, corner: int = 0, border_w: int = 1, margin: Vector4 = Vector4.ZERO) -> StyleBoxFlat:
var sb := StyleBoxFlat.new()
sb.bg_color = bg
sb.set_corner_radius_all(corner)
sb.set_border_width_all(border_w)
sb.border_color = border
sb.content_margin_left = margin.x
sb.content_margin_top = margin.y
sb.content_margin_right = margin.z
sb.content_margin_bottom = margin.w
return sb
# color 缺省哨兵 = alpha<0(真实用色不取负 alpha)——传色即一次到位(旧「label() 后跟
# add_theme_color_override」两行式收单行);省略时保持旧默认:bold=INK_LABEL / 非 bold=主题缺省
static func label(text: String, size: int, bold: bool, color: Color = Color(-1, -1, -1, -1)) -> Label:
var l := Label.new()
l.text = text
l.add_theme_font_size_override("font_size", size)
if color.a >= 0.0:
l.add_theme_color_override("font_color", color)
elif bold:
l.add_theme_color_override("font_color", INK_LABEL)
return l
# ── 模态壳工厂(T4 收口批 2026-10-02——各 overlay 手搓 dim+Center+VBox/Panel 骨架单点化)──
# dim = 全屏半透明遮罩(全屏锚定纪律同既有各 overlay);center 全屏锚定居中;with_panel = 内容包
# PanelContainer(panel_sb 缺省 = 主题默认板——settlement_review 同款);box = 内容列
#(min_size/separation 参数化,min_size ZERO = 不设)。返回 Dictionary{dim, center, panel, box}
#——探针直读的私有件由调用方按名摘引,结构同名同层级不变。
static func modal_dim(parent: Control, dim_color: Color) -> ColorRect:
var dim := ColorRect.new()
dim.color = dim_color
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
parent.add_child(dim)
return dim
static func modal_shell(parent: Control, dim_color: Color, min_size := Vector2.ZERO, separation := 12,
with_panel := false, panel_sb: StyleBox = null) -> Dictionary:
var dim := modal_dim(parent, dim_color)
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
dim.add_child(center)
var panel: PanelContainer = null
var host: Control = center
if with_panel:
panel = PanelContainer.new()
if panel_sb != null:
panel.add_theme_stylebox_override("panel", panel_sb)
center.add_child(panel)
host = panel
var box := VBoxContainer.new()
if min_size != Vector2.ZERO:
box.custom_minimum_size = min_size
box.add_theme_constant_override("separation", separation)
host.add_child(box)
return {"dim": dim, "center": center, "panel": panel, "box": box}
# 属性百分比合成(main._debug_hero_override 与调试浮窗生效值预览共用单点——两处同式实现收口)
static func scaled(base: int, pct: int) -> int:
return maxi(1, int(round(float(base) * (100.0 + float(pct)) / 100.0)))
# 宽度纪律(ui-struct-probe 批 T3——全站按钮最小宽度基准 216+SIZE_FILL:VBox 列内拉满列宽等宽,
# HBox 行内不抢余宽(EXPAND_FILL 会把行撑爆——营地底条 1616px 实证);角落小件/紧凑位传 0 豁免)
static func button(text: String, min_w := 216.0) -> Button:
var b := Button.new()
b.text = text
if min_w > 0.0:
b.custom_minimum_size = Vector2(min_w, 0)
b.size_flags_horizontal = Control.SIZE_FILL
_apply_button_skin(b)
return b
# ── 框材纹理工厂(样张 v2 冻结批 T2——§7.1 容器穿戏服、数据素面;缺位回退 flat/主题默认) ──
# 九宫格 StyleBoxTexture(texture_margin 8px——quantize 脚本同值);tint 供消费点微调色调
static func _tex_sb(tex: Texture2D, margin: float, tint := Color.WHITE) -> StyleBoxTexture:
var sb := StyleBoxTexture.new()
sb.texture = tex
sb.texture_margin_left = margin
sb.texture_margin_top = margin
sb.texture_margin_right = margin
sb.texture_margin_bottom = margin
sb.modulate_color = tint
return sb
# 石板亮面板(B 区舞台/C 区主面板/图例——容器穿戏服;数据框仍 panel_flat 素面 §7.1)
static func panel_skinned(margin: Vector4 = Vector4.ZERO, tint := Color.WHITE) -> StyleBox:
var tex := load_tex(TEX_PANEL_STONE)
if tex == null:
return panel_flat(PANEL_DATA, PANEL_BORDER, 4, 1, margin)
var sb := _tex_sb(tex, 8.0, tint)
sb.content_margin_left = margin.x
sb.content_margin_top = margin.y
sb.content_margin_right = margin.z
sb.content_margin_bottom = margin.w
return sb
# 仪式石框(空心角括框——营地容器框;缺位回退暗面板)
static func frame_skinned(margin: Vector4 = Vector4.ZERO) -> StyleBox:
var tex := load_tex(TEX_BORDER_STONE)
if tex == null:
return panel_flat(BG_OVERLAY, PANEL_BORDER, 6, 1, margin)
var sb := _tex_sb(tex, 12.0)
sb.content_margin_left = margin.x
sb.content_margin_top = margin.y
sb.content_margin_right = margin.z
sb.content_margin_bottom = margin.w
return sb
# 图标槽(营地钥石/柜台件——缺位返回 null,消费点不挂 TextureRect 即回退纯文本)
static func icon_rect(tex_path: String, px: float) -> TextureRect:
var t := load_tex(tex_path)
if t == null:
return null
var tr := TextureRect.new()
tr.texture = t
tr.custom_minimum_size = Vector2(px, px)
tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
return tr
# 按钮皮(常态/按压两态+墨字——缺位 = 引擎默认主题,素材批前逐位一致)
static func _apply_button_skin(b: Button) -> void:
var tex_n := load_tex(TEX_BUTTON_STONE)
if tex_n == null:
return
var tex_p := load_tex(TEX_BUTTON_STONE_PRESSED)
b.add_theme_stylebox_override("normal", _tex_sb(tex_n, 8.0))
b.add_theme_stylebox_override("hover", _tex_sb(tex_n, 8.0))
if tex_p != null:
b.add_theme_stylebox_override("pressed", _tex_sb(tex_p, 8.0))
b.add_theme_stylebox_override("disabled", _tex_sb(tex_p, 8.0))
b.add_theme_color_override("font_color", INK)
b.add_theme_color_override("font_hover_color", INK)
b.add_theme_color_override("font_pressed_color", INK)
b.add_theme_color_override("font_focus_color", INK)
b.add_theme_color_override("font_disabled_color", Color(INK, 0.45))
# ── UI 落码批 M10 公共件(2026-10-04——bcard 竖卡工厂/拨杆开关/样式滑杆/动词按钮/tooltip 主题;
# 消费方 = M2 标题/M3 营地/M4 设置/M6 商店/M7 三选一。单点纪律同 §7.1:新件只加不改旧件)──
# 动词按钮(图标+动词+悬停说明三件套——M2 标题屏按钮组/通用;icon 缺位=纯文字不炸)
static func verb_button(text: String, icon_path: String, tip := "") -> Button:
var b := button(text)
var tex := load_tex(icon_path)
if tex != null:
b.icon = tex
b.add_theme_constant_override("icon_max_width", 20)
b.expand_icon = false
if tip != "":
b.tooltip_text = tip
return b
# 主操作钮(烛光黄平涂+木描边——非奖励金;配色总纲「主按钮禁亮金」;缺 panel_stone 依赖=纯色可靠)
static func primary_button(text: String, tip := "") -> Button:
var b := button(text)
var sb := panel_flat(CANDLE, Color8(110, 69, 52), 5, 2, Vector4(10, 2, 10, 2))
b.add_theme_stylebox_override("normal", sb)
b.add_theme_stylebox_override("hover", panel_flat(CANDLE.lightened(0.06), Color8(110, 69, 52), 5, 2, Vector4(10, 2, 10, 2)))
b.add_theme_stylebox_override("pressed", panel_flat(Color8(214, 172, 88), Color8(110, 69, 52), 5, 2, Vector4(10, 2, 10, 2)))
b.add_theme_color_override("font_color", Color8(58, 42, 8))
b.add_theme_color_override("font_hover_color", Color8(58, 42, 8))
b.add_theme_color_override("font_pressed_color", Color8(58, 42, 8))
b.add_theme_color_override("font_focus_color", Color8(58, 42, 8))
if tip != "":
b.tooltip_text = tip
return b
# 危险钮(深红平涂——放弃/删除类;破坏性确认位置纪律=不得与常规确认同位)
static func danger_button(text: String, tip := "") -> Button:
var b := button(text)
var sb := panel_flat(Color8(180, 78, 54), Color8(110, 42, 28), 5, 2, Vector4(10, 2, 10, 2))
b.add_theme_stylebox_override("normal", sb)
b.add_theme_stylebox_override("hover", panel_flat(Color8(196, 90, 62), Color8(110, 42, 28), 5, 2, Vector4(10, 2, 10, 2)))
b.add_theme_color_override("font_color", Color8(255, 244, 224))
b.add_theme_color_override("font_hover_color", Color8(255, 244, 224))
b.add_theme_color_override("font_pressed_color", Color8(255, 244, 224))
if tip != "":
b.tooltip_text = tip
return b
# bcard 竖卡工厂(商店/三选一同构卡——M6/M7 共用;M7.1 差异仅=价签 vs ✓选择章)
# 返回 {panel, mark, band, icon, name_l, desc_l, price_l, foot_l};set_selected 经 mark/样式换装
class BCard:
extends PanelContainer
var mark: Label
var icon_tr: TextureRect
var name_l: Label
var desc_l: Label
var price_l: Label
var foot_l: Label
var body: VBoxContainer # 内容列(消费方经 add_footer 插件到底注前——M6 购买钮挂位)
var _sb_normal: StyleBox
var _sb_sel: StyleBox
func _init(title: String, desc: String, band: Color, icon_path: String, price := "", footnote := "") -> void:
var tex := UiKit.load_tex(UiKit.TEX_PANEL_STONE)
_sb_normal = UiKit.panel_skinned(Vector4(12, 10, 12, 10)) if tex != null else UiKit.panel_flat(Color8(233, 217, 184), UiKit.PANEL_BORDER, 5, 1, Vector4(12, 10, 12, 10))
_sb_sel = UiKit.panel_flat(Color8(233, 217, 184), Color8(138, 90, 52), 5, 2, Vector4(12, 10, 12, 10))
add_theme_stylebox_override("panel", _sb_normal)
custom_minimum_size = Vector2(216, 300)
var box := VBoxContainer.new()
box.add_theme_constant_override("separation", 8)
add_child(box)
body = box
var band_r := ColorRect.new()
band_r.color = band
band_r.custom_minimum_size = Vector2(0, 6)
box.add_child(band_r)
mark = UiKit.label("✓", 16, true, Color8(58, 42, 8))
mark.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
mark.visible = false
box.add_child(mark)
icon_tr = TextureRect.new()
var itex := UiKit.load_tex(icon_path)
if itex != null:
icon_tr.texture = itex
icon_tr.custom_minimum_size = Vector2(56, 56)
icon_tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon_tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon_tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
icon_tr.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
box.add_child(icon_tr)
name_l = UiKit.label(title, 18, true, INK_TITLE)
name_l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
box.add_child(name_l)
desc_l = UiKit.label(desc, 14, false, Color8(74, 58, 40))
desc_l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
desc_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
box.add_child(desc_l)
var spacer := Control.new()
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
box.add_child(spacer)
price_l = UiKit.label(price, 21, true, GOLD)
price_l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
price_l.visible = price != ""
box.add_child(price_l)
foot_l = UiKit.label(footnote, 11, false, Color8(110, 69, 52))
foot_l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
foot_l.visible = footnote != ""
box.add_child(foot_l)
func set_selected(v: bool) -> void:
add_theme_stylebox_override("panel", _sb_sel if v else _sb_normal)
mark.visible = v
func add_footer(c: Control) -> void:
body.add_child(c)
body.move_child(c, body.get_child_index(foot_l))
static func bcard(title: String, desc: String, band: Color, icon_path: String, price := "", footnote := "") -> BCard:
return BCard.new(title, desc, band, icon_path, price, footnote)
# 拨杆开关(设置屏——46×22 滑柄形;状态字随行由消费方摆;会话级/持久由消费方接 SettingsStore)
class SwitchToggle:
extends Button
var _knob: ColorRect
func _init(initial: bool) -> void:
toggle_mode = true
button_pressed = initial
custom_minimum_size = Vector2(46, 22)
var track := UiKit.panel_flat(Color8(184, 165, 130), Color8(120, 110, 88), 3, 2)
add_theme_stylebox_override("normal", track)
add_theme_stylebox_override("hover", track)
add_theme_stylebox_override("pressed", track)
_knob = ColorRect.new()
_knob.color = Color8(58, 42, 8)
_knob.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_knob)
toggled.connect(func(_p: bool) -> void: _place())
_place()
func _place() -> void:
_knob.position = Vector2(25, 3) if button_pressed else Vector2(3, 3)
_knob.size = Vector2(18, 16)
static func switch_toggle(initial: bool) -> SwitchToggle:
return SwitchToggle.new(initial)
# 样式滑杆(设置屏音量——轨道暖石+默认柄;范围/读数由消费方接)
static func styled_slider(value: float, minv := 0.0, maxv := 100.0) -> HSlider:
var s := HSlider.new()
s.min_value = minv
s.max_value = maxv
s.value = value
s.custom_minimum_size = Vector2(200, 20)
s.add_theme_stylebox_override("slider", panel_flat(Color8(184, 165, 130), PANEL_BORDER, 0, 1))
s.add_theme_stylebox_override("grabber_area", panel_flat(Color8(122, 82, 50), Color8(0, 0, 0, 0), 0, 0))
return s
# tooltip 全站主题(深底多行素牌——§7.2 文字可读硬指标;消费方=任一祖先 Control.theme 挂一次全树生效)
static var _tip_theme: Theme = null
static func tooltip_theme() -> Theme:
if _tip_theme != null:
return _tip_theme
var th := Theme.new()
th.set_stylebox("panel", "TooltipPanel",
panel_flat(Color(0.09, 0.07, 0.05, 0.96), Color8(90, 80, 68), 0, 1, Vector4(8, 6, 8, 6)))
th.set_color("font_color", "TooltipLabel", WARM)
th.set_font_size("font_size", "TooltipLabel", 14)
_tip_theme = th
return _tip_theme