"""Render the VTV preset reference page from the decoded JSON."""
import base64, html, io, json, os, sys
from PIL import Image
data = json.load(open(sys.argv[1]))
OUT = sys.argv[2]
GAUGE = 'c:/VWE/RP412/assets/RP411/GAUGE'
SCALE = 4 # pre-scale nearest-neighbour; masks get smoothed otherwise
def hull_mask(pcc):
"""The plan view as an alpha mask, so it takes the page's own colour.
Index 0 is the black ground; the two greys become two alpha levels,
which keeps the interior panel detail readable."""
im = Image.open(os.path.join(GAUGE, pcc.upper())).convert('P')
pal = im.getpalette()
lut = {}
for i in set(im.tobytes()):
r, g, b = pal[i * 3:i * 3 + 3]
lut[i] = 0 if (r, g, b) == (0, 0, 0) else int(max(r, g, b))
top = max(lut.values()) or 1
alpha = Image.frombytes('L', im.size,
bytes(round(lut[p] * 255 / top) for p in im.tobytes()))
out = Image.merge('RGBA', (Image.new('L', im.size, 255),) * 3 + (alpha,))
out = out.resize((im.width * SCALE, im.height * SCALE), Image.NEAREST)
buf = io.BytesIO()
out.save(buf, 'PNG', optimize=True)
return ('data:image/png;base64,' + base64.b64encode(buf.getvalue()).decode(),
im.size)
masks, sizes = {}, {}
for v in data.values():
art = v.get('hullArt')
if art and art not in masks:
masks[art], sizes[art] = hull_mask(art)
BUTTONS = ['trigger', 'thumb high', 'thumb low', 'pinky'] # index to little finger
# The blacker machines are palette variants of ones already in the list, so
# they read as duplicates when interleaved. Sort them to the end of whatever
# list they land in - within a hull tab as well as overall - and the originals
# come first. The console's own name is what decides it: eleven of the
# thirty-eight are called Black or Blacker something.
def is_variant(key):
return (data[key].get('display') or key).lower().startswith('black')
ORDER = sorted(data, key=lambda v: (is_variant(v), -len(data[v]['weapons']), v))
# blkrbroc's console picture is a Bug, but the game draws it on the Mule hull
# like every other Broccoli - the console entry is simply wrong. Saying so on
# the card raises a question the card cannot answer.
CONSOLE_PICTURE_WRONG = {'blkrbroc'}
def kind(fn):
if fn is None:
return 'empty'
if fn.startswith('Booster'):
return 'booster'
if fn.startswith('Chute'):
return 'chute'
if fn in ('LIFT CUT', 'SIDESLIP', 'reverse thrust'):
return 'handling'
if fn in ('HORN', 'reticle'):
return 'comms'
return 'weapon'
def pretty(fn):
if fn is None:
return '—'
if fn.startswith('Booster'):
return 'BOOST ' + fn[7:] if fn[7:] else 'BOOST'
if fn.startswith('Chute'):
return 'CHUTE'
return {'RivetGun': 'RIVET', 'RivetGun1': 'RIVET 1', 'RivetGun2': 'RIVET 2',
'LaserGun': 'LASER', 'LaserGun1': 'LASER 1', 'LaserGun2': 'LASER 2',
'DemopackDropper': 'DEMO PACK', 'DemoPackDropper': 'DEMO PACK',
'Slaver3': 'SLAVER'}.get(fn, fn.upper())
def loadout_chips(v):
chips = []
if v['boosters']:
chips.append(('booster', '%d× BOOST' % v['boosters']))
if v['chute']:
chips.append(('chute', 'CHUTE'))
for w in v['weapons']:
chips.append(('weapon', pretty(w)))
if not v['weapons']:
chips.append(('empty', 'UNARMED'))
return ''.join('%s' % (k, html.escape(t))
for k, t in chips)
def table(stream):
rows = []
for i, row in enumerate(stream['presets'], 1):
cells = []
for b in BUTTONS:
fn = row.get(b)
cells.append('
')
family = {}
for n, v in data.items():
family.setdefault(v.get('silhouette'), []).append(n)
cards = []
for name in ORDER:
v = data[name]
tags = ' '.join(['armed' if v['weapons'] else 'unarmed']
+ [kind(w) for w in v['weapons']])
code = v.get('silhouette') or ''
display = v.get('display') or name
# Hulls are named by the console, not by their two-letter art code.
hull_class = v.get('hullClass') or (code.upper() if code else 'unknown')
kin = sorted((data[x].get('display') or x)
for x in family.get(code, []) if x != name)
search = ' '.join([name, display, hull_class, code] + v['subsystems']).lower()
art = v.get('hullArt')
if art:
w, h = sizes[art]
note = ''
if v.get('hullMismatch') and name not in CONSOLE_PICTURE_WRONG:
note = (' '
'(console says %s)'
% (html.escape(v['consoleClass']), html.escape(hull_class),
html.escape(v['consoleClass'])))
hull = (''
'{cls} hull{kin}{note}').format(
display=html.escape(display), uri=masks[art], w=w, h=h,
cls=html.escape(hull_class), note=note,
kin=(' · shared with ' + html.escape(', '.join(kin))) if kin else '')
else:
hull = ''
cards.append("""
{hull}
The six amber switches down the right flank of the map screen are not a
display option. Each is a complete factory layout for the four mappable
joystick buttons, authored per vehicle and shipped in the game's resource
file. Pressing one swaps the whole stick over, live, mid-race.
Preset 2 always puts LIFT CUT on the pinky
Preset 6 always puts LIFT CUT on the trigger
Presets 1–5 keep the primary weapon on the trigger
{hulltabs}
{n} / {n} VTV
Vehicle systemsWeaponsPilot controls
{cards}
No VTV matches that.
{n} VTVs carry a preset table; {armed} are armed. Decoded from the pod
RIO ControlMappings stream in assets/RP411/RPL4.RES —
subsystem names from each vehicle's own subsystem list, functions from
ModePreset1..6 mask bits. Boosters, chute and weapons are the vehicle's
activatable systems; LIFT CUT and HORN are pilot controls that also live on the
panel. Empty cells are unbound — nothing on the pod fires them.
The plan views are the vehicle's own damage-gauge silhouette
(GAUGE/s<hull>.pcc), redrawn from the 3-colour original. The game has
no per-vehicle art: vehicles are grouped into hull families and share both the
silhouette and the 3D mesh in VIDEO/, which is keyed by the same two-letter
code. What distinguishes vehicles inside a family is the loadout, not the shape.