r2-1 瓦片族(排版+素材装载+墨族判据+查表)→ views/tile_palette.gd; r2-2 英雄族(贴图/位移/影/朝向/缩放几何)→ views/hero_sprite.gd(Node2D); r2-3 board_view 改引+直引面换指(terrain_tile_sets_test/ui_contrast_probe), 探针与 main 的组合根直读面经转发属性/常量别名保持零改动。 S13 计数守恒:基线含类变量数/公有方法数两条计数型键,故 board_view 用 13 个转发属性 +2 个组件引用压回原计数(45 变量/27 公有方法);baselined 的 wall_tile_for/ wall_center_tile/digit_ink_mode 以查表/辅助函数重构压回阈值内(非改名保违例), 旧文件级键随函数迁出自然失效——基线文件零改动(用户令:只改代码不改基线)。 验收:terrain_tile_sets_test 19 检查绿(判据读数与迁移前同值);gdstyle 基线外新违规 0; S9/S10 绿;probe_gate --click 全绿(含 ui_input_probe 十八断言/click 走查 0 问题); render_gate 棋盘三图+菜单 golden changed=0.00%;撤销证明:测试改引回退旧址符号 → SCRIPT ERROR(Parse Error: Static function "_normalize_floor_luma()" not found)+ error_scan exit 1,还原后 19 检查复绿、error_scan exit 0。
262 lines
14 KiB
GDScript
262 lines
14 KiB
GDScript
## 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_SEAL_MARK := "res://assets/art/board/seal_mark.png" # 封印印记纹理(独立可选件)
|
||
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 seal_tex: Texture2D = null # 封印印记纹理(缺盘=null=字形回退——独立可选件不连瓦片整批)
|
||
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
|
||
# 封印印记纹理(独立可选件:缺盘=字形回退,不触发瓦片整批回落语义)
|
||
seal_tex = UiKit.load_tex(TILE_SEAL_MARK)
|
||
|
||
|
||
# 整批回落:请求套缺位 → 回落 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())]
|