- tools/recolor_terrain_tiles.py: 槽位家族保持重映射(源唯一色→最近 dungeon 锚定家族→家族内亮度秩序线性映射目标色表);目标锚=归档生成器 PALETTES(圣经 §4.3 三关方向,三关换色证明条在案);确定性零随机;源=盘上 v9.3 定稿 21 枚(活件未动) - 产物: game/assets/art/board/terrain_*_underdark|deep_underdark.png 各 21 枚(素材不入 git 与 dungeon 套一致,.import 编辑器刷) - BoardView: 瓦片路径按 board_type 后缀参数化(词表名单单点 TILE_WALL_NAMES);rebuild 按板图型切换(_tiles_board_type 守卫零重载);套缺位→整批回落 dungeon(禁同板混渲染),dungeon 亦缺位→程序化平色原回退 - 机检: terrain_tile_sets_test 入 manifest(第 9 套件)——词表 3 型×套完整性(16 墙+5 地面)+墙顶色相域抽检(dungeon 0.089/underdark 0.625/deep 0.639 各落圣经方向带)+图底分离×3+deep 更暗方向;素材工作区缺位=SKIP 不红(不入 git 纪律) - 轻闸: 冒烟 557/0+ui_input_probe(缺位回退断言保持)+ui_click_probe+9 套件全绿
155 lines
6.8 KiB
Python
155 lines
6.8 KiB
Python
# tools/recolor_terrain_tiles.py — 高墙/地面瓦片程序换色(T6,M-1 迭代 1)
|
||
# 用法:
|
||
# python tools/recolor_terrain_tiles.py # 产 underdark + deep_underdark 两套到源目录
|
||
# python tools/recolor_terrain_tiles.py --palettes underdark
|
||
# 事实源:
|
||
# - 源瓦片 = game/assets/art/board/terrain_*_dungeon.png(v9.3 定稿,active.md 台账 164;
|
||
# 素材不入 git——本脚本读盘上现件,勿删活件)
|
||
# - 目标色表 = _tools/gen_terrain_walls.py PALETTES(归档 归档-terrain-walls-2026-09-26.zip 内
|
||
# 活件;underdark/deep_underdark 槽值系圣经 §4.3 三关方向、三关换色证明条在案的已过闸色表)
|
||
# 方法:槽位家族保持的重映射——源图每唯一色 → 最近 dungeon 锚槽(RGB 距离)定家族(墙顶/立面/
|
||
# 地面/雷坑);家族内按亮度秩序线性映射到目标色表同家族槽列。亮度秩序 = 图底分离与结构读数
|
||
# 的承载面,逐像素保持;输出重排唯一色数与源一致(量化纪律)。确定性零随机。
|
||
from __future__ import annotations
|
||
import argparse
|
||
import os
|
||
import sys
|
||
from PIL import Image
|
||
|
||
# ── dungeon 锚槽(v9.3 生成器同款——家族归属判定用)──
|
||
DUNGEON = {
|
||
"OUT": (43, 34, 30), "D2": (76, 61, 49), "D1": (122, 101, 78),
|
||
"M": (139, 117, 91), "H1": (156, 133, 103), "H2": (181, 155, 119),
|
||
"F_OUT": (26, 21, 19), "F3": (36, 29, 26), "F2": (49, 39, 34),
|
||
"F1": (63, 51, 44), "FH": (88, 71, 56),
|
||
"G_BASE": (88, 66, 50), "G_LIT": (108, 76, 54), "G_DARK": (75, 61, 50),
|
||
"G_CHIP": (134, 82, 52), "G_STONE": (114, 98, 82), "G_STONE_D": (84, 72, 60),
|
||
"P_HOLE": (17, 25, 24), "P_DEEP": (12, 17, 17), "P_GLINT": (33, 58, 48),
|
||
"P_GLINT2": (45, 70, 56), "P_RIM_D": (40, 30, 26), "P_RIM_L": (129, 92, 65),
|
||
}
|
||
FAMILY_OF = {}
|
||
for s in ["OUT", "D2", "D1", "M", "H1", "H2"]:
|
||
FAMILY_OF[s] = "wall"
|
||
for s in ["F_OUT", "F3", "F2", "F1", "FH"]:
|
||
FAMILY_OF[s] = "facade"
|
||
for s in ["G_BASE", "G_LIT", "G_DARK", "G_CHIP", "G_STONE", "G_STONE_D"]:
|
||
FAMILY_OF[s] = "floor"
|
||
for s in ["P_HOLE", "P_DEEP", "P_GLINT", "P_GLINT2", "P_RIM_D", "P_RIM_L"]:
|
||
FAMILY_OF[s] = "crater"
|
||
|
||
PALETTES = {
|
||
"underdark": {
|
||
"OUT": (36, 38, 50), "D2": (72, 78, 96), "D1": (108, 116, 138),
|
||
"M": (138, 145, 166), "H1": (162, 170, 190), "H2": (186, 193, 210),
|
||
"F_OUT": (20, 22, 30), "F3": (30, 33, 44), "F2": (42, 46, 60),
|
||
"F1": (56, 61, 78), "FH": (80, 87, 108),
|
||
"G_BASE": (62, 66, 80), "G_LIT": (76, 82, 98), "G_DARK": (52, 55, 68),
|
||
"G_CHIP": (96, 84, 122), "G_STONE": (84, 90, 108), "G_STONE_D": (62, 66, 80),
|
||
"P_HOLE": (14, 15, 22), "P_DEEP": (9, 10, 15), "P_GLINT": (38, 40, 66),
|
||
"P_GLINT2": (52, 56, 86), "P_RIM_D": (32, 34, 44), "P_RIM_L": (110, 116, 140),
|
||
},
|
||
"deep_underdark": {
|
||
"OUT": (24, 25, 36), "D2": (48, 51, 70), "D1": (74, 79, 104),
|
||
"M": (98, 104, 132), "H1": (124, 131, 160), "H2": (150, 158, 186),
|
||
"F_OUT": (13, 13, 20), "F3": (20, 21, 32), "F2": (29, 31, 46),
|
||
"F1": (40, 43, 62), "FH": (58, 63, 88),
|
||
"G_BASE": (46, 48, 64), "G_LIT": (62, 66, 86), "G_DARK": (38, 40, 54),
|
||
"G_CHIP": (88, 72, 120), "G_STONE": (88, 94, 116), "G_STONE_D": (60, 64, 82),
|
||
"P_HOLE": (8, 8, 14), "P_DEEP": (5, 5, 9), "P_GLINT": (28, 28, 52),
|
||
"P_GLINT2": (40, 42, 70), "P_RIM_D": (22, 23, 34), "P_RIM_L": (84, 90, 116),
|
||
},
|
||
}
|
||
|
||
|
||
def _lum(c):
|
||
return 0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]
|
||
|
||
|
||
def _dist(a, b):
|
||
return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2
|
||
|
||
|
||
def _lerp(a, b, t):
|
||
return tuple(int(round(a[i] + (b[i] - a[i]) * t)) for i in range(3))
|
||
|
||
|
||
def build_map(src_colors, target_palette):
|
||
"""源唯一色 → 目标色。家族归属 = 最近 dungeon 锚;家族内按亮度秩序映射到目标同家族槽列。"""
|
||
fam_anchors = {} # family -> [(lum, slot, rgb)] 升序
|
||
for slot, fam in FAMILY_OF.items():
|
||
fam_anchors.setdefault(fam, []).append((_lum(DUNGEON[slot]), slot, DUNGEON[slot]))
|
||
for fam in fam_anchors:
|
||
fam_anchors[fam].sort()
|
||
tgt_ramps = {}
|
||
for fam in fam_anchors:
|
||
slots = [( _lum(target_palette[s]), s, target_palette[s]) for s in FAMILY_OF if FAMILY_OF[s] == fam]
|
||
slots.sort()
|
||
tgt_ramps[fam] = slots
|
||
mapping = {}
|
||
fam_members = {}
|
||
for c in src_colors:
|
||
best_fam, best = None, None
|
||
for fam, anchors in fam_anchors.items():
|
||
for _, slot, rgb in anchors:
|
||
d = _dist(c, rgb)
|
||
if best is None or d < best:
|
||
best, best_fam = d, fam
|
||
fam_members.setdefault(best_fam, []).append(c)
|
||
for fam, members in fam_members.items():
|
||
members.sort(key=_lum)
|
||
ramp = tgt_ramps[fam]
|
||
n_s, n_t = len(members), len(ramp)
|
||
for i, c in enumerate(members):
|
||
t = 0.0 if n_s <= 1 else i / (n_s - 1)
|
||
pos = t * (n_t - 1)
|
||
lo = int(pos)
|
||
hi = min(lo + 1, n_t - 1)
|
||
mapping[c] = _lerp(ramp[lo][2], ramp[hi][2], pos - lo)
|
||
return mapping
|
||
|
||
|
||
def remap_tile(src_path, out_path, target_palette):
|
||
im = Image.open(src_path).convert("RGBA")
|
||
px = im.load()
|
||
w, h = im.size
|
||
colors = sorted({px[x, y][:3] for y in range(h) for x in range(w)})
|
||
mapping = build_map(colors, target_palette)
|
||
for y in range(h):
|
||
for x in range(w):
|
||
r, g, b, a = px[x, y]
|
||
nr, ng, nb = mapping[(r, g, b)]
|
||
px[x, y] = (nr, ng, nb, a)
|
||
im.save(out_path)
|
||
return len(colors)
|
||
|
||
|
||
def main():
|
||
here = os.path.dirname(os.path.abspath(__file__))
|
||
default_board = os.path.normpath(os.path.join(here, "..", "game", "assets", "art", "board"))
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--src", default=default_board)
|
||
ap.add_argument("--out", default=default_board)
|
||
ap.add_argument("--palettes", default="underdark,deep_underdark")
|
||
args = ap.parse_args()
|
||
# 墙瓦片 = terrain_*_dungeon.png;地板变体 = terrain_floor_dungeon_<i>.png(数字后缀——词表 §一)
|
||
import re
|
||
pat = re.compile(r"^terrain_.*_dungeon(_\d+)?\.png$")
|
||
sources = sorted(f for f in os.listdir(args.src) if pat.match(f))
|
||
if not sources:
|
||
print("[recolor] 源目录无 terrain_*_dungeon* 瓦片——素材缺位(不入 git 纪律),零输出", file=sys.stderr)
|
||
return 1
|
||
for pal in args.palettes.split(","):
|
||
P = PALETTES[pal]
|
||
n_out = 0
|
||
for f in sources:
|
||
out_name = f.replace("_dungeon", "_%s" % pal)
|
||
n = remap_tile(os.path.join(args.src, f), os.path.join(args.out, out_name), P)
|
||
n_out += 1
|
||
print("[recolor] %s <- %s(唯一色 %d)" % (out_name, f, n))
|
||
print("[recolor] %s:%d 枚完成" % (pal, n_out))
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|