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

258 lines
14 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
## 棋盘瓦片件:高墙/地面瓦片整批装载与运行时镜像合成、地板明度归一、数字墨族按板自适应(静态算法族)、
## 格坐标确定性散列与十六正交情形墙瓦片查表。装载缓存七成员持于件内(同板型零重载、换板重载时序同源),
## 查表族读件持棋盘引用(棋盘视图 rebuild 经 bind_board 同步)。零绘制面、零玩法数据写入。
class_name TilePalette
extends RefCounted
# ── 高墙/地面瓦片纹理路径单点 ──
# 素材 PNG 不入 git——一律 UiKit.load_tex(exists 守卫)装载,禁 preload;
# 缺位返回 null,整批任一缺位 = 弃用瓦片走程序化平色回退(棋盘视图 BRICK/FLOOR,fresh checkout 冒烟不炸)。
# 词表 = design/research/board-tile-vocab-plan-a-2026-09-26.md(16 唯一墙瓦片 + 5 地面变体;
# 东/东北/东南/end_e/in_ne/in_se 六镜像件运行时 flip_x 合成,不出文件——词表 §二)。
# 路径按 board_type 后缀参数化——terrain_<件名>_<board_type>.png;
# 词表 §一 16 唯一墙瓦片名单单点在本常量数组。underdark/deep_underdark 两套 =
# tools/recolor_terrain_tiles.py 程序换色产;任一套缺位 → 整批回落
# dungeon 套(禁同板混渲染);dungeon 套亦缺位 → 程序化平色回退(fresh checkout 冒烟不炸)。
const TILE_WALL_NAMES := ["wall_center", "wall_center_crack", "wall_center_speckle", "wall_n", "wall_s", "wall_w", "wall_nw", "wall_sw", "wall_hrun", "wall_vrun", "wall_end_n", "wall_end_s", "wall_end_w", "wall_lone", "wall_in_nw", "wall_in_sw"]
const TILE_WALL_FMT := "res://assets/art/board/terrain_%s_%s.png" # %s=件名 %s=board_type
const TILE_FLOOR_FMT := "res://assets/art/board/terrain_floor_%s_%d.png" # %s=board_type 1..%d 变体
const TILE_FLOOR_VARIANTS := 5 # 地面瓦片变体数(词表 §一 floor_1..5)
# ---- 数字墨族按板自适应(墨色按关自适应+三关对比断言)----
const DIGIT_INK_CR_P90 := 4.5 # 墨族对地板 p90 亮斑对比度门槛(失效模式=亮白筋纹冲字,WCAG 数字体同源)
const DIGIT_INK_CR_MEAN_BRIGHT := 2.5 # 亮族对地板均值门槛(中调防吃字)
const DIGIT_INK_CR_MEAN_DARK := 3.0 # 深族对地板均值门槛(大字 AA 级——2.4 的实测观感=隐形)
# 墙瓦片 16 正交情形映射(下标 = nf<<3|ef<<2|sf<<1|wf,nf/ef/sf/wf = 北/东/南/西邻是地板)。
# 词表 §一 16 情形 = 0 地板(center 族,键 15 另走对角凹角/中心三变体)+ 3 地板×4(端头)+
# 2 地板×6(条带/凸角)+ 1 地板×4(单缘)+ 4 地板(对角凹角/中心三变体);lone 件与原实现同址
# 不入查表(0 地板收尾落 wall_w)
const WALL_MASK_TILE := ["wall_w", "wall_w", "wall_s", "wall_sw", "wall_e", "wall_vrun", "wall_se",
"wall_end_s", "wall_n", "wall_nw", "wall_hrun", "wall_end_e", "wall_ne", "wall_end_n",
"wall_end_w", "wall_center"]
# 四邻皆墙格:对角凹角优先 nw > ne > sw > se(词表 §三.2——双凹角不做专件,多对角地板回落
# 首中单凹角件,残留 1px 缝为可接受限)
const WALL_DIAG_DIRS := [Vector2i(-1, -1), Vector2i(1, -1), Vector2i(-1, 1), Vector2i(1, 1)]
const WALL_DIAG_TILES := ["wall_in_nw", "wall_in_ne", "wall_in_sw", "wall_in_se"]
# 无对角地板 → 中心三变体按格坐标哈希 60/20/20(posmod 保非负下标,10 槽展开免分支)
const WALL_CENTER_PICK := ["wall_center", "wall_center", "wall_center", "wall_center", "wall_center",
"wall_center", "wall_center_crack", "wall_center_crack", "wall_center_speckle", "wall_center_speckle"]
# ── 装载缓存(原棋盘视图同名成员随迁;缺位 = 全 false/空——程序化平色回退)──
var tiles: Dictionary[String, Texture2D] = {} # 词表键 → Texture2D(镜像件已预翻转)
var floor_tiles: Array[Texture2D] = [] # 地面瓦片五变体(下标 0..4 = floor_1..5)
var tiles_ok := false # false = 素材缺位/未装载——调用方走程序化平色原路径
var tiles_board_type := "dungeon" # 当前装载瓦片套的 board_type(rebuild 按板切换)
var digit_dark_ink := true # 数字墨族按板自适应(true=深墨三档)。缺盘平色回退态 #B39A78 裸土
# 为中明度底——深墨族 CR 4.96 达标、亮墨族仅 2.2,故缺盘默认深墨;瓦片装载后按实测分位数覆写
var digit_plate := false # 数字色牌垫底模式(双峰纹理板——直画两族皆不过门槛时落牌,
# 素牌+深墨 12:1 纹理免疫;渲染帧取证 underdark 系深墨直画隐形,故启用色牌)
# ── 查表用棋盘态(rebuild 换板时经 bind_board 同步)──
var board: BoardData = null
var minesweeper: MinesweeperRules = null
func bind_board(board_ref: BoardData, minesweeper_ref: MinesweeperRules) -> void:
board = board_ref
minesweeper = minesweeper_ref
# 瓦片整批守卫装载(首次建板与换板型各一次):21 枚全成才启用——任一缺位按整批缺位处理
#(禁同板混渲染;fresh checkout 无素材目录 → 静默全回退)。缺位不 push_error——可选素材缺位是
# 预期路径非故障(英雄 load_tex 纪律同款,防冒烟刷屏)。镜像件运行时 flip_x 合成,不出文件。
func load_board_tiles(board_type: String = "dungeon") -> void:
if board_type == "":
board_type = "dungeon"
# 16 墙瓦片装载循环节(词表 TILE_WALL_NAMES 单点;
# 词表本身被 terrain_tile_sets_test 直读,保留)
var t: Dictionary[String, Texture2D] = {}
for wall_name in TILE_WALL_NAMES:
t[wall_name] = UiKit.load_tex(TILE_WALL_FMT % [wall_name, board_type])
var floors: Array[Texture2D] = []
for i in range(1, TILE_FLOOR_VARIANTS + 1):
floors.append(UiKit.load_tex(TILE_FLOOR_FMT % [board_type, i]))
for k in t:
if t[k] == null:
_request_dungeon_fallback(board_type)
return
for tex in floors:
if tex == null:
_request_dungeon_fallback(board_type)
return
# 地板归一(圣经 v3.5 冻结①/§1.4——已开格底归一世界底层档 L≈0.10,带 0.08–0.14):
# 方向稿 _v3_darken_floors 同式移植;现行瓦片 L≈0.036 低于带下沿故实为提亮;
# 本归一为程序代役(逐批装载时对 freshly-loaded 原图测光,幂等)。归一后逐像素明度顺路采集,
# 驱动数字墨族按板自适应(digit_ink_mode——均值分位同源同像素,禁二次解码)
var lumas: Array[Variant] = []
for i in range(floors.size()):
floors[i] = normalize_floor_luma(floors[i], 0.10, lumas)
var ink_mode := digit_ink_mode(lumas)
digit_dark_ink = bool(ink_mode.dark)
digit_plate = bool(ink_mode.plate)
# 六镜像件(词表 §二:wall_e←wall_w / wall_ne←wall_nw / wall_se←wall_sw /
# wall_end_e←wall_end_w / wall_in_ne←wall_in_nw / wall_in_se←wall_in_sw;只水平翻转禁旋转
# ——北受光不受损)
t["wall_e"] = flip_tex(t["wall_w"])
t["wall_ne"] = flip_tex(t["wall_nw"])
t["wall_se"] = flip_tex(t["wall_sw"])
t["wall_end_e"] = flip_tex(t["wall_end_w"])
t["wall_in_ne"] = flip_tex(t["wall_in_nw"])
t["wall_in_se"] = flip_tex(t["wall_in_sw"])
tiles = t
floor_tiles = floors
tiles_board_type = board_type
tiles_ok = true
# 整批回落:请求套缺位 → 回落 dungeon 套(禁同板混渲染);dungeon 本身缺位 =
# 维持程序化平色回退原语义(tiles_ok 恒 false,缺位不刷屏纪律不变)
func _request_dungeon_fallback(requested: String) -> void:
if requested != "dungeon":
load_board_tiles("dungeon")
# 水平镜像贴图运行时合成(flip_h 语义):Image 级逐位翻转后重建 ImageTexture——CellUnit 零改动。
# VRAM 压缩源先解压(flip_x 不支持压缩格式);32×32 原尺寸逐位翻转,无 ±1px 取整问题
static func flip_tex(tex: Texture2D) -> Texture2D:
var img := tex.get_image()
if img.is_compressed():
img.decompress()
img.flip_x()
return ImageTexture.create_from_image(img)
# 地板瓦片明度归一(方向稿 _v3_darken_floors 同式移植):8×8 双线性重采样均值测光
# (sRGB→线性域,Rec.709 系数),增益 k=(目标/实测)^(1/2.2) 钳 [0.6,1.8],逐通道 sRGB 域线性拉伸。
# 输入恒为 freshly-loaded 原图(load_tex 每次盘读)——幂等可重入;32×32 五变体开销可忽略
static func normalize_floor_luma(tex: Texture2D, target_l: float = 0.10, lumas_out: Variant = null) -> Texture2D:
var img: Image = tex.get_image()
if img.is_compressed():
img.decompress()
img.convert(Image.FORMAT_RGBA8)
var small: Image = img.duplicate()
small.resize(8, 8, Image.INTERPOLATE_BILINEAR)
var sum := 0.0
for y in range(8):
for x in range(8):
var p := small.get_pixel(x, y)
sum += 0.2126 * srgb_lin(p.r) + 0.7152 * srgb_lin(p.g) + 0.0722 * srgb_lin(p.b)
var k := pow(target_l / maxf(sum / 64.0, 0.001), 1.0 / 2.2)
k = clampf(k, 0.6, 1.8)
for y in range(img.get_height()):
for x in range(img.get_width()):
var c := img.get_pixel(x, y)
var nr := minf(c.r * k, 1.0)
var ng := minf(c.g * k, 1.0)
var nb := minf(c.b * k, 1.0)
img.set_pixel(x, y, Color(nr, ng, nb, c.a))
if lumas_out != null:
lumas_out.append(0.2126 * srgb_lin(nr) + 0.7152 * srgb_lin(ng) + 0.0722 * srgb_lin(nb))
return ImageTexture.create_from_image(img)
static func srgb_lin(c: float) -> float:
return c / 12.92 if c <= 0.04045 else pow((c + 0.055) / 1.055, 2.4)
# 归一后地板逐像素明度 → 选数字呈现模式。三态:亮族直画(暗纹理板)/深族直画(中暗板)/
# 色牌垫底(双峰纹理板——近黑斑与近白筋并存时任何墨族直画必融一峰,渲染帧取证
# 实证 underdark 系深墨隐形,牌上深墨 12:1 纹理免疫)。家族最弱成员(亮族=金亮/深族=锈墨)
# 对 p90 与均值双门槛;亮族过选亮族,否则深族(均值门 3.0),两族皆不过落牌。
# 实测:dungeon p90=0.121 亮族 4.79/5.80 ✓ 直画;underdark p90=0.406 亮族 ✗、
# 深族 5.34/**2.41**(均值 <3.0 ✗)→ 落牌;deep_underdark 同→落牌
static func digit_ink_mode(lumas: Array) -> Dictionary:
var sorted := lumas.duplicate()
sorted.sort()
var n := sorted.size()
if n == 0:
return {"dark": false, "plate": false, "cr_p90_bright": 0.0, "cr_mean_bright": 0.0, "cr_p90_dark": 0.0, "cr_mean_dark": 0.0}
var p90: float = sorted[int(n * 0.9)]
var crs := _family_contrasts(p90, _mean_of(sorted))
var bright_ok: bool = crs.x >= DIGIT_INK_CR_P90 and crs.z >= DIGIT_INK_CR_MEAN_BRIGHT
var dark_ok: bool = crs.y >= DIGIT_INK_CR_P90 and crs.w >= DIGIT_INK_CR_MEAN_DARK
return {"dark": not bright_ok and dark_ok, "plate": not bright_ok and not dark_ok,
"cr_p90_bright": crs.x, "cr_mean_bright": crs.z, "cr_p90_dark": crs.y, "cr_mean_dark": crs.w}
# 均值(明度集合逐元素算术平均)
static func _mean_of(values: Array) -> float:
var sum := 0.0
for v in values:
sum += v
return sum / float(values.size())
# 两族对比度四元组:x=亮族 p90 / y=深族 p90 / z=亮族均值 / w=深族均值
static func _family_contrasts(p90: float, mean: float) -> Vector4:
return Vector4(contrast_y(UiKit.INK_L_GOLD, p90), contrast_y(UiKit.INK_RUST, p90),
contrast_y(UiKit.INK_L_GOLD, mean), contrast_y(UiKit.INK_RUST, mean))
static func contrast_y(ink: Color, bg: float) -> float:
var y := ink_y(ink)
return (maxf(y, bg) + 0.05) / (minf(y, bg) + 0.05)
static func ink_y(c: Color) -> float:
return 0.2126 * srgb_lin(c.r) + 0.7152 * srgb_lin(c.g) + 0.0722 * srgb_lin(c.b)
# 格坐标确定性散列(禁 RNG/禁 randomize 纪律——「初始化随机落定禁逐轮重掷」圣经原则三落点:
# 同格坐标恒同值,refresh/重绘/逐轮不变)。乘加混合 + 移位雪崩;64 位整型乘法回绕即哈希语义
#(GDScript int 溢出静默回绕),板域 10×10/12×12 内 mod 10/mod 5 分布均匀
static func cell_hash(c: Vector2i) -> int:
var h := c.x * 374761393 + c.y * 668265263
h = (h ^ (h >> 13)) * 1274126177
return h ^ (h >> 16)
# 四正交邻墙判定(词表 §三.1:板外视为墙;未开格=墙、已开格=地板——世界观口径「未开=墙、
# 已开=挖出的空间」)。唯一调用面 wall_tile_for/wall_center_tile(棋盘视图 refresh 对
# 排雷玩法非空守卫在先)
func is_wall(c: Vector2i) -> bool:
if c.x < 0 or c.y < 0 or c.x >= board.width or c.y >= board.height:
return true
return not minesweeper.is_revealed(c)
# 四邻地板掩码(位序 = nf<<3|ef<<2|sf<<1|wf,见 WALL_MASK_TILE)
func _neighbor_mask(c: Vector2i) -> int:
var m := 0
if not is_wall(c + Vector2i(0, -1)):
m |= 8
if not is_wall(c + Vector2i(1, 0)):
m |= 4
if not is_wall(c + Vector2i(0, 1)):
m |= 2
if not is_wall(c + Vector2i(-1, 0)):
m |= 1
return m
# 墙瓦片查表(词表 §一 16 正交情形映射 + §三.2 凹角回落):nf/ef/sf/wf = 北/东/南/西邻是地板。
# 16 情形 = 0 地板(center 族)+ 3 地板×4(端头)+ 2 地板×6(条带/凸角)+ 1 地板×4(单缘)
# + 4 地板(对角凹角/中心三变体),逐键穷尽
func wall_tile_for(c: Vector2i) -> Texture2D:
var mask := _neighbor_mask(c)
if mask == 15:
return wall_center_tile(c) # 四邻皆地板 → 对角凹角/中心三变体(下位函数)
return tiles[WALL_MASK_TILE[mask]]
# 四邻皆地板格:对角凹角优先 nw > ne > sw > se;无对角地板 → 中心三变体按格坐标哈希 60/20/20
func wall_center_tile(c: Vector2i) -> Texture2D:
for i in WALL_DIAG_DIRS.size():
var d: Vector2i = WALL_DIAG_DIRS[i]
if not is_wall(c + d):
return tiles[WALL_DIAG_TILES[i]]
return tiles[WALL_CENTER_PICK[posmod(cell_hash(c), 10)]]
# 地面瓦片五变体按格坐标哈希均匀分配(词表 §一 floor_1..5 / §三.3「同法哈希分配」;
# posmod 保非负下标)
func floor_tile_for(c: Vector2i) -> Texture2D:
return floor_tiles[posmod(cell_hash(c), floor_tiles.size())]