Files
gplan-2/tools/recolor_terrain_tiles.py
DKChan 066a9f7e31 docs(active残留): active.md 废除补刀——10-18 五张待执行卡摘「唯一例外=active.md」假指令15处+baseline.json 08b 死记录再删+recolor_terrain_tiles.py 头注出处注记摘除
08b 复活成因:roadmap 压缩批 8f66ae15 与废除提交 27812167 同日、worktree 分叉早于废除,
合回时旧 baseline 被原样带回(该批自记「其余条目原样保留」)——再删并留此记录防三犯。
历史文档约 19 处过去时引用按 registry 注销批(d54cbed7)先例保留不动;
在途分支合回时 diff 再见 08b/active.md 一律就地拒收。
2026-10-20 00:39:26 +08:00

155 lines
6.7 KiB
Python
Raw Permalink 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 — 高墙/地面瓦片程序换色
# 用法:
# 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 定稿;
# 素材不入 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())