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 与旧档逐字节相同(主角档零漂移实证)。
158 lines
5.9 KiB
Python
158 lines
5.9 KiB
Python
#!/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() |