Files
gplan-2/tools/palette_check.py
T
DKChan b163341915 feat(tools): B段机检三件落地 + 逐键定档硬闸升级(立绘定档批)
B1 standee_asset_check 硬闸升级(圣经 §8.3③ 挂点规格落地):
- 逐键断言档高(原「高恒48」退役)——档高表改为 import pixelize_candidates.
  STANDEE_TIER_H,守定值单点纪律,检查器内不复写第二份数字;
- 宽度约束由虚设区间「1..75」改为物理约束「源宽×2 ≤ 站桩盒宽150」
  (standee_body.gd:29 box.x=150 / :237 scale=2.0);
- 色数 ≤32,**排除全透明像素**——naive set统计会把 (0,0,0,0) 计成一色
  ⇒ 报33 假红灯(2026-10-07 实踩),口径已固化进docstring;
- 新增描边亮度断言(character-design §10 亮描边暖白族/禁黑描边),
  阈值 40% 由 _tmp_probe_outline.py 实测标定(敌人实测 54.1–84.3%);
  主角三态实测≈0.4% 属已知缺口,本闸豁免不判红,另立素材批修。
- 负向验证:喂旧 48 档 ⇒ 8 件红灯(逐键档高+2 件描边不足),闸确有效。
- 新增 --json 输出。

B2 palette_check 新建:§4.1 新色双门执法器。**两处口径订正写进 docstring**:
① 48 色上限治的是设计色板 token,不是每张 PNG 色数(初版误套⇒12/12 假红);
② ≥8/255 近色门只管「新增色 vs 既有色板」,不是存量两两互查
  (初版误查存量⇒ drider22 组/巨蜘蛛 82 组噪声)。无 --baseline 时只读不判红。

