Files
llamamod/tools/make_testlevel.py
T

193 lines
7.6 KiB
Python

#!/usr/bin/env python3
"""Build a test level: in an empty area of the MAP grid, lay a floor plate made of copies
of a flat floor tile, place one strat (object) per type on it in a grid, put the
player (strat "Llama") at the start and append a small parkour (floating floor
plates, going up). Geometry/textures/strat code (TRAK/STPC/TEXT ...) stay those of
the source level; only the MAP chunk is rewritten.
make_testlevel.py source.wad target.wad [--keep-original] [--spacing 3] [--name "TestLevel"]
--keep-original : keep the original strats (otherwise only player + grid + cheats)
Strat types are recognised by name (strats.py); parameter/type fields are copied from the
first original instance, the waypoint index is kept (may point at original waypoints).
"""
import argparse
import os
import struct
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import mapchunk as M # noqa: E402
import strats as S # noqa: E402
import wadtool # noqa: E402
FX = 4096
# not in the grid: camera/cutscene/trigger zones, the player itself
SKIP_PREFIX = ('CutScene', 'Camera', 'MainMenu', 'Options', 'Title', 'TheNewGroove', 'Force',
'EndGameTrigger', 'RiverTriggerBox', 'FallOffNoHurt')
KEEP_ORIGINAL = ('CheatTeleport', 'CheatCharge')
PLAYER = 'Llama'
def trak_models(trak):
n = struct.unpack_from('<I', trak, 0)[0]
o = 4 + n * 0x84
out = []
for i in range(n):
h = 4 + i * 0x84
nv, nf = struct.unpack_from('<HH', trak, h + 0x6c)
q = sum(struct.unpack_from('<3H', trak, h + 0x78))
vs = [struct.unpack_from('<3f', trak, o + 0x18 * k) for k in range(nv)]
out.append(vs)
o += nv * 0x18 + nf * 0x1c + q * 0x20
return out
def find_floor_tile(m, models, force=None):
"""Most frequent tile whose model is a flat 1x1 surface."""
from collections import Counter
flat = set()
for i, vs in enumerate(models):
if vs and len({round(v[1], 3) for v in vs}) == 1 and \
min(v[0] for v in vs) <= -0.49 and max(v[0] for v in vs) >= 0.49 and \
min(v[2] for v in vs) <= -0.49 and max(v[2] for v in vs) >= 0.49:
flat.add(i)
c = Counter(mi for t, mi in zip(m['tiles'], m['tilemodel']) if mi in flat and not t[5])
# walkable floors sit at model y = +0.25 (strats stand on tile y + 0.25)
best = [k for k, _ in c.most_common() if abs(models[k][0][1] - 0.25) < 1e-3]
mdl = force if force is not None else (best or [c.most_common(1)[0][0]])[0]
idx = next(i for i, (t, mi) in enumerate(zip(m['tiles'], m['tilemodel'])) if mi == mdl and not t[5])
return idx, mdl, models[mdl][0][1]
def empty_rect(m, w, h):
occ = {(int(t[0]), int(-t[2])) for t in m['tiles']}
for z0 in range(m['H'] - h, -1, -1):
for x0 in range(0, m['W'] - w + 1):
if all((x, z) not in occ for x in range(x0 - 1, x0 + w + 1)
for z in range(z0 - 1, z0 + h + 1)):
return x0, z0
raise SystemExit(f'no free {w}x{h} area in the {m["W"]}x{m["H"]} grid')
def main():
ap = argparse.ArgumentParser()
ap.add_argument('src')
ap.add_argument('dst')
ap.add_argument('--spacing', type=int, default=3)
ap.add_argument('--keep-original', action='store_true')
ap.add_argument('--name')
ap.add_argument('--floor-model', type=int, help='TRAK model index for floor plates')
ap.add_argument('--y', type=float, default=None, help='height of the floor plate (tile y)')
a = ap.parse_args()
ch, wf, m, lo, types = S.level_types(a.src)
models = trak_models(ch['TRAK'])
ftile, fmodel, fsurf = find_floor_tile(m, models, a.floor_model)
names = sorted(n for n in types if n != PLAYER and not n.startswith(SKIP_PREFIX)
and n not in KEEP_ORIGINAL and n != 'LevelDoor')
cols = 6
rows = (len(names) + cols - 1) // cols
sp = a.spacing
gw, gh = cols * sp + 2, rows * sp + 4 # grid + start strip
pk = 14 # parkour strip
x0, z0 = empty_rect(m, gw + pk, gh)
base = m['tiles'][ftile]
y = a.y if a.y is not None else base[1]
print(f'floor tile #{ftile} model {fmodel} (surface y+{fsurf}), area x{x0}..{x0 + gw + pk} '
f'z{z0}..{z0 + gh}, height {y}')
def add_tile(cx, cz, ty):
m['tiles'].append((cx + 0.5, ty, -(cz + 0.5), base[3], fmodel, 0))
m['tiles2'].append((0, base[3], 0, int((cx + 0.5) * FX), int(ty * FX), int((cz + 0.5) * FX)))
m['tilemodel'].append(fmodel)
m['vcol_split'].append(m['vcol_split'][ftile])
for cx in range(x0, x0 + gw):
for cz in range(z0, z0 + gh):
add_tile(cx, cz, y)
# parkour: stepping plates with gaps, each +0.5 higher, then a 3x3 goal platform
px, pz, py = x0 + gw + 1, z0 + 1, y
steps = []
for i in range(6):
add_tile(px, pz, py)
steps.append((px, pz, py))
px += 2
py += 0.5
if px > x0 + gw + pk - 4:
px, pz = x0 + gw + 1, pz + 3
for dx in range(3):
for dz in range(3):
add_tile(px + dx, pz + dz, py)
goal = (px + 1, pz + 1, py)
def srec(proto, cx, cz, ty, ry=None):
s = list(proto)
s[1] = proto[1] if ry is None else ry
s[3], s[4], s[5] = int((cx + 0.5) * FX), int((ty + fsurf) * FX), int((cz + 0.5) * FX)
n, pi = proto[7], proto[8]
if n:
s[8] = len(m['list1'])
m['list1'].extend(m['_list1_orig'][pi:pi + n])
else:
s[8] = -1
s[15] = 0
return tuple(s)
m['_list1_orig'] = list(m['list1'])
old = m['strats']
new = []
if a.keep_original:
new = list(old)
else:
m['list1'] = []
for s in old:
n = S.strat_name(ch['STPC'], s[6], lo)
if n in KEEP_ORIGINAL:
pi = s[8]
s = list(s)
if s[7]:
s[8] = len(m['list1'])
m['list1'].extend(m['_list1_orig'][pi:pi + s[7]])
new.append(tuple(s))
# player at the start (edge of the grid, facing into it)
if PLAYER in types:
new = [s for s in new if S.strat_name(ch['STPC'], s[6], lo) != PLAYER]
new.insert(0, srec(types[PLAYER][0], x0 + 1, z0 + 1, y, ry=types[PLAYER][0][1]))
placed = []
for k, nm in enumerate(names):
cx = x0 + 2 + (k % cols) * sp
cz = z0 + 3 + (k // cols) * sp
new.append(srec(types[nm][0], cx, cz, y))
placed.append((nm, cx, cz))
# coins on the parkour, exit (LevelDoor) on the goal platform
if 'Coin' in types:
for (cx, cz, ty) in steps:
new.append(srec(types['Coin'][0], cx, cz, ty + 0.5))
if 'LevelDoor' in types:
new.append(srec(types['LevelDoor'][0], goal[0], goal[1], goal[2]))
m['strats'] = new
m['strat_x'] = max(m['strat_x'], len(new) + 64)
m['vcol'] = b''.join(m['vcol_split'])
for i, t in enumerate(m['tiles']): # consistency
assert 0 <= int(t[0] - 0.5) < m['W'] and 0 <= int(-t[2] - 0.5) < m['H'], (i, t)
data = M.build(m, wf)
vc = M.trak_vertcounts(ch['TRAK'])
chk = M.parse(data, wf, vc)
assert len(chk['strats']) == len(new) and not chk['rest']
wch = wadtool.read(a.src)
wadtool.get(wch, 'MAP ')[1] = data
if a.name:
c = wadtool.get(wch, 'NAME')
c[1] = c[1][:4] + a.name.encode('latin1') + b'\0'
wadtool.write(a.dst, wch)
print(f'{len(m["tiles"])} Tiles, {len(new)} Strats ({len(placed)} types in the grid) -> {a.dst}')
for nm, cx, cz in placed:
print(f' {nm:30s} cell ({cx},{cz})')
if __name__ == '__main__':
main()