Card-art generation pipeline for monsterrobot.games
Four directions per tile (neon box art / vector side art / amiga comic / 80s airbrush) generated on the farm, plus the picker page and the installer that ships chosen art and its risograph hover b-side. Recipe rules, all learned the hard way against the existing covers: tiles render 172-240px so silhouette and colour are all that read; the page draws every title in HTML so art carries no lettering (the old covers baked in garbled type); busy interiors smuggle text back via album sleeves and go muddy small, so it is one hero subject per card. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8c8bb8cebb
commit
f3773919e5
190
tools/cardgen/build_picker.py
Normal file
190
tools/cardgen/build_picker.py
Normal file
@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the cover-art picker page (self-contained, images inlined as webp data URIs).
|
||||
|
||||
Judged on the same dark ground the cards actually live on, and at the true
|
||||
172px tile size, because that is the only size that matters on the real page.
|
||||
"""
|
||||
import base64, io, json, os, subprocess, sys, tempfile
|
||||
|
||||
CARDS = sys.argv[1]
|
||||
CURRENT = sys.argv[2] # dir of existing covers for comparison
|
||||
PROFILES = json.load(open(sys.argv[3]))
|
||||
OUT = sys.argv[4]
|
||||
VARIANTS = ['neon', 'vector', 'comic', 'paint']
|
||||
LABELS = {'neon': 'neon box art', 'vector': 'vector side art',
|
||||
'comic': 'amiga comic', 'paint': '80s airbrush'}
|
||||
# tiles the arcade page shows for each game id
|
||||
TITLES = {p['id']: p.get('title', p['id']) for p in PROFILES}
|
||||
|
||||
|
||||
def webp(path, width=380, q=72):
|
||||
"""Downscale to a data URI (macOS sips cannot WRITE webp — use cwebp)."""
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
dst = os.path.join(td, 'o.webp')
|
||||
r = subprocess.run(['cwebp', '-q', str(q), '-resize', str(width), '0', path, '-o', dst],
|
||||
capture_output=True)
|
||||
if r.returncode != 0 or not os.path.exists(dst):
|
||||
return None
|
||||
b = open(dst, 'rb').read()
|
||||
return 'data:image/webp;base64,' + base64.b64encode(b).decode()
|
||||
|
||||
|
||||
rows = []
|
||||
for p in PROFILES:
|
||||
gid = p['id']
|
||||
cells = []
|
||||
wide = p.get('aspect') == 'wide'
|
||||
src_dir = os.path.join(os.path.dirname(CARDS), 'hero') if wide else CARDS
|
||||
cur = webp(os.path.join(CURRENT, p.get('current_cover', gid) + '.jpg'),
|
||||
620 if wide else 300, 70)
|
||||
for v in VARIANTS:
|
||||
d = webp(os.path.join(src_dir, f'{gid}__{v}.png'), 560 if wide else 380)
|
||||
if d:
|
||||
cells.append({'v': v, 'src': d})
|
||||
if cells:
|
||||
rows.append({'id': gid, 'title': TITLES.get(gid, gid), 'cur': cur,
|
||||
'cells': cells, 'wide': wide})
|
||||
|
||||
total_kb = sum(len(c['src']) for r in rows for c in r['cells']) // 1024
|
||||
print(f'{len(rows)} games, {sum(len(r["cells"]) for r in rows)} variants, ~{total_kb//1024}MB inlined')
|
||||
|
||||
html = ['''<title>Cover art — pick one per game</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#07080d; --panel:#0d1018; --line:#1b2233; --ink:#dbe6da; --dim:#7b8894;
|
||||
--faint:#47535f; --phos:#3dff9a; --pink:#ff3ea5; --cyan:#38d6ff;
|
||||
--mono:ui-monospace,"Cascadia Code","JetBrains Mono",Menlo,monospace;
|
||||
--disp:"Arial Black","Helvetica Neue",Impact,sans-serif;
|
||||
}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{background:var(--bg);color:var(--ink);font-family:var(--mono);font-size:14px;line-height:1.5;
|
||||
background-image:radial-gradient(circle at 12% 0%,rgba(255,62,165,.10),transparent 42%),
|
||||
radial-gradient(circle at 88% 100%,rgba(56,214,255,.10),transparent 42%),
|
||||
repeating-linear-gradient(0deg,rgba(255,255,255,.014) 0 1px,transparent 1px 3px)}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 18px 100px}
|
||||
header{padding:26px 0 8px}
|
||||
h1{font-family:var(--disp);text-transform:uppercase;font-size:clamp(1.4rem,3.4vw,2.1rem);
|
||||
letter-spacing:-.02em;color:#fff;line-height:1}
|
||||
h1 .g{color:var(--phos)}
|
||||
.sub{color:var(--dim);margin-top:.5rem;max-width:64ch}
|
||||
.bar{position:sticky;top:0;z-index:20;display:flex;flex-wrap:wrap;align-items:center;gap:10px;
|
||||
background:rgba(7,8,13,.93);border-bottom:1px solid var(--line);padding:11px 0;margin:14px 0 4px;
|
||||
backdrop-filter:blur(8px)}
|
||||
.count{color:var(--phos);letter-spacing:.1em;text-transform:uppercase;font-size:11px}
|
||||
button{font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;
|
||||
background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:4px;
|
||||
padding:7px 12px;cursor:pointer;transition:.15s}
|
||||
button:hover{border-color:var(--pink);color:#fff}
|
||||
button:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
|
||||
button.on{border-color:var(--phos);color:var(--phos)}
|
||||
/* 114 inlined covers is ~100MB of decoded bitmap if the browser paints them
|
||||
all at once, which blanks the page on scroll. Only render rows near the
|
||||
viewport; the intrinsic size keeps the scrollbar honest. */
|
||||
.game{border-top:1px solid var(--line);padding:20px 0 24px;
|
||||
content-visibility:auto;contain-intrinsic-size:auto 360px}
|
||||
.gh{display:flex;align-items:baseline;gap:12px;margin-bottom:12px}
|
||||
.gh .t{font-family:var(--disp);text-transform:uppercase;color:#fff;font-size:16px}
|
||||
.gh .pick{color:var(--phos);font-size:11px;letter-spacing:.1em;text-transform:uppercase}
|
||||
.gh .pick.none{color:var(--faint)}
|
||||
.strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;align-items:start}
|
||||
body.tiny .strip{grid-template-columns:repeat(auto-fill,172px)}
|
||||
.opt{border:1px solid var(--line);border-radius:9px;overflow:hidden;background:var(--panel);
|
||||
cursor:pointer;transition:transform .16s,border-color .16s,box-shadow .16s;padding:0;display:block;
|
||||
text-align:left;width:100%}
|
||||
.opt:hover{transform:translateY(-4px);border-color:var(--pink);box-shadow:0 12px 30px -14px rgba(255,62,165,.6)}
|
||||
.opt.sel{border-color:var(--phos);box-shadow:0 0 0 1px var(--phos),0 12px 30px -14px rgba(61,255,154,.6)}
|
||||
.opt .art{position:relative;aspect-ratio:896/1152;background:#000}
|
||||
.game.wide .strip{grid-template-columns:repeat(auto-fit,minmax(300px,1fr))}
|
||||
body.tiny .game.wide .strip{grid-template-columns:1fr}
|
||||
.game.wide .opt .art{aspect-ratio:21/9}
|
||||
.opt .art img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
|
||||
.opt .lab{display:flex;align-items:center;gap:6px;padding:7px 9px;font-size:10px;
|
||||
letter-spacing:.09em;text-transform:uppercase;color:var(--dim)}
|
||||
.opt.sel .lab{color:var(--phos)}
|
||||
.opt .lab .k{margin-left:auto;color:var(--faint)}
|
||||
.opt.cur{opacity:.62}
|
||||
.opt.cur .lab{color:var(--pink)}
|
||||
.opt.cur:hover{transform:none;border-color:var(--line);box-shadow:none;cursor:default}
|
||||
.out{position:fixed;left:0;right:0;bottom:0;z-index:30;background:rgba(7,8,13,.96);
|
||||
border-top:1px solid var(--line);padding:10px 18px;display:flex;gap:12px;align-items:center;
|
||||
backdrop-filter:blur(8px)}
|
||||
.out code{flex:1;color:var(--cyan);font-size:12px;overflow-x:auto;white-space:nowrap;padding:4px 0}
|
||||
@media(prefers-reduced-motion:reduce){*{transition:none!important}}
|
||||
</style>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<h1>Cover art — <span class="g">pick one</span> per game</h1>
|
||||
<p class="sub">Four directions per game, generated clean on the farm. The old covers had
|
||||
garbled AI lettering baked in; these carry no type at all, because the site already
|
||||
draws every title in HTML. Judge them small — that grey card is the cover you have now.</p>
|
||||
</header>
|
||||
<div class="bar">
|
||||
<span class="count" id="count">0 picked</span>
|
||||
<button id="tiny">actual tile size</button>
|
||||
<button id="jump">next undecided</button>
|
||||
<button id="copy">copy my picks</button>
|
||||
</div>''']
|
||||
|
||||
for r in rows:
|
||||
cls = 'game wide' if r['wide'] else 'game'
|
||||
html.append(f''' <section class="{cls}" id="g-{r['id']}" data-game="{r['id']}">
|
||||
<div class="gh"><span class="t">{r['title']}</span><span class="pick none">no pick</span></div>
|
||||
<div class="strip">''')
|
||||
if r['cur']:
|
||||
html.append(f''' <div class="opt cur"><div class="art"><img decoding="async" src="{r['cur']}" alt=""></div>
|
||||
<div class="lab">current</div></div>''')
|
||||
for i, c in enumerate(r['cells'], 1):
|
||||
html.append(f''' <button class="opt" data-v="{c['v']}"><div class="art"><img decoding="async" src="{c['src']}" alt=""></div>
|
||||
<div class="lab">{LABELS[c['v']]}<span class="k">{i}</span></div></button>''')
|
||||
html.append(' </div>\n </section>')
|
||||
|
||||
html.append('''</div>
|
||||
<div class="out"><code id="picks">pick a cover in each row - your choices collect here</code>
|
||||
<button id="copy2">copy</button></div>
|
||||
<script>
|
||||
const picks = {};
|
||||
const games = [...document.querySelectorAll('.game')];
|
||||
const fmt = () => games.filter(g => picks[g.dataset.game])
|
||||
.map(g => g.dataset.game + '=' + picks[g.dataset.game]).join(' ');
|
||||
function sync(){
|
||||
document.getElementById('count').textContent =
|
||||
Object.keys(picks).length + ' / ' + games.length + ' picked';
|
||||
document.getElementById('picks').textContent =
|
||||
Object.keys(picks).length ? fmt() : 'pick a cover in each row - your choices collect here';
|
||||
}
|
||||
games.forEach(g => {
|
||||
const badge = g.querySelector('.pick');
|
||||
g.querySelectorAll('.opt:not(.cur)').forEach(o => o.addEventListener('click', () => {
|
||||
g.querySelectorAll('.opt').forEach(x => x.classList.remove('sel'));
|
||||
o.classList.add('sel');
|
||||
picks[g.dataset.game] = o.dataset.v;
|
||||
badge.textContent = o.dataset.v;
|
||||
badge.classList.remove('none');
|
||||
sync();
|
||||
}));
|
||||
});
|
||||
document.getElementById('tiny').addEventListener('click', e => {
|
||||
document.body.classList.toggle('tiny');
|
||||
e.currentTarget.classList.toggle('on');
|
||||
});
|
||||
document.getElementById('jump').addEventListener('click', () => {
|
||||
const next = games.find(g => !picks[g.dataset.game]);
|
||||
if (next) next.scrollIntoView({behavior:'smooth', block:'start'});
|
||||
});
|
||||
const copy = () => {
|
||||
const t = fmt();
|
||||
if (!t) return;
|
||||
navigator.clipboard.writeText(t);
|
||||
[...document.querySelectorAll('#copy,#copy2')].forEach(b => {
|
||||
const o = b.textContent; b.textContent = 'copied'; setTimeout(()=>b.textContent=o, 1200);
|
||||
});
|
||||
};
|
||||
document.getElementById('copy').addEventListener('click', copy);
|
||||
document.getElementById('copy2').addEventListener('click', copy);
|
||||
sync();
|
||||
</script>''')
|
||||
|
||||
open(OUT, 'w').write('\n'.join(html))
|
||||
print(f'→ {OUT} ({os.path.getsize(OUT)//1024}KB)')
|
||||
62
tools/cardgen/gen_cards.py
Normal file
62
tools/cardgen/gen_cards.py
Normal file
@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the 4-variant card-art spec for monsterrobot.games from profiles.json.
|
||||
|
||||
Recipe rules, all learned from the bake-off against the existing covers:
|
||||
* Tiles render 172-240px wide -> silhouette and colour are all that read.
|
||||
* The page draws every title in HTML, so art carries NO lettering. The old
|
||||
covers baked in garbled type ("INFTE! COPFRIICGN") - that is the slop tell.
|
||||
* Busy interiors smuggle text back in via album sleeves/posters and go muddy
|
||||
at thumbnail size. Single hero subject, always.
|
||||
"""
|
||||
import json, os, sys
|
||||
|
||||
OUT = sys.argv[2] if len(sys.argv) > 2 else '/private/tmp/claude-501/-Users-m3ultra-Documents/097e3817-807d-439d-b5f5-95a6ffd4a1f9/scratchpad/cards'
|
||||
|
||||
READS = ("single bold hero subject filling the frame, strong clean silhouette, "
|
||||
"dramatic rim light, high contrast, saturated limited palette, "
|
||||
"deep dark background, poster composition, reads clearly at thumbnail size")
|
||||
NOTEXT = ("completely wordless, no text, no letters, no words, no title, no logo, "
|
||||
"no signage, no watermark, no signature, blank unlabelled sleeves and "
|
||||
"plain unmarked surfaces")
|
||||
|
||||
VARIANTS = [
|
||||
# key model steps style
|
||||
('neon', 'flux2-klein-4b', 4,
|
||||
"1987 British home-computer game box art, airbrushed gouache, glossy "
|
||||
"highlights, heroic low angle, screaming neon cyan and magenta on pure black"),
|
||||
('vector', 'flux2-klein-4b', 4,
|
||||
"1980s arcade cabinet side art, bold flat vector shapes, hard edges, thick "
|
||||
"black outlines, blazing neon on black, geometric and graphic, no gradients"),
|
||||
('comic', 'flux2-klein-4b', 4,
|
||||
"early-1990s Amiga game cover painting, chunky saturated brushwork, bold cel "
|
||||
"shading, comic-book energy, primary colours, dark vignette"),
|
||||
('paint', 'z-image-turbo', 8,
|
||||
"classic 1980s airbrush poster painting, silhouetted figures against a glowing "
|
||||
"backlit haze, lush deep colour, cinematic, painted on black"),
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
profiles = json.load(open(sys.argv[1]))
|
||||
spec = []
|
||||
for p in profiles:
|
||||
subj = p['hero_subject']
|
||||
pal = p.get('palette') or ''
|
||||
for key, model, steps, style in VARIANTS:
|
||||
params = {
|
||||
'prompt': f"{style}. {subj}. {pal + '. ' if pal else ''}{READS}. {NOTEXT}",
|
||||
'model': model, 'steps': steps,
|
||||
'width': 896, 'height': 1152, 'seed': 7,
|
||||
}
|
||||
if model == 'z-image-turbo':
|
||||
params['quantize'] = '4'
|
||||
spec.append({'out': f"{OUT}/{p['id']}__{key}.png",
|
||||
'operator': 'flux_local', 'params': params})
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
path = OUT + '/spec.json'
|
||||
json.dump(spec, open(path, 'w'), indent=1)
|
||||
print(f'{len(spec)} jobs ({len(profiles)} games x {len(VARIANTS)}) -> {path}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
33
tools/cardgen/hero_spec.py
Normal file
33
tools/cardgen/hero_spec.py
Normal file
@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Hero banner variants. The slot is 21:9 and the current art is a PORTRAIT
|
||||
image cropped to fit, so most of it is thrown away. The page scrims the left
|
||||
side for the headline, so the robot is composed hard right with clean dark
|
||||
space on the left."""
|
||||
import json, os
|
||||
|
||||
OUT = '/private/tmp/claude-501/-Users-m3ultra-Documents/097e3817-807d-439d-b5f5-95a6ffd4a1f9/scratchpad/hero'
|
||||
SUBJ = ("a colossal chrome monster robot standing menacingly among glowing arcade "
|
||||
"cabinets in a dark neon arcade, reflections on a wet tiled floor, "
|
||||
"composed on the right hand side of a wide frame")
|
||||
READS = ("ultra wide cinematic banner, the left third is empty dark negative space, "
|
||||
"strong silhouette, high contrast, saturated neon on near black")
|
||||
NOTEXT = ("completely wordless, no text, no letters, no words, no logo, no marquee "
|
||||
"lettering, no watermark, no signature, blank unmarked cabinet panels")
|
||||
STYLES = {
|
||||
'neon': "1987 British home-computer game box art, airbrushed gouache, glossy highlights, screaming neon cyan and magenta on black",
|
||||
'vector': "1980s arcade cabinet side art, bold flat vector shapes, thick black outlines, blazing neon on black, geometric",
|
||||
'comic': "early-1990s Amiga game cover painting, chunky saturated brushwork, bold cel shading, dark vignette",
|
||||
'paint': "classic 1980s airbrush poster painting, silhouettes against a glowing backlit haze, lush deep colour, cinematic",
|
||||
}
|
||||
spec = []
|
||||
for k, st in STYLES.items():
|
||||
p = {'prompt': f'{st}. {SUBJ}. {READS}. {NOTEXT}',
|
||||
'model': 'z-image-turbo' if k == 'paint' else 'flux2-klein-4b',
|
||||
'steps': 8 if k == 'paint' else 4,
|
||||
'width': 1344, 'height': 576, 'seed': 13}
|
||||
if k == 'paint':
|
||||
p['quantize'] = '4'
|
||||
spec.append({'out': f'{OUT}/hero__{k}.png', 'operator': 'flux_local', 'params': p})
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
json.dump(spec, open(OUT + '/spec.json', 'w'), indent=1)
|
||||
print(len(spec), 'hero jobs')
|
||||
95
tools/cardgen/install_picks.py
Normal file
95
tools/cardgen/install_picks.py
Normal file
@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install chosen card art onto monsterrobot.games.
|
||||
|
||||
Usage: install_picks.py "cratewars=vector stylus9=neon ..." [--dry]
|
||||
|
||||
For each pick it:
|
||||
1. trims the paper border z-image sometimes paints around 'paint' variants,
|
||||
2. writes covers/<name>.jpg at the tile's native 896x1152,
|
||||
3. queues a matching risograph <name>_riso.jpg for the page's hover flip,
|
||||
4. rsyncs both to the lander (host path — the nginx mount is read-only).
|
||||
"""
|
||||
import json, os, subprocess, sys, tempfile
|
||||
|
||||
S = os.path.dirname(os.path.abspath(__file__))
|
||||
CARDS = f'{S}/cards'
|
||||
RISO = f'{S}/riso'
|
||||
STAGE = f'{S}/install'
|
||||
VPS = 'humanjing@100.71.119.27'
|
||||
LANDER = '/home/humanjing/monsterrobot.games/covers'
|
||||
PROFILES = {p['id']: p for p in json.load(open(f'{S}/profiles.json'))}
|
||||
|
||||
RISO_STYLE = ("risograph screenprint poster, three flat spot colours only, visible halftone "
|
||||
"dot texture, slight misregistration, bold graphic shapes, flat matte inks")
|
||||
READS = ("single bold hero subject filling the frame, strong clean silhouette, high contrast, "
|
||||
"poster composition, reads clearly at thumbnail size")
|
||||
NOTEXT = ("completely wordless, no text, no letters, no words, no logo, no watermark, "
|
||||
"no signature, blank unlabelled sleeves")
|
||||
|
||||
|
||||
def sh(*a):
|
||||
r = subprocess.run(a, capture_output=True)
|
||||
if r.returncode:
|
||||
sys.exit(f'FAILED: {" ".join(a)}\n{r.stderr.decode()[:400]}')
|
||||
|
||||
|
||||
def to_cover(src, dst, trim):
|
||||
"""896x1152 jpeg, optionally trimming a painted paper border first."""
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
cur = src
|
||||
if trim:
|
||||
w = int(subprocess.run(['sips', '-g', 'pixelWidth', src], capture_output=True)
|
||||
.stdout.decode().split(':')[-1])
|
||||
h = int(subprocess.run(['sips', '-g', 'pixelHeight', src], capture_output=True)
|
||||
.stdout.decode().split(':')[-1])
|
||||
cur = os.path.join(td, 'c.png')
|
||||
sh('sips', '-c', str(int(h * 0.955)), str(int(w * 0.955)), src, '--out', cur)
|
||||
sh('sips', '-s', 'format', 'jpeg', '-s', 'formatOptions', '82',
|
||||
'-z', '1152', '896', cur, '--out', dst)
|
||||
|
||||
|
||||
def main():
|
||||
picks = dict(p.split('=') for p in sys.argv[1].split() if '=' in p)
|
||||
dry = '--dry' in sys.argv
|
||||
os.makedirs(STAGE, exist_ok=True)
|
||||
os.makedirs(RISO, exist_ok=True)
|
||||
|
||||
# 1. main covers from the picked variants
|
||||
riso_spec = []
|
||||
for gid, variant in picks.items():
|
||||
src = f'{CARDS}/{gid}__{variant}.png'
|
||||
if not os.path.exists(src):
|
||||
print(f' ! missing {src}'); continue
|
||||
name = PROFILES[gid].get('current_cover', gid)
|
||||
if name == '__none__':
|
||||
name = gid
|
||||
to_cover(src, f'{STAGE}/{name}.jpg', trim=(variant == 'paint'))
|
||||
print(f' ✓ {name}.jpg ({variant})')
|
||||
p = PROFILES[gid]
|
||||
riso_spec.append({
|
||||
'out': f'{RISO}/{name}_riso.png', 'operator': 'flux_local',
|
||||
'params': {'prompt': f"{RISO_STYLE}. {p['hero_subject']}. {READS}. {NOTEXT}",
|
||||
'model': 'flux2-klein-4b', 'steps': 4,
|
||||
'width': 896, 'height': 1152, 'seed': 7}})
|
||||
|
||||
# 2. the hover b-sides
|
||||
json.dump(riso_spec, open(f'{RISO}/spec.json', 'w'), indent=1)
|
||||
if not dry:
|
||||
sh(sys.executable, f'{os.path.expanduser("~")}/Documents/paradramorama/tools/mbgen.py',
|
||||
f'{RISO}/spec.json')
|
||||
for item in riso_spec:
|
||||
base = os.path.basename(item['out'])[:-4]
|
||||
if os.path.exists(item['out']):
|
||||
to_cover(item['out'], f'{STAGE}/{base}.jpg', trim=False)
|
||||
print(f' ✓ {base}.jpg')
|
||||
|
||||
# 3. ship
|
||||
if dry:
|
||||
print('\n[dry] staged only, nothing uploaded')
|
||||
return
|
||||
sh('rsync', '-az', f'{STAGE}/', f'{VPS}:{LANDER}/')
|
||||
print(f'\n✓ uploaded {len(os.listdir(STAGE))} files to {LANDER}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
164
tools/cardgen/profiles.json
Normal file
164
tools/cardgen/profiles.json
Normal file
@ -0,0 +1,164 @@
|
||||
[
|
||||
{
|
||||
"id": "hero",
|
||||
"title": "Hero banner",
|
||||
"current_cover": "hero",
|
||||
"aspect": "wide",
|
||||
"hero_subject": "colossal chrome monster robot among glowing arcade cabinets",
|
||||
"palette": ""
|
||||
},
|
||||
{
|
||||
"id": "dj90s",
|
||||
"title": "90s DJ Sim",
|
||||
"current_cover": "dj90s",
|
||||
"hero_subject": "a teenager in a cluttered 1990s Australian bedroom hunched over two belt-drive turntables on a plank-and-crate desk, headphones on one ear, milk crates of records under the bench, late afternoon light through venetian blinds",
|
||||
"palette": "faded teal, warm orange, wood-panel brown"
|
||||
},
|
||||
{
|
||||
"id": "nottonight",
|
||||
"title": "Not Tonight",
|
||||
"current_cover": "nottonight",
|
||||
"hero_subject": "a huge nightclub doorman filling a doorway at night, one arm out flat blocking the way, stamp in his fist, a long queue bunched behind a rope in the rain, red door light on his face",
|
||||
"palette": "night blue, sodium orange, deep red"
|
||||
},
|
||||
{
|
||||
"id": "thriftgod",
|
||||
"title": "Thriftgod",
|
||||
"current_cover": "thriftgod",
|
||||
"hero_subject": "a treasure hunter elbow deep in an overflowing op-shop bin of tangled clothes and dusty records, holding one find aloft in a shaft of fluorescent light",
|
||||
"palette": "fluorescent beige, faded pastel jumble, brown"
|
||||
},
|
||||
{
|
||||
"id": "procity",
|
||||
"title": "Procity",
|
||||
"current_cover": "procity",
|
||||
"hero_subject": "a giant hand lowering a glowing skyscraper into place on a tiny isometric city block, cranes and roads sprawling below, dusk light",
|
||||
"palette": "twilight blue, warm window gold, concrete grey"
|
||||
},
|
||||
{
|
||||
"id": "recordstoreguy",
|
||||
"title": "Record Store Guy",
|
||||
"current_cover": "recordstoreguy",
|
||||
"hero_subject": "a record store clerk diving sideways as an entire shelf of records avalanches down on him, blank sleeves fanning through the air mid-fall, cinematic film grain",
|
||||
"palette": "amber tungsten, deep shadow black, VHS teal"
|
||||
},
|
||||
{
|
||||
"id": "stylus9",
|
||||
"title": "Stylus-9",
|
||||
"current_cover": "stylus9",
|
||||
"hero_subject": "a glowing wireframe diamond stylus ship screaming down a canyon of black vinyl groove walls, tracer fire ahead, a colossal chrome tonearm looming overhead",
|
||||
"palette": "vector black, phosphor cyan, hot magenta"
|
||||
},
|
||||
{
|
||||
"id": "morp",
|
||||
"title": "Beyond Morp",
|
||||
"current_cover": "morp",
|
||||
"hero_subject": "a lone shopper dwarfed at the mouth of an endless maze of record racks curving overhead like canyon walls, one bare bulb swinging above, green terminal light spilling across the floor",
|
||||
"palette": "phosphor green, black, dusty cardboard brown"
|
||||
},
|
||||
{
|
||||
"id": "turncraft",
|
||||
"title": "Turncraft",
|
||||
"current_cover": "turncraft",
|
||||
"hero_subject": "a close low three-quarter view of a pair of chunky DJ turntables and a mixer, one hand pinning the spinning platter mid scratch, tonearm and cartridge in sharp focus",
|
||||
"palette": "matte black, brushed silver, red LED glow"
|
||||
},
|
||||
{
|
||||
"id": "cratewars",
|
||||
"title": "Crate Wars 2",
|
||||
"current_cover": "cratewars",
|
||||
"hero_subject": "two rival crate diggers wrestling over a milk crate stuffed with records in a bare warehouse, blank sleeves spilling and skidding across concrete, a beat-up van idling behind them",
|
||||
"palette": "nicotine amber, concrete grey, blood red"
|
||||
},
|
||||
{
|
||||
"id": "roguelike",
|
||||
"title": "Monster Roguelike",
|
||||
"current_cover": "roguelike",
|
||||
"hero_subject": "a hunched crate digger with a bulging record bag facing a matted wild-eyed neckbeard monster clutching warped melted records like claws, in a torchlit dungeon aisle of record racks",
|
||||
"palette": "dungeon black, gold, sickly green"
|
||||
},
|
||||
{
|
||||
"id": "keyboardwarrior",
|
||||
"title": "Keyboard Warrior",
|
||||
"current_cover": "keyboardwarrior",
|
||||
"hero_subject": "two furious music nerds hunched at facing desks in the dark, faces lit blue by monitors, hammering mechanical keyboards at each other like duelling swordsmen",
|
||||
"palette": "forum blue, monitor glow white, dark grey"
|
||||
},
|
||||
{
|
||||
"id": "vinylgauntlet",
|
||||
"title": "Vinyl Gauntlet",
|
||||
"current_cover": "vinylgauntlet",
|
||||
"hero_subject": "a lone digger swinging a record crate at a swarming mob of collector ghouls in a warehouse aisle, records flying like discs, a glowing pile of gold and vinyl in the corner",
|
||||
"palette": "dungeon purple black, gold, acid green"
|
||||
},
|
||||
{
|
||||
"id": "morpquest",
|
||||
"title": "Morpquest",
|
||||
"current_cover": "morpquest",
|
||||
"hero_subject": "a queue of desperate collectors surging at dawn across a wet car park toward a roller door, one lone digger clutching a blank white label record above his head",
|
||||
"palette": "sky blue, brick orange, black"
|
||||
},
|
||||
{
|
||||
"id": "deadstock",
|
||||
"title": "Deadstock",
|
||||
"current_cover": "deadstock",
|
||||
"hero_subject": "a battered portable suitcase record player open on a stone floor, its spinning platter throwing a blinding shaft of white light up onto a huge sealed vault door",
|
||||
"palette": "cyan, magenta, black"
|
||||
},
|
||||
{
|
||||
"id": "blobbo",
|
||||
"title": "Blobbo",
|
||||
"current_cover": "blobbo",
|
||||
"hero_subject": "a fat translucent jelly blob mid splat against a candy coloured platform, wobbling out of shape, trailing a smear of wet paint and a burst of confetti, cartoon cannons behind",
|
||||
"palette": "sky blue, hot pink, lime green"
|
||||
},
|
||||
{
|
||||
"id": "glytch",
|
||||
"title": "Glytch",
|
||||
"current_cover": "glytch",
|
||||
"hero_subject": "a half grown clone creature floating in a glowing bubbling vat on a factory production line, cables snaking into its back, a chunky CRT monitor bolted beside it flickering with magenta scanlines",
|
||||
"palette": "deep violet black, hot magenta, cyan"
|
||||
},
|
||||
{
|
||||
"id": "gutsy",
|
||||
"title": "Guts",
|
||||
"current_cover": "gutsy",
|
||||
"hero_subject": "a tiny chrome bio-ship banking hard down a slick pink peristaltic tunnel, wet ribbed tissue walls glistening, splashes of green acid and glowing molecule clusters ahead",
|
||||
"palette": "wet salmon pink, bile green, chrome silver"
|
||||
},
|
||||
{
|
||||
"id": "hardyards",
|
||||
"title": "Hard Yards",
|
||||
"current_cover": "hardyards",
|
||||
"hero_subject": "a person hauling a rope tight on a shade sail in a suburban Australian backyard as a black hail supercell rolls over the gum trees, hailstones bouncing off a corrugated carport",
|
||||
"palette": "storm grey blue, dry gum leaf green, rusted corrugated iron"
|
||||
},
|
||||
{
|
||||
"id": "sandoniette",
|
||||
"title": "Sandoniette",
|
||||
"current_cover": "sandoniette",
|
||||
"hero_subject": "a scowling samurai in armour completing a katana slash through a flying sandwich, halved bread cheese egg and cutlet spinning apart mid air, a splash of sauce trailing the blade",
|
||||
"palette": "washi cream, ink black, vermilion red"
|
||||
},
|
||||
{
|
||||
"id": "toastsim",
|
||||
"title": "Toastsim",
|
||||
"current_cover": "toastsim",
|
||||
"hero_subject": "a chrome two slot toaster on a wooden kitchen bench at the moment of ejection, a slice of sourdough launched into the air trailing crumbs and butter, tomatoes garlic and a knife scattered around",
|
||||
"palette": "golden crust brown, tomato red, warm kitchen cream"
|
||||
},
|
||||
{
|
||||
"id": "mollycool",
|
||||
"title": "Molly Cool",
|
||||
"current_cover": "mollycool",
|
||||
"hero_subject": "a glowing white particle creature with two energy hands hauling two brightly coloured atoms together against a straining bond of light, a swirling storm of jittering coloured spheres all around in the dark",
|
||||
"palette": "black, electric white, emission green and blue"
|
||||
},
|
||||
{
|
||||
"id": "paradramorama",
|
||||
"title": "Paradramorama",
|
||||
"current_cover": "__none__",
|
||||
"hero_subject": "a formless crackling ball of electric blue static pouring itself into the front panel of a rack mounted synthesizer, the machine's knobs and meters glowing awake, other dark hardware watching from the studio gloom",
|
||||
"palette": "near black navy, electric cyan, violet"
|
||||
}
|
||||
]
|
||||
Loading…
Reference in New Issue
Block a user