193 lines
7.6 KiB
Python
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()
|