#!/usr/bin/env python3 """Testlevel bauen: in einem leeren Bereich des MAP-Rasters eine Bodenplatte aus Kopien eines flachen Boden-Tiles anlegen, darauf je einen Strat (Objekt) pro Typ im Raster aufstellen, den Spieler (Strat "Llama") an den Anfang setzen und einen kleinen Parkour (schwebende Bodenplatten, treppauf) anhängen. Geometrie/Texturen/Strat-Code (TRAK/STPC/TEXT …) bleiben die des Quell-Levels; nur der MAP-Chunk wird neu geschrieben. make_testlevel.py quelle.wad ziel.wad [--keep-original] [--spacing 3] [--name "TestLevel"] --keep-original : Original-Strats behalten (sonst nur Spieler + Raster + Cheats) Strat-Typen werden per Name erkannt (strats.py); Parameter/Typ-Felder werden von der ersten Original-Instanz kopiert, Wegpunkt-Index bleibt (zeigt ggf. auf Original-Wegpunkte). """ 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 # nicht ins Raster: Kamera/Cutscene/Trigger-Zonen, Spieler selbst 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('= 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]) # begehbare Böden liegen bei Modell-y = +0.25 (Strats stehen auf 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'kein freier Bereich {w}x{h} im Raster {m["W"]}x{m["H"]}') 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-Modellindex für Bodenplatten') ap.add_argument('--y', type=float, default=None, help='Höhe der Bodenplatte (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 # Raster + Startstreifen pk = 14 # Parkour-Streifen 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'Boden-Tile #{ftile} Modell {fmodel} (Fläche y+{fsurf}), Bereich x{x0}..{x0 + gw + pk} ' f'z{z0}..{z0 + gh}, Höhe {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: Trittplatten mit Lücken, je +0.5 höher, dann Zielplattform 3x3 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)) # Spieler an den Start (Rand des Rasters, Blick ins Raster) 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)) # Münzen auf dem Parkour, Ausgang (LevelDoor) auf der Zielplattform 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']): # Konsistenz 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)} Typen im Raster) -> {a.dst}') for nm, cx, cz in placed: print(f' {nm:30s} Zelle ({cx},{cz})') if __name__ == '__main__': main()