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

850 lines
46 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
## 呈现层共享小部件工厂+全站取色/描边唯一出口——各视图禁内联 Color8/StyleBoxFlat 字面量,换皮只改本文件。
class_name UiKit
extends RefCounted
# ── 色板(语义族) ──
const INK := Color8(51, 38, 27) # 墨族——正文/数字(对格底 ≥4.5:1)
const INK_COLD := Color8(19, 46, 54) # 青墨 1-3 #132E36(数字墨三档·冷=稀疏;亮瓦自适应深墨族档兼色牌后备路径)
const INK_OCHRE := Color8(60, 37, 12) # 赭墨 4-5 #3C250C(数字墨三档·中;亮瓦自适应深墨族档兼色牌后备路径)
const INK_RUST := Color8(74, 30, 20) # 锈墨 6-8 #4A1E14(数字墨三档·暖=密集;亮瓦自适应深墨族档兼色牌后备路径)
# ── 亮墨三档(圣经 v3.5 冻结④——数字直画已开格底:色温承袭密度语义,冷亮/暖白/金亮;
# 实测对比 6.0–6.3:1(A3a);金亮低饱和避奖励金语义;色牌后备组用深墨族上牌〔12:1〕。
# 亮墨族限暗纹理板(dungeon),亮筋纹板按 TilePalette.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(233, 217, 184) # 面板底·数据框(AP 条/状态槽)#E9D9B8(demo 唯一来源对齐,Δ≤6/255)
const PANEL_BORDER := Color8(120, 110, 88) # 面板细线框;兼熄灭点阵(DOT_OFF 别名)#786E58(demo 唯一来源对齐,Δ≤6/255)
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)
const GOLD_BRIGHT := Color8(224, 184, 96) # 金族亮档(敌顺位徽标①②③/重甲徽标对暗舞台底 6.5:1;值=收益金 e0b860 同门)
const GOLD_LIT := Color8(230, 190, 90) # 亮金(候选高亮)
const GOLD_DIM := Color8(140, 116, 60) # 暗金(已过节点)
const GOLD_DONE := Color8(104, 86, 44) # 已过节点暗金·关卡画面呈现态 #68562C(GOLD_DIM #8C743C 对双色墨均 <4.5:1,对比硬指标逼守此值;GOLD_DIM 他用不动)
const SEAL_BLACK := Color8(0, 0, 0) # 封印黑 #000000——封印宣告色(紫色与微光蓝族色相冲突故弃;
# 黑色无色相,天然避开禁入带——与妨害紫 281° 各留 12-14° 间距)
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(圣经 §4.1 冻结值)
const HERO_BLUE_DEEP := Color8(30, 90, 150) # 小人蓝同族深档(意图增益徽标墨——PANEL_DATA 垫牌上 4.8:1;命名同 CONTACT_GLOW_DEEP 规则)
const HERO_BLUE_BRIGHT := Color8(126, 186, 240) # 小人蓝同族亮档(魔法护甲徽标对暗舞台底 5.9:1;HERO_BLUE 语义岗〔board_view:35 小人边框/level_map:237〕在役不动)
const BOSS_PURPLE := Color8(143, 47, 106) # 紫赤(Boss 专属)
const DEBUFF_PURPLE := Color8(122, 66, 148) # 妨害紫(意图 ☠)
# ── 类型色五族(值 = demo --tg-* 冻结抄录〔需求卡片 §2.2 抄录表〕;消费面 = 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)
# 类型色带分族判定(三选一与商店货架同源同判:id/kind 小写子串匹配,默认=通用增益带。
# demo「工兵爆破」卡=紫带(magic 族)——blast 归 TG_MAGIC,
# TG_AOE 只接 aoe 键(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"):
return TG_AOE
if id_lower.contains("blast") or id_lower.contains("magic"):
return TG_MAGIC
if id_lower.contains("pierce"):
return TG_PIERCE
return TG_BUFF
# ── 未开格类型微光五组(HUD框架「未开格类型微光」节实装面;「不并入」卡顶带
# TG_* 两套色带两套归组并存,禁互为引用)──
# 色相 = art-bible §4.2 冻结五组(色相既裁不可重裁,只允许按明度档做亮度缩放)。
# 🔴 冻结面稽核:逐组 HSV 对照冻结值——BUFF/HEAL/PIERCE/
# MAGIC 四组 ΔH ≤0.13°(8bit 量化噪声)、ΔS ≤0.002,属纯 V 缩放;群伤组 #A61D37(冻结
# #FF2D55 的 V=0.65 缩放,H=348.57°/S=0.824 逐位不动,保持绯色相避开危险红)。
# 五组均满足「只动明度、不动色相」。
# 明度档 = provisional 值——补齐 §4.2 从未定义的「各组一档明度互辨」数值
# (色弱灰度互辨通道)。
# 🔴 生成口径:阶梯必须在**渲染合成空间**(sRGB/伽马空间)里排——
# 引擎 canvas 混合按帧缓冲编码(伽马)做线性插值,色弱玩家屏幕上看到的是合成值而非色值本身。
# 按 CIE L*(线性)排档会把群伤/魔法排反(绯红线性亮于紫罗兰,但伽马空间绯红 G+B 通道低,
# 合成后反而更暗——实测最弱对 CR 仅 1.05)。五档按合成空间单调阶梯排列:以「带内均值 ≈
# 0.49×色值 + 0.51×瓦片」合成模型反解,相邻组模型 CR = 1.245/1.234/1.239/1.270(判据线
# 1.20;模型域与实测的差 = shader 距离场权重非扁平混合,故互辨判据的终审在渲染取证探针实测,
# L1 棋盘探针逐对读数见 reports/iter30-实现报告读数表)。
# 明度档只许在 provisional 域内自调(L3 预授权③一轮),翻动色相 = 触 art-bible §4.2 冻结面
# = L3 停机呈报。
const TG_GLOW_BUFF := Color8(247, 223, 67) # 通用增益·芥末黄(attack/defense/weaken/action/gp)
const TG_GLOW_HEAL := Color8(104, 208, 66) # 治疗·黄绿(heal)
const TG_GLOW_PIERCE := Color8(48, 166, 166) # 穿刺·青(pierce)
const TG_GLOW_MAGIC := Color8(123, 91, 214) # 魔法·紫罗兰(magic)
const TG_GLOW_AOE := Color8(166, 29, 55) # 群伤·绯红(blast)——冻结 #FF2D55 的 V=0.65 档(色相零移动)
const TYPE_GLOW_ALPHA := 170.0 / 255.0 # 泛光强度(art-bible §4.2 v2 呈现参数 α170)
const TYPE_GLOW_BAND_FRAC := 0.125 # 带宽/格边(cell÷8——同上 v2 冻结参数)
const TYPE_GLOW_SOFT := 0.55 # 帯内软边起点(占带宽比;0=边缘突变,越大衰减越长)
const TYPE_GLOW_CORNER := 2.0 # 圆角掩罩半径(px——微光不溢出格面圆角)
const GLOW_NONE := 0
const GLOW_BUFF := 1
const GLOW_HEAL := 2
const GLOW_PIERCE := 3
const GLOW_AOE := 4
const GLOW_MAGIC := 5
# 九 cell_type → 五组封闭判定(成员集合逐字照抄 art-bible §4.2;清单外值 = 数据错,返回 GLOW_NONE
# 由调用方显式报错登记,禁静默空光——图标键错误处理同款纪律。
# 禁子串匹配、禁复用 tg_band():那是卡顶色带的 demo 冻结值且 blast 归 TG_MAGIC,与本案五组归属相反)
static func glow_group_for_cell_type(cell_type: String) -> int:
match cell_type:
"attack", "defense", "weaken", "action", "gp":
return GLOW_BUFF
"heal":
return GLOW_HEAL
"pierce":
return GLOW_PIERCE
"blast":
return GLOW_AOE
"magic":
return GLOW_MAGIC
_:
return GLOW_NONE
static func glow_color_for_group(g: int) -> Color:
match g:
GLOW_BUFF:
return TG_GLOW_BUFF
GLOW_HEAL:
return TG_GLOW_HEAL
GLOW_PIERCE:
return TG_GLOW_PIERCE
GLOW_AOE:
return TG_GLOW_AOE
GLOW_MAGIC:
return TG_GLOW_MAGIC
_:
return Color(0, 0, 0, 0)
# ── 呈现散件裸色搬运(本节立后 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) # 墙脚阴线(方向稿同色)
const FOG_MEMORY_PLATE := Color8(58, 58, 58) # 记忆板 #3A3A3A(§6.5)
const FOG_MEMORY_INK := Color8(196, 196, 196) # 记忆墨 #C4C4C4(对记忆板 ≈6.3:1)
const FOG_OUTLINE := Color8(30, 24, 18) # 雾面轮廓墨
const ICON_TINT_ON_GOLD := Color(24.0 / 240.0, 20.0 / 226.0, 16.0 / 200.0) # 当前金底图标墨 = WARM 分量乘法调制(双色墨制)
const DUSK := Color8(90, 108, 140) # 暮蓝(全作唯一暮蓝档——失败终局)
const DIM_NIGHT := Color8(24, 18, 12) # 终局压暗底 #18120C(demo 结算仪式暗纱——胜利侧 dim 基色)
const WARM := Color8(240, 226, 200) # 暖白(暗底正文)
const WARM_SOFT := Color8(232, 213, 168) # 暖白次级(说明文)#E8D5A8(配色总纲「米色去灰」)
const WARM_FAINT := Color8(205, 184, 140) # 暖白弱化(帮助文)#CDB88C(配色总纲「米色去灰」)
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;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)
const CONTACT_GLOW_DEEP := Color8(150, 190, 230) # 接触带呼吸暗端(lerp 目标)
const DIGIT_PLATE := Color8(245, 234, 211) # 数字色牌(素色暖纸白直角牌——art-bible §4.1;数字一律垫牌,
# 可读不依赖格底——原则二)
const DIGIT_PLATE_EDGE := Color8(210, 198, 172) # 色牌牌缘线
const SLOT_BACKDROP := Color8(62, 54, 44) # B 区状态图标槽实底(深墨棕——白图标对比承载)#3E362C
const SLOT_BACKDROP_SOFT := Color8(128, 106, 78) # 图标槽实底·柔和档(BCard 图标槽明度差降档:对米色卡面 3.7:1〔深档 8.7:1 过硬〕,白图标 5.1:1 ≥3:1 图形对比硬约束;B 区状态槽仍用深档 SLOT_BACKDROP)
const BG_ROOT := Color8(58, 46, 34) # 屏幕根底 #3A2E22(配色总纲「提暖提亮禁近黑」)
const BG_OVERLAY := Color8(54, 43, 32) # 模态遮罩底 #362B20
const BG_BRIGHT := Color8(69, 55, 35) # 暖亮档底 #453723(标题/营地/商店等暖亮屏根底)
const CANDLE := Color8(246, 198, 103) # 烛光黄 #F6C667(主操作色/候选节点/钥石徽章底;
# 与 §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 深底
# 呈现杂色档(各视图裸 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) # 站桩石板地台·顶缘受光线
const CAMP_HEADER_BAND := Color8(110, 69, 38) # 营地柜台头带深棕 #6E4526(4-camp.png 方向稿取色;对齐 demo --wood-deep)
const CAMP_ROW_PLATE := Color8(240, 230, 206) # 营地行板暖米(同上)
const CAMP_BADGE_PANEL := Color8(40, 32, 24) # 营地钥石徽暗底(同上)
const HP_BAR_BACK := Color8(40, 34, 32) # 迷你血条深底 #282220(兼血数垫牌)
# BBCode 十六进制(RichTextLabel 用;与上同值)
const HEX_GOLD := "f0a828"
const HEX_INK := "33261b"
const HEX_ON_GOLD := "3a2a08"
# 结算行/叙事行语义档(battle_log_view 行格式化与 narrator 横幅行的裸 hex 单点化;
# 名随呈现语义)
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 内建承诺:开启时时长减半;结算逐行兑现为信息载体降速不跳过)──
# 唯一入口 = 设置面开关(调整板本体在调试浮窗)
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
# ── 单位/特效纹理路径单点 ──
# 素材 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 档)
const TEX_HERO_ATTACK := "res://assets/art/character/character_hero_attack.png" # B 区站桩立绘·attack 态(三态切换)
const TEX_HERO_HIT := "res://assets/art/character/character_hero_hit.png" # B 区站桩立绘·hit 态(三态切换)
# 敌种立绘(B 区站桩)——文件键 = 数据表主键 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" # 灰矮人石君王
# 框材组纹理槽(♻ 复用件量化改色入包,产物 = 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" # 柜台三·奖励格图鉴
# 节点图标五键(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" # 节点图标·商店房
# 手绘功能图标十键(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" # 图标·GP 袋
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
# ── 屏底氛围层(demo .tilebg opacity .14/.vignette/.beams 对齐;
# demo 石纹只作弱质感,聚焦靠暗角)──
const BACKDROP_TILE_ALPHA := 0.14 # demo .tilebg{opacity:.14}
const BG_MAP_TOP := Color8(74, 59, 42) # 地图屏渐变顶 #4A3B2A(demo #v-map 180deg 0%;低点 40% = BG_ROOT #3A2E22)
# 石纹弱质感全屏片(0.14;缺素材返回 null = 调用侧回退平色)
static func backdrop_tile() -> TextureRect:
var bd_tex := load_tex(TEX_BACKDROP_STONE)
if bd_tex == null:
return null
var bd := TextureRect.new()
bd.texture = bd_tex
bd.stretch_mode = TextureRect.STRETCH_TILE
bd.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
bd.modulate = Color(1, 1, 1, BACKDROP_TILE_ALPHA)
bd.set_anchors_preset(Control.PRESET_FULL_RECT)
bd.mouse_filter = Control.MOUSE_FILTER_IGNORE
return bd
# 径向暗角(demo .vignette——ellipse 90%×80% at 50% 42%,透明 58% → 黑 0.30 满角)
static func vignette_rect() -> TextureRect:
var g := Gradient.new()
g.offsets = PackedFloat32Array([0.0, 0.58, 1.0])
g.colors = PackedColorArray([Color(0, 0, 0, 0), Color(0, 0, 0, 0), Color(0, 0, 0, 0.30)])
var t := GradientTexture2D.new()
t.gradient = g
t.fill = GradientTexture2D.FILL_RADIAL
t.fill_from = Vector2(0.5, 0.42)
t.fill_to = Vector2(1.4, 0.42)
t.width = 256
t.height = 256
var tr := TextureRect.new()
tr.texture = t
tr.stretch_mode = TextureRect.STRETCH_SCALE
tr.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR # 平滑渐变件白名单(全局 Nearest 下显式豁免)
tr.set_anchors_preset(Control.PRESET_FULL_RECT)
tr.mouse_filter = Control.MOUSE_FILTER_IGNORE
return tr
# 斜光带(demo #v-start .beams——repeating-linear-gradient 102deg,暖白 5% 窄带 240-340/560px;
# 程序纹理循环带,确定性生成零随机。光柱采用连续 alpha:
# 带内 alpha 0→0.05→0 横向过渡+纵向衰减上亮下暗;
# 纹理 filter=显式 LINEAR 白名单(与暗角/地图渐变同血统)
static func _beams_rect() -> TextureRect:
var img := Image.create(560, 560, false, Image.FORMAT_RGBA8)
_paint_beams(img)
var tr := TextureRect.new()
tr.texture = ImageTexture.create_from_image(img)
tr.stretch_mode = TextureRect.STRETCH_TILE
tr.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR # 平滑渐变件白名单(连续 alpha 与暗角/地图渐变同血统)
tr.set_anchors_preset(Control.PRESET_FULL_RECT)
tr.mouse_filter = Control.MOUSE_FILTER_IGNORE
return tr
# 光带几何常量(12° 斜向光带——带轴倾角/带域/峰值强度;_paint_beams 单消费)
const BEAMS_TILT_CA := 0.9135 # cos(0.2094)——12°(102deg 带轴的垂直倾角)
const BEAMS_TILT_SA := 0.4067 # sin(0.2094)
const BEAMS_BAND_LO := 240.0
const BEAMS_BAND_HI := 340.0
const BEAMS_PEAK := 0.05
## 光带像素域(循环体提函数——12° 斜向光带双三角衰减:带内峰值×纵向衰减逐像素着色)
func _paint_beams(img: Image) -> void:
var ca := BEAMS_TILT_CA
var sa := BEAMS_TILT_SA
var beam_col := Color(1.0, 236.0 / 255.0, 200.0 / 255.0)
var band_mid := (BEAMS_BAND_LO + BEAMS_BAND_HI) * 0.5
var band_half := (BEAMS_BAND_HI - BEAMS_BAND_LO) * 0.5
for y in 560:
var v_fall := 1.0 - float(y) / 560.0 # 纵向衰减:上亮下暗(连续 alpha)
for x in 560:
var d := fposmod(x * ca + y * sa, 560.0)
if d >= BEAMS_BAND_LO and d < BEAMS_BAND_HI:
var a: float = BEAMS_PEAK * (1.0 - absf(d - band_mid) / band_half) * v_fall
img.set_pixel(x, y, Color(beam_col.r, beam_col.g, beam_col.b, a))
# 竖向渐变底(demo #v-map linear-gradient(180deg, top 0%, low 40%)——stop 后恒 low)
static func _gradient_base(top: Color, low: Color, stop := 0.4) -> TextureRect:
var g := Gradient.new()
g.offsets = PackedFloat32Array([0.0, stop])
g.colors = PackedColorArray([top, low])
var t := GradientTexture2D.new()
t.gradient = g
t.fill_from = Vector2(0.5, 0.0)
t.fill_to = Vector2(0.5, 1.0)
t.width = 64
t.height = 256
var tr := TextureRect.new()
tr.texture = t
tr.stretch_mode = TextureRect.STRETCH_SCALE
tr.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR # 平滑渐变件白名单(全局 Nearest 下显式豁免)
tr.set_anchors_preset(Control.PRESET_FULL_RECT)
tr.mouse_filter = Control.MOUSE_FILTER_IGNORE
return tr
# CJK 随包字体(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
# ── 字号档位表(六档;全站 UiKit.label/字号 override 只许取
# 本表。归档映射:10/11/12→CAPTION、13/14→HINT、15/16→BODY、17/18/20→SUB、
# 21/22/24/26→TITLE、32/40→H1(就近归档);棋盘数字公式 maxi(18, ceil(格边×0.5)) 与站桩血数
# 11px 条带内读数豁免不入表(art-bible §7.4)──
const FS_CAPTION := 12 # 装饰 caption/分区小字/微计数(§7.4 豁免下限)
const FS_HINT := 14 # 提示/tooltip 下限档
const FS_BODY := 16 # 正文/明细下限档
const FS_SUB := 18 # 副标题/小组标题(=棋盘数字下限同值)
const FS_TITLE := 24 # 屏标题/横幅/价格
const FS_H1 := 34 # 主标题/演算暴击
# ── 秘宝解锁条件计数键 → 呈现标签(秘宝匣柜台条件进度文案单点;键域=
# TreasureItemTable.UNLOCK_COUNTER_KEYS 封闭枚举,键出域走「条件」兜底不炸)──
const TREASURE_REQ_LABELS := {"adventure_cleared": "冒险成功", "boss_kill_count": "Boss击杀", "vision_used": "真视使用"}
# 秘宝档位 → 呈现名(契约 2 三档;legendary 传说空集合法不在柜)
const TREASURE_TIER_LABELS := {"common": "普通", "excellent": "优秀", "rare": "稀有", "legendary": "传说"}
# ── StyleBox 工厂 ──
# margin = (左, 上, 右, 下);border_w = 0 即无边框(根底/遮罩用)
# 圆角率单点(容器/按钮全站唯一圆角值;数据框/数字牌/AP 点阵/准线/血条
# =0 直角不变;底座椭圆 17/结算金印 200/状态脉冲环 100 圆形 = §7.1 明示例外保留)
const RADIUS_OBJECT := 5
# 九宫格切片(panel 石皮 16 与 demo .plate9 slice16 对齐;button 维持 8、仪式框维持 12)
const PANEL_SLICE := 16.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)——传色即一次到位;省略时缺省
# 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
# ── 模态壳工厂(各 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
const PANEL_DEFAULT := true # modal_shell 面板哨兵(非 StyleBox 实例——null=无面板/本哨兵=默认样式盒面板/StyleBox=自定义)
# 面板哨兵解析(六参收拢五参——三态:null=无面板〔原 with_panel=false〕/PANEL_DEFAULT=默认样式盒面板
# 〔原 with_panel=true+panel_sb=null〕/StyleBox 实例=自定义样式盒面板〔原双参形态〕)
static func _resolve_shell_panel(panel_sb) -> PanelContainer:
if panel_sb == null:
return null
var panel := PanelContainer.new()
if panel_sb != PANEL_DEFAULT:
panel.add_theme_stylebox_override("panel", panel_sb)
return panel
static func modal_shell(parent: Control, dim_color: Color, min_size := Vector2.ZERO, separation := 12,
panel_sb = 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 := _resolve_shell_panel(panel_sb)
var host: Control = center
if panel != null:
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}
# ── 全屏页壳(demo #v-shop/#v-bonus 全屏页形态落地:
# 商店全屏化 + 竖卡 232×352。暖底+石纹弱质感+暗角+56px 通栏顶条+内容列;
# 商店=暖亮底、三选一=根底,两屏同构差异仅底色与顶条工具件)──
const PAGE_TOP_H := 56.0
static func page_shell(host: Control, bright := true) -> Dictionary:
var page := Control.new()
page.set_anchors_preset(Control.PRESET_FULL_RECT)
host.add_child(page)
var base := ColorRect.new()
base.color = BG_BRIGHT if bright else BG_ROOT
base.set_anchors_preset(Control.PRESET_FULL_RECT)
base.mouse_filter = Control.MOUSE_FILTER_STOP # 页底挡点击——模态语义由组合根输入锁兜底
page.add_child(base)
var tile := backdrop_tile()
if tile != null:
page.add_child(tile)
page.add_child(vignette_rect())
var top := PanelContainer.new()
top.set_anchors_preset(Control.PRESET_TOP_WIDE)
top.custom_minimum_size = Vector2(0, PAGE_TOP_H)
top.add_theme_stylebox_override("panel",
panel_flat(Color8(38, 29, 20), Color8(85, 72, 58), 0, 1, Vector4(26, 6, 26, 6)))
page.add_child(top)
var top_row := HBoxContainer.new()
top_row.add_theme_constant_override("separation", 12)
top.add_child(top_row)
var box := VBoxContainer.new()
box.set_anchors_preset(Control.PRESET_FULL_RECT)
box.offset_top = PAGE_TOP_H + 20
box.offset_left = 26
box.offset_right = -26
box.offset_bottom = -22
box.add_theme_constant_override("separation", 10)
page.add_child(box)
return {"page": page, "base": base, "top_row": top_row, "box": box}
# 属性百分比合成(board_resolvers.debug_hero_override 与调试浮窗生效值预览共用单点——两处同式实现收口)
static func scaled(base: int, pct: int) -> int:
return maxi(1, int(round(float(base) * (100.0 + float(pct)) / 100.0)))
# 宽度纪律(全站按钮最小宽度基准 216+SIZE_FILL:VBox 列内拉满列宽等宽,
# HBox 行内不抢余宽(EXPAND_FILL 会把行撑爆——营地底条 1616px 实证);角落小件/紧凑位传 0 豁免)
# 钮高纪律(统一全站钮高,消除标题屏主钮与常规钮不齐):
# 全站常规钮统一最小高 40(主操作钮/危险钮经 button() 同高对齐,压扁形态从此不存在);
# 豁免域与宽度纪律同款——min_w=0 紧凑位(HUD 顶条小钮/角标)不抬行高
const BUTTON_MIN_H := 40.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, BUTTON_MIN_H)
b.size_flags_horizontal = Control.SIZE_FILL
_apply_button_skin(b)
return b
# ── 框材纹理工厂(§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)
# content margin 低于切片的分量自动抬到切片——切片 16 后石框纹理不吃内容
# (9 消费面 7 处 margin<16;工厂单点 clamp,调用点零改)
static func panel_skinned(margin: Vector4 = Vector4.ZERO, tint := Color.WHITE) -> StyleBox:
var m := Vector4(maxf(margin.x, PANEL_SLICE), maxf(margin.y, PANEL_SLICE),
maxf(margin.z, PANEL_SLICE), maxf(margin.w, PANEL_SLICE))
var tex := load_tex(TEX_PANEL_STONE)
if tex == null:
return panel_flat(PANEL_DATA, PANEL_BORDER, RADIUS_OBJECT, 1, m)
var sb := _tex_sb(tex, PANEL_SLICE, tint)
sb.content_margin_left = m.x
sb.content_margin_top = m.y
sb.content_margin_right = m.z
sb.content_margin_bottom = m.w
return sb
# 仪式石框(空心角括框——营地容器框;缺位回退暗面板)
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
# 钥石徽(badge 工厂件——消费方 = 营地覆盖层 + 状态面板钥石余额行。形制 = 烛光黄底+木色描边
# +钥石图标(icon_rect 守卫装载,缺位=纯文本回退)+深墨字;零新素材零新色值——值全取现行 token)
class KeystoneBadge:
extends PanelContainer
var value_label: Label
func _init(text: String, icon_path: String, icon_px: float) -> void:
add_theme_stylebox_override("panel",
UiKit.panel_flat(UiKit.CANDLE, UiKit.CAMP_HEADER_BAND, UiKit.RADIUS_OBJECT, 1, Vector4(14, 8, 14, 8)))
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 6)
add_child(row)
if icon_path != "":
var ic := UiKit.icon_rect(icon_path, icon_px)
if ic != null:
row.add_child(ic)
value_label = UiKit.label(text, UiKit.FS_SUB, true, UiKit.INK_ON_GOLD)
row.add_child(value_label)
func set_text(t: String) -> void:
value_label.text = t
static func keystone_badge(text: String, icon_path := TEX_ICON_KEYSTONE, icon_px := 32.0) -> KeystoneBadge:
return KeystoneBadge.new(text, icon_path, icon_px)
# 按钮皮·五态矩阵(逐实例 override 不动主题层)
# 悬停:常态图+modulate 提亮(0.06 档与 primary_button 现行 lightened(0.06) 同纪律,零新素材);
# 禁用:本族底+α0.45 幽灵化(与 font_disabled_color 同纪律);
# 焦点:INK 2px 内缩 1px 环(与 font_focus_color=INK 同源;引擎 focus 样式盒叠加绘制,不遮钮底)
const BUTTON_HOVER_LIFT := Color(1.06, 1.06, 1.06, 1.0) # 悬停提亮档(modulate 乘数>1=中间调提亮,高光像素封顶不漂)
const BUTTON_GHOST_ALPHA := 0.45 # 禁用幽灵化 α(与文字禁用同档)
const BUTTON_FOCUS_RING_W := 2.0 # 焦点环宽 px
const BUTTON_FOCUS_RING_INSET := 1.0 # 焦点环内缩 px(负 expand margin 实现)
static func _focus_ring_sb() -> StyleBoxFlat:
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0, 0, 0, 0)
sb.draw_center = false
sb.set_border_width_all(BUTTON_FOCUS_RING_W)
sb.border_color = INK
sb.set_expand_margin_all(-BUTTON_FOCUS_RING_INSET)
return sb
# 按钮皮(五态统一管线——常态/悬停/按压/禁用/焦点+墨字;缺位素材 = 引擎默认主题)
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, BUTTON_HOVER_LIFT))
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_n, 8.0, Color(1, 1, 1, BUTTON_GHOST_ALPHA)))
b.add_theme_stylebox_override("focus", _focus_ring_sb())
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, BUTTON_GHOST_ALPHA))
# ── 公共件(bcard 竖卡工厂/拨杆开关/样式滑杆/动词按钮/tooltip 主题;
# 消费方 = 标题/营地/设置/商店/三选一屏。单点纪律同 §7.1:新件只加不改旧件)──
# 动词按钮(图标+动词+悬停说明三件套——标题屏按钮组/通用;icon 缺位=纯文字不炸)
static func primary_button(text: String, tip := "") -> Button:
var b := button(text)
var sb := panel_flat(CANDLE, Color8(110, 69, 52),RADIUS_OBJECT, 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),RADIUS_OBJECT, 2, Vector4(10, 2, 10, 2)))
b.add_theme_stylebox_override("pressed", panel_flat(Color8(214, 172, 88), Color8(110, 69, 52),RADIUS_OBJECT, 2, Vector4(10, 2, 10, 2)))
b.add_theme_stylebox_override("disabled",
panel_flat(Color(CANDLE, BUTTON_GHOST_ALPHA), Color(Color8(110, 69, 52), BUTTON_GHOST_ALPHA), RADIUS_OBJECT, 2, Vector4(10, 2, 10, 2)))
b.add_theme_stylebox_override("focus", _focus_ring_sb())
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))
b.add_theme_color_override("font_disabled_color", Color(Color8(58, 42, 8), BUTTON_GHOST_ALPHA))
if tip != "":
b.tooltip_text = tip
return b
# 危险钮(深红平涂——放弃/删除类;破坏性确认位置纪律=不得与常规确认同位)
static func danger_button(text: String, tip := "", min_w := 216.0) -> Button:
var b := button(text, min_w)
var sb := panel_flat(Color8(180, 78, 54), Color8(110, 42, 28),RADIUS_OBJECT, 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),RADIUS_OBJECT, 2, Vector4(10, 2, 10, 2)))
b.add_theme_stylebox_override("pressed",
panel_flat(Color8(180, 78, 54).darkened(0.18), Color8(110, 42, 28),RADIUS_OBJECT, 2, Vector4(10, 2, 10, 2)))
b.add_theme_stylebox_override("disabled",
panel_flat(Color(Color8(180, 78, 54), BUTTON_GHOST_ALPHA), Color(Color8(110, 42, 28), BUTTON_GHOST_ALPHA), RADIUS_OBJECT, 2, Vector4(10, 2, 10, 2)))
b.add_theme_stylebox_override("focus", _focus_ring_sb())
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))
b.add_theme_color_override("font_focus_color", Color8(255, 244, 224))
b.add_theme_color_override("font_disabled_color", Color(Color8(255, 244, 224), BUTTON_GHOST_ALPHA))
if tip != "":
b.tooltip_text = tip
return b
# 卡顶类型色带·Bayer 有序抖动(
# demo CSS 平滑渐变/blur 违反像素纪律,改 4×4 Bayer 有序抖动台阶:α 由深至 0,2px 元胞逐行降覆盖;
# 纯静态绘制零动效(减动效无涉);Bayer 阶数=实现细节白名单项,取 4×4)
class DitherBand:
extends Control
var band: Color
func _init(c: Color) -> void:
band = c
custom_minimum_size = Vector2(0, 8) # 8px = 4×4 Bayer 全周期
func _draw() -> void:
var bayer := [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]]
var cell := 2.0
var rows := int(ceil(custom_minimum_size.y / cell))
var cols := int(ceil(size.x / cell))
for ry in rows:
var cover := 1.0 - float(ry) / float(rows) # 顶行全覆盖 → 底行趋零
for rx in cols:
if float(bayer[ry % 4][rx % 4]) / 16.0 < cover:
draw_rect(Rect2(float(rx) * cell, float(ry) * cell, cell, cell), band, true)
# bcard 竖卡工厂(商店/三选一同构卡——两屏共用;差异仅=价签 vs ✓选择章)
# 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 插件到底注前——购买钮挂位)
var _sb_normal: StyleBox
var _sb_sel: StyleBox
func _init(title: String, desc: String, band: Color, icon_path: String, opts := {}) -> void:
var price: String = opts.get("price", "")
var footnote: String = opts.get("footnote", "")
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(PANEL_DATA, PANEL_BORDER,RADIUS_OBJECT, 1, Vector4(12, 10, 12, 10)) # 回退=数据板单点
_sb_sel = UiKit.panel_flat(PANEL_DATA, Color8(138, 90, 52), RADIUS_OBJECT, 2, Vector4(12, 10, 12, 10)) # 选中底=数据板单点
add_theme_stylebox_override("panel", _sb_normal)
custom_minimum_size = Vector2(232, 352) # demo .bcard 232×352
size_flags_vertical = Control.SIZE_SHRINK_CENTER # 行容器不纵向拉伸(卡恒 352 高居中——HBox fill 会拉到 ~536)
var box := VBoxContainer.new()
box.add_theme_constant_override("separation", 8)
add_child(box)
body = box
var band_r := DitherBand.new(band) # 卡顶类型色带=Bayer 有序抖动带(随各 TG_* 逐卡着色)
box.add_child(band_r)
mark = UiKit.label("✓",FS_BODY, true, Color8(58, 42, 8))
mark.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
mark.visible = false
box.add_child(mark)
box.add_child(_build_icon_slot(icon_path))
name_l = UiKit.label(title,FS_SUB, true, INK_TITLE)
name_l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
box.add_child(name_l)
desc_l = UiKit.label(desc,FS_HINT, 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,FS_TITLE, true, GOLD_INK) # GOLD 对货架板 1.46:1 越带,换深金墨 5.21:1(三选一价签恒空串隐藏零波及)
price_l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
price_l.visible = price != ""
box.add_child(price_l)
foot_l = UiKit.label(footnote,FS_CAPTION, false, Color8(110, 69, 52))
foot_l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
foot_l.visible = footnote != ""
box.add_child(foot_l)
# 图标槽(demo .ic 76×76 槽底+图标 60=20px 源 3 整数倍;明度差降档——柔和档;白图标 ≥3:1 硬约束在役)
func _build_icon_slot(icon_path: String) -> PanelContainer:
var slot := PanelContainer.new()
slot.add_theme_stylebox_override("panel", UiKit.panel_flat(SLOT_BACKDROP_SOFT, PANEL_BORDER, 0, 1))
slot.custom_minimum_size = Vector2(76, 76)
slot.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
icon_tr = TextureRect.new()
var itex := UiKit.load_tex(icon_path)
if itex != null:
icon_tr.texture = itex
icon_tr.custom_minimum_size = Vector2(60, 60)
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
slot.add_child(icon_tr)
return slot
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, foot_l.get_index())
## 竖卡工厂(BCard._init 同形收拢 6→5 参——opts 键 {price, footnote} 缺省逐字同值)
static func bcard(title: String, desc: String, band: Color, icon_path: String, opts := {}) -> BCard:
return BCard.new(title, desc, band, icon_path, opts)
# 拨杆开关(设置屏——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),RADIUS_OBJECT, 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)
# 样式滑杆/tooltip 主题词条单点迁
# game/assets/theme/main_theme.tres 全局主题:滑轨 slider/grabber_area/grabber、
# TooltipPanel/TooltipLabel;禁新旧并存。消费方几何内联:
# settings_overlay 音量滑轨 / debug_overlay _slider_row