B3 path_check 新建:asset-path-map 三层核对(目录/文件/res://引用/退役命名)。
口径订正:① res:// 根 = game/ 目录(project.godot 处),非仓库根;
② printf 路径模板(terrain_%s_%d)非字面路径,跳过;
③ 退役命名只扫代码+素材清单,且命中行含「退役/更正/改判」等记述词则豁免
  (初版全仓扫 .md ⇒ active.md/记忆文件里的历史记述全被误判,64 假红);
④ 素材环境三态区分(full/partial/empty)——worktree 素材靠人工拷贝同步,
  partial 态缺项判「未同步」而非「悬空」(character/ 已拷、fx|board|ui 未拷)。
- 首跑读数:主区 1 处真问题 = art-bible §8.5 参考核验条仍指废路径
  production/mockups/(v3.9 批更正了 4 处,此条漏网)——本批已修。

A0/A1 实测读数归档:八敌新档逐键命中 STANDEE_TIER_H(36/38/46/58/62/74/90/94),
主角三态 md5 与旧档逐字节相同(主角档零漂移实证)。
2026-10-07 18:39:31 +08:00

158 lines
5.9 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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""B2 色板检查器(圣经 §8.3①挂点实现 · 立绘定档批 2026-10-07)。
⚠⚠ 口径纪律(2026-10-07 实跑后订正,**勿改回**)——两处极易写错:
1. **48 色上限不是「每张 PNG 的色数」**。§4.1 的「全作色板硬上限 48 色」指
**设计色板 token**(UI 语义色/墨色/材质色那套),不是像素画的独立色数。
像素画单张另有**32 色冻结**口径,由 standee_asset_check 管(像素域)。
初版把 48 套到每张 PNG 上⇒ 12 张全红(误报率 100%),已作废。
本器改为:单张色数**只报读数不判红**;48 只在 --tokens 模式下校验色板表。
2. **近色 ≥8/255 是「新色入板双门」,不是存量两两互查**。§4.1 原文:
「**新色**入板须同时过两道门……②与既有任一色的 RGB 三通道差 ≥8/255」。
它管的是**新增色 vs 既有色板**。对存量色两两互查会把 AI 生成的明暗渐变
全部打成近色(实测 drider 22 组 / 巨蜘蛛 82 组)⇒ 噪声淹没真问题。
本器改为:近色**只对新增色集**判(新增色 vs 基线色板)。
另:必须排除全透明像素 (0,0,0,0)(同standee_asset_check 的坑)。
用法:
# 主用法:扫目录,新增色相对 --baseline 判定
python tools/palette_check.py <目录> --baseline <基线目录>
# 无基线:只出读数(单张色数/ 近色对计数),不判红
python tools/palette_check.py <目录>
退出码: 0 通过 / 1 有红灯(仅新增色违规或超单张上限时)
"""
import os
import sys
from PIL import Image
# §4.1 硬上限(**设计色板 token 口径**,非像素画单张色数)
TOKEN_MAX = 48
NEAR_TOL = 8 # §4.1 新色双门:与既有任一色三通道差 ≥8/255
# 像素画单张色数(像素域32 色冻结)——由 standee_asset_check 执法,
# 本器只作读数提示,避免两处口径打架
PIXEL_COLOR_SOFT_MAX = 32
def palette_of(path):
"""返回 (不透明像素唯一色集合, 像素数)。排除全透明。"""
im = Image.open(path).convert("RGBA")
px = im.load()
w, h = im.size
cols = set()
n = 0
for y in range(h):
for x in range(w):
c = px[x, y]
if c[3] == 0:
continue
cols.add(c[:3])
n += 1
return cols, n
def nearest_to(col, pool, tol):
"""返回 col 在 pool 中最近的色及其差值(差 < tol 算近色)。"""
best, bd = None, 999
for c in pool:
d = max(abs(col[k] - c[k]) for k in range(3))
if d < bd:
best, bd = c, d
return best, bd
def hexs(c):
return "#%02X%02X%02X" % c
def scan(d, baseline_cols, report_only):
files = []
for dirpath, _, names in os.walk(d):
for fn in sorted(names):
if fn.lower().endswith(".png"):
files.append(os.path.join(dirpath, fn))
files.sort()
print("=== %s(%d 张 PNG)===" % (d, len(files)))
n_fail = 0
all_new = set()
for p in files:
cols, npx = palette_of(p)
rel = os.path.relpath(p, d)
notes = []
msgs = []
over = len(cols) > PIXEL_COLOR_SOFT_MAX
notes.append("色%d%s" % (len(cols), "(>32 像素域冻结)" if over else ""))
new = set()
if baseline_cols is not None:
new = cols - baseline_cols
if new:
# §4.1 新色双门②:新增色须与既有任一色差 ≥8/255
bad = []
for c in sorted(new):
near, d3 = nearest_to(c, baseline_cols, 999)
if d3 < NEAR_TOL:
bad.append((c, near, d3))
all_new |= new
notes.append("新增%d" % len(new))
for c, near, d3 in bad[:5]:
msgs.append("新增色 %s 与既有 %s 三通道差 %d < %d"
"(§4.1 新色双门②,须走换色合并)"
% (hexs(c), hexs(near), d3, NEAR_TOL))
if len(bad) > 5:
msgs.append("…… 另有 %d 个新增色同样不过门" % (len(bad) - 5))
else:
notes.append("新增0")
line = "FAIL " if msgs else ("WARN " if (report_only and over) else "PASS ")
print("%s %-46s %s" % (line, rel, " ".join(notes)))
for m in msgs:
print(" └─ %s" % m)
n_fail += 1 if msgs else 0
print("小计:%d 张,%d 张有红灯\n" % (len(files), n_fail))
return n_fail, len(files), all_new
def main():
args = sys.argv[1:]
baseline = args[args.index("--baseline") + 1] if "--baseline" in args else None
dirs = [a for a in args if not a.startswith("--")
and os.path.isdir(a) and a != baseline]
if not dirs:
print(__doc__)
sys.exit(2)
baseline_cols = None
if baseline:
baseline_cols = set()
for dirpath, _, names in os.walk(baseline):
for fn in names:
if fn.lower().endswith(".png"):
cols, _ = palette_of(os.path.join(dirpath, fn))
baseline_cols |= cols
print("基线色板:%s,唯一色 %d" % (baseline, len(baseline_cols)))
total_fail = total_files = 0
total_new = set()
report_only = baseline_cols is None
for d in dirs:
f, n, new = scan(d, baseline_cols, report_only)
total_fail += f
total_files += n
total_new |= new
print("色板机检完成:%d 张 PNG,%d 张有红灯,累计新增色 %d"
% (total_files, total_fail, len(total_new)))
if report_only:
print("提示:未给 --baseline ⇒ 本次为只读模式(不判红)。"
"『色数>32』标 WARN 属像素域提示,最终执法在 standee_asset_check。")
sys.exit(1 if total_fail else 0)
if __name__ == "__main__":
main()