①--fixer: rust pixelfixer(D:/code/pixel-art-fixer,MIT)收网格(网格检测+重建)+封闭灰袋清除收编(三闸=近底色∧中性灰∧组件不触图缘,防误伤盔甲与影;09-28 手工批次正式化,遗留项清偿) ②--standee48: 九张 48px 槽位档(八敌 pixel-1+主角 normal_1x→character_enemy_*/character_hero_portrait)——统一比例最大体量归 48(相对体量语义=字形回退同构)/底中锚脚贴底缘/块内众数投票降采样(零插值,输出色⊂源色板;NEAREST 直缩 0.25x 糊,goblin A/B 实证,代拍板待追认)/超宽按宽收兜底 ③tools/standee_asset_check.py 机检: 高 48/宽≤75/底锚/≤32 色/alpha 二值 五断言,必需集=八敌+主角三态(attack/hit 缺位=T3 范围暂 MISS) 产物九张已落 worktree game/assets/art/character/(gitignored 不入库);机检 9/9 过;轻闸=冒烟 576/0+三探针 PASS 零退化
80 lines
2.8 KiB
Python
80 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""站桩槽位素材机检(M-3 迭代 6 T1——48px 档契约断言;重闸素材面闸)。
|
||
|
||
四断言(契约=计划书 C4/圣经 §8.2):
|
||
尺寸 高恒 48(槽位源档 ×2 整数放大=盒高 96);宽 1..75(盒宽 150 半值)。
|
||
锚点 底行有不透明像素(底中锚——纵向比例自底边口径,脚贴画布底缘)。
|
||
色数 ≤32(fixer 批 32 色冻结;NEAREST 采样不产生新色)。
|
||
无插值 alpha 二值(仅 0/255——双线性/混合会在边缘产生中间 α)。
|
||
|
||
用法: python tools/standee_asset_check.py [角色素材目录]
|
||
默认 game/assets/art/character/;必需集=八敌+主角 portrait/attack/hit。
|
||
"""
|
||
import os
|
||
import sys
|
||
from collections import Counter
|
||
from PIL import Image
|
||
|
||
REQUIRED = ["character_enemy_%s.png" % k for k in
|
||
("bugbear", "drider", "duergar", "duergar_king", "giant_spider",
|
||
"goblin", "hook_horror", "ogre_chief")] + \
|
||
["character_hero_portrait.png", "character_hero_attack.png", "character_hero_hit.png"]
|
||
SLOT_H = 48
|
||
W_MAX = 75
|
||
COLOR_MAX = 32
|
||
|
||
|
||
def check(path):
|
||
problems = []
|
||
im = Image.open(path).convert("RGBA")
|
||
w, h = im.size
|
||
if h != SLOT_H:
|
||
problems.append("高应 48,实得 %d" % h)
|
||
if not (1 <= w <= W_MAX):
|
||
problems.append("宽应 1..%d,实得 %d" % (W_MAX, w))
|
||
px = im.load()
|
||
colors = set()
|
||
alphas = set()
|
||
bottom_opaque = 0
|
||
for y in range(h):
|
||
for x in range(w):
|
||
c = px[x, y]
|
||
alphas.add(c[3])
|
||
if c[3] > 0:
|
||
colors.add(c[:3])
|
||
if y == h - 1:
|
||
bottom_opaque += 1
|
||
if bottom_opaque < 1:
|
||
problems.append("底行无不透明像素(底中锚失守——脚未贴画布底缘)")
|
||
if len(colors) > COLOR_MAX:
|
||
problems.append("色数 %d > %d" % (len(colors), COLOR_MAX))
|
||
if not alphas <= {0, 255}:
|
||
problems.append("alpha 非二值(插值痕迹:%s)" % sorted(alphas - {0, 255})[:5])
|
||
return problems, (w, h, len(colors))
|
||
|
||
|
||
def main():
|
||
root = sys.argv[1] if len(sys.argv) > 1 else os.path.join("game", "assets", "art", "character")
|
||
missing = [f for f in REQUIRED if not os.path.isfile(os.path.join(root, f))]
|
||
n_fail = 0
|
||
for f in REQUIRED:
|
||
p = os.path.join(root, f)
|
||
if not os.path.isfile(p):
|
||
print("MISS %-36s 缺位" % f)
|
||
n_fail += 1
|
||
continue
|
||
problems, info = check(p)
|
||
if problems:
|
||
n_fail += 1
|
||
for pr in problems:
|
||
print("FAIL %-36s %s" % (f, pr))
|
||
else:
|
||
print("PASS %-36s %dx%d 色数%d" % (f, info[0], info[1], info[2]))
|
||
print("机检完成:%d 件必需,%d 件未过" % (len(REQUIRED), n_fail))
|
||
sys.exit(1 if n_fail else 0)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|