Play liveChoose your opponent
Join
54be506
100 lines · 5.3 KB Raw Permalink History
1"""Builds the viewer's art pack, viewer/art/, from a sparse clone of 无名杀 (libnoname/noname): the portraits and card
2art that src/viewer/noname/art.json maps the upstream engine's generals and cards to, the woodden theme's pieces and
3its fonts cut down to the characters the viewer draws with them. Needs Pillow (with WebP) and fonttools.
4
5 git clone --filter=blob:none --no-checkout https://github.com/libnoname/noname <noname>
6 git -C <noname> sparse-checkout set --no-cone $(python3 src/art.py --paths)
7 git -C <noname> checkout 90b58bb9934edb1712fd187a80c8323b7aeabaf3
8 python3 src/art.py <noname>
9"""
10import io, json, os, re, shutil, sys
11from PIL import Image
12from fontTools import subset
13
14SRC = os.path.dirname(os.path.abspath(__file__))
15GAME = os.path.dirname(SRC)
16OUT = os.path.join(GAME, 'viewer', 'art')
17PORTRAIT_W, PORTRAIT_KB, CARD_KB = 300, 60, 30
18THEME = [('theme/woodden/grid.png', 'grid.png'), ('theme/woodden/wood2.png', 'wood2.png'), ('theme/woodden/wood.png', 'wood.png'),
19 ('theme/woodden/wood2.jpg', 'wood2.jpg'), ('image/card/handcard.png', 'handcard.png'), *[(f'theme/style/hp/image/glass{i}.png', f'hp{i}.png') for i in range(1, 5)]]
20FONTS = ('xinwei', 'huangcao')
21
22
23def webp(im, path, limit_kb, q=82):
24 """Save as WebP at the best quality that fits the size limit."""
25 while True:
26 buf = io.BytesIO()
27 im.save(buf, 'WEBP', quality=q, method=6)
28 if buf.tell() <= limit_kb * 1024 or q <= 40:
29 break
30 q -= 6
31 with open(path, 'wb') as f:
32 f.write(buf.getvalue())
33 return buf.tell()
34
35
36def main():
37 art = json.load(open(os.path.join(SRC, 'viewer', 'noname', 'art.json'), encoding='utf-8'))
38 if sys.argv[1] == '--paths': # what the sparse clone needs
39 paths = [f'image/character/{c}.jpg' for c in sorted(set(art['generals'].values()))]
40 paths += [f'image/card/{c}.png' for c in sorted(set(art['cards'].values()))]
41 paths += [src for src, _ in THEME] + ['theme/woodden/wood.jpg', 'theme/style/cardback/image/official.png']
42 paths += [f'font/{f}.woff2' for f in (*FONTS, 'suits')]
43 print('\n'.join(f'/apps/core/{p}' for p in paths))
44 return
45 core = os.path.join(sys.argv[1], 'apps', 'core')
46 shutil.rmtree(OUT, ignore_errors=True)
47 for d in ('general', 'card', 'theme', 'font'):
48 os.makedirs(os.path.join(OUT, d))
49 sizes = {}
50
51 # Portraits: 无名杀's are 350x464; kept whole (the seat card crops with CSS), 300 wide.
52 for cid in sorted(set(art['generals'].values())):
53 im = Image.open(os.path.join(core, 'image', 'character', f'{cid}.jpg')).convert('RGB')
54 im = im.resize((PORTRAIT_W, round(im.height * PORTRAIT_W / im.width)), Image.LANCZOS)
55 sizes[f'general/{cid}'] = webp(im, os.path.join(OUT, 'general', f'{cid}.webp'), PORTRAIT_KB)
56
57 # Card art: 120x120 with alpha, drawn on the theme's card face.
58 for cid in sorted(set(art['cards'].values())):
59 im = Image.open(os.path.join(core, 'image', 'card', f'{cid}.png')).convert('RGBA')
60 sizes[f'card/{cid}'] = webp(im, os.path.join(OUT, 'card', f'{cid}.webp'), CARD_KB, q=88)
61
62 # The woodden theme (its grid and wood), the glass 勾玉 and the card back.
63 for src, dst in THEME:
64 shutil.copy(os.path.join(core, src), os.path.join(OUT, 'theme', dst))
65 sizes['theme/wood'] = webp(Image.open(os.path.join(core, 'theme/woodden/wood.jpg')).convert('RGB'), os.path.join(OUT, 'theme', 'wood.webp'), 40, q=84)
66 back = Image.open(os.path.join(core, 'theme/style/cardback/image/official.png')).convert('RGB').resize((240, 240), Image.LANCZOS)
67 sizes['theme/cardback'] = webp(back, os.path.join(OUT, 'theme', 'cardback.webp'), 30)
68
69 # Fonts, cut to what is drawn in them: names of generals, skills and cards, and the viewer's own labels.
70 gens = json.load(open(os.path.join(GAME, 'referee', 'generals.json'), encoding='utf-8'))
71 words = list(gens) + [s for g in gens.values() for s in g['skills']] + list(art['cards']) + ['火杀', '雷杀']
72 for root, _, files in os.walk(os.path.join(SRC, 'viewer')):
73 for f in files:
74 if f.endswith(('.tsx', '.ts', '.css')):
75 words += re.findall(r'[ -鿿＀-￯]+', open(os.path.join(root, f), encoding='utf-8').read())
76 text = ''.join(sorted(set(''.join(words)))) + '0123456789+-/×·??' + ''.join(chr(c) for c in range(0x20, 0x7f))
77 for name in FONTS:
78 opts = subset.Options()
79 opts.flavor = 'woff2'
80 opts.layout_features = ['*']
81 font = subset.load_font(os.path.join(core, 'font', f'{name}.woff2'), opts)
82 s = subset.Subsetter(opts)
83 s.populate(text=text)
84 s.subset(font)
85 out = os.path.join(OUT, 'font', f'{name}.woff2')
86 subset.save_font(font, out, opts)
87 sizes[f'font/{name}'] = os.path.getsize(out)
88 shutil.copy(os.path.join(core, 'font', 'suits.woff2'), os.path.join(OUT, 'font', 'suits.woff2'))
89
90 for f in ('art.css', 'NOTICE'):
91 shutil.copy(os.path.join(SRC, 'art', f), os.path.join(OUT, f))
92
93 total = sum(os.path.getsize(os.path.join(r, f)) for r, _, fs in os.walk(OUT) for f in fs)
94 big = max(sizes.items(), key=lambda kv: kv[1])
95 print(f'{len(art["generals"])} generals ({len(set(art["generals"].values()))} portraits), {len(art["cards"])} cards, '
96 f'{len(text)} glyphs; {total / 1e6:.1f} MB in {OUT}; largest {big[0]} {big[1] // 1024} KB')
97
98
99if __name__ == '__main__':
100 main()