lili c1d1e2b400 feat(art): 《山海行纪》资产一期批产(锚 r1)——112 镜头 2.72MB 图集入库
矿彩国风锚 r1 落地:P1 80 镜头(图 1 tileset+三常规兽各 6 帧+词缀精英/光环/双 Boss 22 帧含立绘+UI 13+底图)
+P2 44 镜头(主角 13 帧定妆帧参考图法跨帧身份一致+法器图标 12+弹道命中特效 15+拾取 4);
5 图集 xingji-{map1,beasts,boss,ui,actors}+tiles+bg 共 2,911,369B≈2.72MB(首屏切面≈2.03MB,boss 页 1.03MB=
懒加载分包面,分包决策归开发席 M4);机检 C1–C5 全绿。累计 127 呼 ¥4.19、首过 92%,远离升档 B 判据;
clean-room 五条全程(兽名不入 prompt/伪字物件化改词)。
主会话修机检门两态口径:C2/C3 keyed 中间件只活在批产工作区(ASSET_WORK 不入仓),整目录缺席=仓库态
复跑降级警示跳过、部分缺席仍判错(消除席位环境绿/CI 必红假门,仓库态复跑 rc=0);
顺手修 asset-atlas 局部重打包整写 manifest 丢条目缺陷。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 04:27:00 -07:00

89 lines
4.4 KiB
Python

#!/usr/bin/env python3
# 《山海行纪》资产批产 · 拼板图(迁移自批次一 contact-sheet.py;眼验入口)
# 用法: python3 scripts/asset-contact-sheet.py <输出.png> [--prio P1] [--group beasts] [--cols 8] [--title 标题] [--raw]
# python3 scripts/asset-contact-sheet.py --files <输出.png> <标题> <标签=路径>...
# 默认取 keyed 后精灵(所见即入图集帧);--raw 取生图原片;bleed 镜头(tiles/bg)始终取原片。
# 中文标题字体:环境变量 FONT_TTF 或批次一同款霞鹜文楷(OFL)。
import sys, os, json, tempfile
from PIL import Image, ImageDraw, ImageFont
GAME = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
WORK = os.environ.get('ASSET_WORK') or os.path.join(tempfile.gettempdir(), 'xingji-a1-work')
BATCH1 = os.path.normpath(os.path.join(GAME, '..', '..', '..', 'docs-design', '山海风格版-批次一'))
FONT_TTF = os.environ.get('FONT_TTF') or '/private/tmp/claude-501/-Users-lili-Project-games-development-ai/69248bd4-16ac-4906-aea1-4157edf7b10f/scratchpad/deps/LXGWWenKai-Regular.ttf'
def load_font(size):
try: return ImageFont.truetype(FONT_TTF, size)
except Exception: return ImageFont.load_default()
def make_sheet(items, out_path, cols, title, tile_w=300, tile_h=330):
label_h, title_h, gap = 26, 52, 8
rows = (len(items) + cols - 1) // cols
W = cols * (tile_w + gap) + gap
H = title_h + rows * (tile_h + label_h + gap) + gap
sheet = Image.new('RGB', (W, H), (30, 34, 32))
d = ImageDraw.Draw(sheet)
d.text((gap + 4, 10), title, fill=(232, 197, 106), font=load_font(28))
f_label = load_font(17)
for i, (label, path) in enumerate(items):
r, c = divmod(i, cols)
x = gap + c * (tile_w + gap)
y = title_h + r * (tile_h + label_h + gap)
try:
im = Image.open(path)
if im.mode == 'RGBA': # 透明底铺浅格,眼验抠图质量
bgc = Image.new('RGB', im.size, (58, 62, 58))
bgc.paste(im, (0, 0), im)
im = bgc
else:
im = im.convert('RGB')
except Exception as e:
d.text((x, y), f'{label}: 打开失败 {e}', fill=(200, 80, 80), font=f_label); continue
im.thumbnail((tile_w, tile_h))
sheet.paste(im, (x + (tile_w - im.width) // 2, y + (tile_h - im.height) // 2))
d.rectangle([x, y + tile_h, x + tile_w, y + tile_h + label_h], fill=(20, 23, 21))
d.text((x + 5, y + tile_h + 3), label, fill=(220, 220, 210), font=f_label)
sheet.save(out_path)
print(f'拼板完成:{out_path} ({W}x{H}, {len(items)} 格, {os.path.getsize(out_path)//1024}KB)')
def locate(shot, prefer_raw):
"""镜头 → 现存图:keyed 优先(sprite),bleed/raw 回原片,adopt 回批次一源。"""
cands = []
if shot['type'] == 'sprite' and not prefer_raw:
cands.append(os.path.join(WORK, 'keyed', shot['group'], shot['id'] + '.png'))
for ext in ('.jpg', '.jpeg', '.png', '.webp'):
cands.append(os.path.join(WORK, 'raw', shot['id'] + ext))
if shot.get('adopt'):
cands.append(os.path.join(BATCH1, shot['adopt']))
for p in cands:
if os.path.exists(p): return p
return None
if __name__ == '__main__':
if len(sys.argv) > 1 and sys.argv[1] == '--files':
out, title = sys.argv[2], sys.argv[3]
items = [(kv.split('=', 1)[0], kv.split('=', 1)[1]) for kv in sys.argv[4:]]
make_sheet(items, out, cols=min(6, max(1, len(items))), title=title)
sys.exit(0)
out = sys.argv[1]
argv = sys.argv[2:]
prio = group = None; cols = 8; title = '山海行纪 · 资产一期'; raw = False
i = 0
while i < len(argv):
if argv[i] == '--prio': prio = argv[i+1]; i += 2
elif argv[i] == '--group': group = argv[i+1]; i += 2
elif argv[i] == '--cols': cols = int(argv[i+1]); i += 2
elif argv[i] == '--title': title = argv[i+1]; i += 2
elif argv[i] == '--raw': raw = True; i += 1
else: sys.exit(f'未知参数 {argv[i]}')
shots = json.load(open(os.path.join(GAME, 'scripts', 'asset-shots.json'), encoding='utf-8'))['shots']
items, missing = [], []
for s in shots:
if prio and s['prio'] != prio: continue
if group and s['group'] != group: continue
p = locate(s, raw)
if p: items.append((s['id'], p))
else: missing.append(s['id'])
if missing: print(f"缺图 {len(missing)}:{','.join(missing)}")
make_sheet(items, out, cols=cols, title=title)