Files
gplan-2/tools/recolor_terrain_tiles.py
T
DKChan f2642a87a6 feat(art): T6 underdark/deep_underdark 瓦片换色——程序换色产 L2/L3 两套入包+机检(台账164 换色遗留清偿)
- 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 套件全绿
2026-09-28 03:42:15 +08:00

155 lines
6.8 KiB
Python
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.
# 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())