994 lines
45 KiB
Python
994 lines
45 KiB
Python
#!/usr/bin/env python3
|
|
"""levelbuild - eigene Level fuer "Ein Koenigreich fuer ein Lama" bauen (Croc-2-Engine, PC).
|
|
|
|
Ein Level wird als Python-Skript beschrieben, das eine Funktion ``build(L)`` enthaelt.
|
|
``L`` ist ein :class:`Level` mit Grundkoerpern (Boxen, Plattformen, Rampen, Treppen, ...),
|
|
Texturen per Name, automatischer Vertexfarben-Beleuchtung (Sonne, Himmel, Schatten,
|
|
Umgebungsverdeckung) und Objekt-Platzierung (Strats per Typname, Parameter vom Original).
|
|
|
|
levelbuild.py LEVEL.py OUT.wad [--base t1l1m001.wad] [--install] [--info]
|
|
levelbuild.py --types [--base ...] platzierbare Strat-Typen des Basislevels
|
|
levelbuild.py --textures OUT.png Kontaktabzug der Texturen (mit Namen/Index)
|
|
|
|
Koordinaten (wie im Spiel / bei Strats): x nach Osten, y nach oben, z = Rasterzeile
|
|
(positiv). 1 Einheit = 1 Rasterzelle. Das Raster des Basislevels ist 96x96 (x,z in 0..96).
|
|
Winkel in Grad (0..360) um die y-Achse.
|
|
Doku: ../notes/levelbau.md
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import importlib.util
|
|
import math
|
|
import os
|
|
import random
|
|
import struct
|
|
import sys
|
|
|
|
import numpy as np
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, HERE)
|
|
import geomlib as G # noqa: E402
|
|
import mapchunk as M # noqa: E402
|
|
import stpcwalk # noqa: E402
|
|
import strats as S # noqa: E402
|
|
|
|
WADS = os.path.expanduser('~/.wine-lama/iso-dk/Setup/Eng/Cd/wads')
|
|
BASE = os.path.join(WADS, 't1l1m001.wad')
|
|
MODWAD = os.path.expanduser('~/.wine-lama/mods/testlevel/wads/T1L9M001.wad')
|
|
FX = 4096
|
|
|
|
# Texturnamen fuer das Dorf (t1l1m001). Kontaktabzug: notes/img/texturen_dorf.png
|
|
TEX = {
|
|
'gras': 1, 'gras2': 170, 'gras_hell': 190, 'gras_streifen': 212, 'gras_dunkel': 157,
|
|
'gras_fleck': 99, 'blumen': 87, 'blumen2': 81, 'blumen3': 82,
|
|
'sand': 92, 'sand2': 14, 'sand3': 11, 'erde': 3, 'erde2': 4, 'erde3': 5,
|
|
'graskante': 38, 'graskante2': 2, 'graskante_sand': 12,
|
|
'stein': 149, 'stein2': 166, 'stein_blau': 154, 'stein_kante': 145,
|
|
'stein_gruen': 169, 'stein_gruen2': 148, 'stein_moos': 199,
|
|
'platte': 214, 'platte2': 225, 'platte_kante': 223,
|
|
'holz_dunkel': 203, 'bambus': 141, 'bambus2': 201, 'holz_maser': 127, 'holz_maser2': 128,
|
|
'kiste': 233, 'rinde': 222, 'holz_braun': 126, 'holz_braun2': 129, 'holzbalken': 142,
|
|
'holz_ringe': 119, 'holzkante': 118,
|
|
'inka': 131, 'muster_lila': 202, 'muster_lila2': 208, 'muster_lila3': 200,
|
|
'pilz_rot': 136, 'pilz_rot2': 137, 'treppe_stein': 63,
|
|
}
|
|
# zusaetzliche Flachfarben (werden an die Texturtabelle angehaengt): Name -> (R, G, B)
|
|
FLAT = {
|
|
'meer': (40, 120, 150), 'meer_tief': (25, 85, 120), 'weiss': (235, 235, 225),
|
|
'rot': (190, 50, 40), 'gelb': (230, 190, 60), 'blau': (60, 90, 190), 'schwarz': (20, 20, 20),
|
|
'gruen': (70, 140, 50), 'grau': (120, 120, 120),
|
|
}
|
|
# Strat-Typen, die im Dorf-Code stehen, aber im Dorf nicht platziert sind:
|
|
# Typfelder (nvars, Slots, Flags, Zusatz) + Parameter aus einem anderen Level uebernehmen
|
|
FOREIGN = {'RubyIdol': 't1l1m002.wad'}
|
|
|
|
|
|
def deg(a):
|
|
return int(round((a % 360) / 360 * 4096)) & 0xfff
|
|
|
|
|
|
# ---------------------------------------------------------------- Geometrie-Sammelbehaelter
|
|
class Quad:
|
|
__slots__ = ('p', 'tex', 'coll', 'mat', 'tint', 'shade', 'double', 'render')
|
|
|
|
def __init__(self, p, tex, coll=True, mat=0, tint=(1, 1, 1), shade=True, double=False, render=True):
|
|
self.p = p # 3 oder 4 Punkte (Spielkoordinaten)
|
|
self.tex, self.coll, self.mat, self.tint, self.shade, self.double = tex, coll, mat, tint, shade, double
|
|
self.render = render # False: nur Kollision (unsichtbar)
|
|
|
|
|
|
def _sub(a, b):
|
|
return (a[0] - b[0], a[1] - b[1], a[2] - b[2])
|
|
|
|
|
|
def _cross(a, b):
|
|
return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0])
|
|
|
|
|
|
def _norm(v):
|
|
ln = math.sqrt(sum(c * c for c in v)) or 1.0
|
|
return tuple(c / ln for c in v)
|
|
|
|
|
|
def to_exp(p):
|
|
"""Spielraum (z positiv) -> Exportraum/Vertexraum (z negativ)."""
|
|
return (p[0], p[1], -p[2])
|
|
|
|
|
|
class Level:
|
|
"""Baukasten. Alle Methoden nehmen Spielkoordinaten (x, z = Rasterzelle, y oben)."""
|
|
|
|
def __init__(self, base=BASE, name='Testwelt', seed=1):
|
|
self.base = base
|
|
self.name = name
|
|
self.rng = random.Random(seed)
|
|
self.quads = []
|
|
self.solids = [] # AABBs (x0,y0,z0,x1,y1,z1) fuer Schatten/Umgebungsverdeckung
|
|
self.strats = [] # dict(name, x,y,z, rot, params, wp, variant)
|
|
self.kill = None # (y, x0, z0, x1, z1)
|
|
self.sun = dict(azimuth=215, elevation=50, color=(1.0, 0.93, 0.80), strength=0.62,
|
|
sky=(0.50, 0.55, 0.66), ground=(0.30, 0.28, 0.24), ao=0.55, ao_radius=1.3,
|
|
shadow=0.35)
|
|
self.flats_used = {}
|
|
self._load()
|
|
|
|
# ------------------------------------------------------------ Basislevel
|
|
def _load(self):
|
|
self.ch = G.wad_read(self.base)
|
|
self.wf = G.wfpc_flags(self.ch)
|
|
self.trak0 = G.parse_trak(G.wad_get(self.ch, 'TRAK'))
|
|
self.pages, self.texs = G.parse_text(G.wad_get(self.ch, 'TEXT'))
|
|
vc = [len(m.verts) for m in self.trak0]
|
|
self.map = M.parse(G.wad_get(self.ch, 'MAP '), self.wf, vc)
|
|
self.W, self.H = self.map['W'], self.map['H']
|
|
st = G.wad_get(self.ch, 'STPC')
|
|
self.stpc = st
|
|
self.code_lo = stpcwalk.walk(st, self.wf)['code']
|
|
self.protos = {} # name -> [instanz-tupel je Code-Variante]
|
|
for s in self.map['strats']:
|
|
nm = (S.strat_name(st, s[6], self.code_lo) or '?@%x' % s[6]).replace('(spawn)', '')
|
|
lst = self.protos.setdefault(nm, [])
|
|
if all(p[6] != s[6] for p in lst):
|
|
lst.append(s)
|
|
self.pool0 = list(self.map['list1'])
|
|
self.foreign_params = {}
|
|
named = dict((n, o) for o, n in S.all_named(st, self.code_lo))
|
|
for nm, wad in FOREIGN.items():
|
|
if nm in self.protos or nm not in named:
|
|
continue
|
|
try:
|
|
ch2, wf2, m2, lo2, types2 = S.level_types(os.path.join(WADS, wad))
|
|
except Exception:
|
|
continue
|
|
if nm in types2:
|
|
s = list(types2[nm][0])
|
|
prm = m2['list1'][s[8]:s[8] + s[7]] if s[7] else []
|
|
s[6] = named[nm]
|
|
s[13] = -1
|
|
self.protos[nm] = [tuple(s)]
|
|
self.foreign_params[nm] = prm
|
|
|
|
def types(self):
|
|
return sorted(self.protos)
|
|
|
|
def tex(self, t):
|
|
"""Texturname, Index (int) oder Flachfarbe ('meer' / (r,g,b))."""
|
|
if isinstance(t, int):
|
|
return t
|
|
if isinstance(t, tuple):
|
|
key = 'rgb%02x%02x%02x' % t
|
|
FLAT.setdefault(key, t)
|
|
t = key
|
|
if t in TEX:
|
|
return TEX[t]
|
|
if t in FLAT:
|
|
if t not in self.flats_used:
|
|
self.flats_used[t] = len(self.texs) + len(self.flats_used)
|
|
return self.flats_used[t]
|
|
raise KeyError('unbekannte Textur %r (Namen: %s)' % (t, ', '.join(sorted(TEX) + sorted(FLAT))))
|
|
|
|
# ------------------------------------------------------------ Grundbausteine
|
|
def quad(self, p, tex, coll=True, mat=0, tint=(1, 1, 1), rot=0, shade=True, double=False, render=True):
|
|
"""Ein Viereck p = [unten-links, unten-rechts, oben-rechts, oben-links] von vorne gesehen
|
|
(Textur aufrecht). rot = 0..3 dreht die Textur in 90-Grad-Schritten."""
|
|
p = [tuple(map(float, q)) for q in p]
|
|
rot %= 4
|
|
p = p[rot:] + p[:rot]
|
|
self.quads.append(Quad(p, self.tex(tex), coll, mat, tint, shade, double, render))
|
|
|
|
def collider(self, p, mat=0, normal=(0, 1, 0)):
|
|
"""Unsichtbare Kollisionsflaeche (3/4 Punkte), Vorderseite Richtung 'normal'."""
|
|
self.quad(_orient([tuple(map(float, q)) for q in p], normal), 'gras', coll=True, mat=mat, render=False)
|
|
|
|
def _hface(self, x0, z0, x1, z1, y, tex, up=True, step=1.0, rot='random', tint=(1, 1, 1), coll=True, mat=0):
|
|
"""Waagerechte Flaeche, in Kacheln (step) unterteilt, an ganzen Zahlen ausgerichtet."""
|
|
xs = _cuts(x0, x1, step)
|
|
zs = _cuts(z0, z1, step)
|
|
for xa, xb in zip(xs, xs[1:]):
|
|
for za, zb in zip(zs, zs[1:]):
|
|
r = self.rng.choice((0, 2)) if rot == 'random' else rot # 2 Varianten: weniger Modelle
|
|
if up: # von oben gesehen: unten-links = (xa, zb)? -> Normale +y
|
|
p = [(xa, y, za), (xb, y, za), (xb, y, zb), (xa, y, zb)]
|
|
else:
|
|
p = [(xa, y, zb), (xb, y, zb), (xb, y, za), (xa, y, za)]
|
|
self.quad(_orient(p, (0, 1 if up else -1, 0)), tex, coll=coll, mat=mat, tint=tint, rot=r)
|
|
|
|
def _vface(self, a, b, y0, y1, tex, top_tex=None, step=1.0, tint=(1, 1, 1), coll=True, mat=0, normal=None):
|
|
"""Senkrechte Wand von a=(x,z) nach b=(x,z), aussen = rechts von a->b (von oben, z nach unten)."""
|
|
L = math.hypot(b[0] - a[0], b[1] - a[1])
|
|
if L < 1e-6 or y1 - y0 < 1e-6:
|
|
return
|
|
n = max(1, int(round(L / step)))
|
|
ys = _cuts(y0, y1, step, align=False)
|
|
for i in range(n):
|
|
t0, t1 = i / n, (i + 1) / n
|
|
pa = (a[0] + (b[0] - a[0]) * t0, a[1] + (b[1] - a[1]) * t0)
|
|
pb = (a[0] + (b[0] - a[0]) * t1, a[1] + (b[1] - a[1]) * t1)
|
|
for k, (ya, yb) in enumerate(zip(ys, ys[1:])):
|
|
tx = top_tex if (top_tex is not None and k == len(ys) - 2) else tex
|
|
p = [(pa[0], ya, pa[1]), (pb[0], ya, pb[1]), (pb[0], yb, pb[1]), (pa[0], yb, pa[1])]
|
|
nn = normal or _wall_normal(a, b)
|
|
self.quad(_orient(p, nn), tx, coll=coll, mat=mat, tint=tint)
|
|
|
|
def floor(self, x0, z0, x1, z1, y, tex='gras', rot='random', tint=(1, 1, 1), step=1.0, coll=True):
|
|
"""Flache Bodenflaeche (nur Oberseite)."""
|
|
self._hface(x0, z0, x1, z1, y, tex, True, step, rot, tint, coll)
|
|
|
|
def box(self, x0, z0, x1, z1, y0, y1, top='gras', side='erde', side_top=None, bottom=None,
|
|
tint=(1, 1, 1), rot='random', sides='nesw', solid=True, coll=True, mat=0, step=1.0, side_coll=None):
|
|
"""Quader. side_top: Textur fuer die oberste Reihe der Seitenwaende (z.B. 'graskante').
|
|
bottom: Unterseite zeichnen (fuer schwebende Plattformen). sides: welche Seiten (n=z0,
|
|
s=z1, w=x0, e=x1)."""
|
|
x0, x1 = min(x0, x1), max(x0, x1)
|
|
z0, z1 = min(z0, z1), max(z0, z1)
|
|
y0, y1 = min(y0, y1), max(y0, y1)
|
|
if top:
|
|
self._hface(x0, z0, x1, z1, y1, top, True, step, rot, tint, coll, mat)
|
|
if bottom:
|
|
self._hface(x0, z0, x1, z1, y0, bottom, False, step, 0, tint, coll, mat)
|
|
sc = coll if side_coll is None else side_coll
|
|
if side:
|
|
# Waende so, dass die Aussenseite rechts liegt
|
|
if 'n' in sides:
|
|
self._vface((x1, z0), (x0, z0), y0, y1, side, side_top, step, tint, sc, mat, (0, 0, -1))
|
|
if 's' in sides:
|
|
self._vface((x0, z1), (x1, z1), y0, y1, side, side_top, step, tint, sc, mat, (0, 0, 1))
|
|
if 'w' in sides:
|
|
self._vface((x0, z0), (x0, z1), y0, y1, side, side_top, step, tint, sc, mat, (-1, 0, 0))
|
|
if 'e' in sides:
|
|
self._vface((x1, z1), (x1, z0), y0, y1, side, side_top, step, tint, sc, mat, (1, 0, 0))
|
|
if solid:
|
|
self.solids.append((x0, y0, z0, x1, y1, z1))
|
|
|
|
def platform(self, x, z, w, d, y, top='gras', side='erde', thick=0.5, side_top='graskante',
|
|
bottom='erde', **kw):
|
|
"""Schwebende Plattform: Oberkante y, Ecke (x,z), Groesse w x d."""
|
|
self.box(x, z, x + w, z + d, y - thick, y, top, side, side_top=side_top, bottom=bottom, **kw)
|
|
|
|
def pillar(self, x, z, y0, y1, size=1.0, side='stein', top='stein', **kw):
|
|
"""Saeule mit Mittelpunkt (x,z)."""
|
|
h = size / 2
|
|
self.box(x - h, z - h, x + h, z + h, y0, y1, top, side, **kw)
|
|
|
|
def ramp(self, x0, z0, x1, z1, ya, yb, direction='+x', top='sand', side='erde', base=None,
|
|
tint=(1, 1, 1), step=1.0, coll=True):
|
|
"""Rampe ueber dem Rechteck; steigt in 'direction' (+x,-x,+z,-z) von ya auf yb.
|
|
Unterkante 'base' (Standard: ya - 0.5); Seiten und Stirnflaeche werden geschlossen."""
|
|
x0, x1 = min(x0, x1), max(x0, x1)
|
|
z0, z1 = min(z0, z1), max(z0, z1)
|
|
base = (min(ya, yb) - 0.5) if base is None else base
|
|
ax = direction[1]
|
|
sgn = 1 if direction[0] == '+' else -1
|
|
|
|
def h(x, z):
|
|
t = (x - x0) / (x1 - x0) if ax == 'x' else (z - z0) / (z1 - z0)
|
|
if sgn < 0:
|
|
t = 1 - t
|
|
return ya + (yb - ya) * t
|
|
xs, zs = _cuts(x0, x1, step), _cuts(z0, z1, step)
|
|
for xa, xb in zip(xs, xs[1:]):
|
|
for za, zb in zip(zs, zs[1:]):
|
|
p = [(xa, h(xa, za), za), (xb, h(xb, za), za), (xb, h(xb, zb), zb), (xa, h(xa, zb), zb)]
|
|
self.quad(_orient(p, (0, 1, 0)), top, coll=coll, tint=tint)
|
|
# Seiten: Trapeze in Streifen
|
|
edges = [((x1, z0), (x0, z0), (0, 0, -1)), ((x0, z1), (x1, z1), (0, 0, 1)),
|
|
((x0, z0), (x0, z1), (-1, 0, 0)), ((x1, z1), (x1, z0), (1, 0, 0))]
|
|
for a, b, nn in edges:
|
|
Ln = math.hypot(b[0] - a[0], b[1] - a[1])
|
|
n = max(1, int(round(Ln / step)))
|
|
for i in range(n):
|
|
pa = (a[0] + (b[0] - a[0]) * i / n, a[1] + (b[1] - a[1]) * i / n)
|
|
pb = (a[0] + (b[0] - a[0]) * (i + 1) / n, a[1] + (b[1] - a[1]) * (i + 1) / n)
|
|
ha, hb = h(*pa), h(*pb)
|
|
if max(ha, hb) - base < 1e-6:
|
|
continue
|
|
p = [(pa[0], base, pa[1]), (pb[0], base, pb[1]), (pb[0], hb, pb[1]), (pa[0], ha, pa[1])]
|
|
self.quad(_orient(p, nn), side, coll=coll, tint=tint)
|
|
lo = min(ya, yb)
|
|
self.solids.append((x0, base, z0, x1, lo, z1))
|
|
|
|
def stairs(self, x0, z0, x1, z1, y0, y1, n, direction='+x', top='stein', side='stein2', base=None,
|
|
walkable=True, tint=(1, 1, 1)):
|
|
"""Treppe mit n Stufen auf dem Rechteck, steigt in 'direction' (+x,-x,+z,-z) von y0 auf y1.
|
|
walkable=True: Kollision als unsichtbare Schraege (Lama kann hochlaufen; die Engine hat
|
|
keine Stufen-Logik, schon 0.12 hohe Kanten blockieren), sonst echte Stufen (springen)."""
|
|
x0, x1 = min(x0, x1), max(x0, x1)
|
|
z0, z1 = min(z0, z1), max(z0, z1)
|
|
base = (y0 - 0.5) if base is None else base
|
|
ax = direction[1]
|
|
neg = direction[0] == '-'
|
|
# in lokale Koordinaten u (Steigrichtung 0..1) / v (quer 0..1) abbilden
|
|
def P(u, v, y):
|
|
if neg:
|
|
u = 1 - u
|
|
return ((x0 + (x1 - x0) * u, y, z0 + (z1 - z0) * v) if ax == 'x'
|
|
else (x0 + (x1 - x0) * v, y, z0 + (z1 - z0) * u))
|
|
|
|
def nrm(du, dv):
|
|
if neg:
|
|
du = -du
|
|
return (du, 0, dv) if ax == 'x' else (dv, 0, du)
|
|
rc = not walkable
|
|
for i in range(n):
|
|
u0, u1 = i / n, (i + 1) / n
|
|
yt = y0 + (y1 - y0) * (i + 1) / n
|
|
yl = base if i == 0 else y0 + (y1 - y0) * i / n
|
|
last = i == n - 1
|
|
# Trittflaeche
|
|
q = [P(u0, 0, yt), P(u1, 0, yt), P(u1, 1, yt), P(u0, 1, yt)]
|
|
self.quad(_orient(q, (0, 1, 0)), top, coll=(rc or last), tint=tint)
|
|
# Setzstufe (Stirnseite), Fuss bis base
|
|
q = [P(u0, 0, base), P(u0, 1, base), P(u0, 1, yt), P(u0, 0, yt)]
|
|
self.quad(_orient(q, nrm(-1, 0)), side, coll=rc, tint=tint)
|
|
# Seitenwaende dieser Stufe (bis base)
|
|
for v, dv in ((0, -1), (1, 1)):
|
|
q = [P(u0, v, base), P(u1, v, base), P(u1, v, yt), P(u0, v, yt)]
|
|
self.quad(_orient(q, nrm(0, dv)), side, coll=True, tint=tint)
|
|
self.solids.append(_aabb([P(u0, 0, base), P(u1, 1, yt)]))
|
|
# Rueckwand
|
|
q = [P(1, 0, base), P(1, 1, base), P(1, 1, y1), P(1, 0, y1)]
|
|
self.quad(_orient(q, nrm(1, 0)), side, coll=True, tint=tint)
|
|
if walkable:
|
|
ue = (n - 1) / n if n > 1 else 1.0
|
|
q = [P(0, 0, y0), P(ue, 0, y1), P(ue, 1, y1), P(0, 1, y0)]
|
|
self.collider(q)
|
|
|
|
def fence(self, pts, y, height=0.6, thick=0.12, post='holz_dunkel', rail='holz_maser', posts_every=1.0):
|
|
"""Zaun (Pfosten + Querlatte) entlang Punktliste [(x,z),...] auf Hoehe y. Mit Kollision."""
|
|
for (ax, az), (bx, bz) in zip(pts, pts[1:]):
|
|
L = math.hypot(bx - ax, bz - az)
|
|
n = max(1, int(round(L / posts_every)))
|
|
for i in range(n + 1):
|
|
px, pz = ax + (bx - ax) * i / n, az + (bz - az) * i / n
|
|
self.box(px - thick, pz - thick, px + thick, pz + thick, y, y + height, top=post, side=post, rot=0)
|
|
# Latte als duenner Quader
|
|
t = thick * 0.6
|
|
yb = y + height * 0.55
|
|
if abs(bx - ax) >= abs(bz - az):
|
|
self.box(min(ax, bx), az - t, max(ax, bx), az + t, yb, yb + 0.15, top=rail, side=rail, rot=0,
|
|
solid=False)
|
|
else:
|
|
self.box(ax - t, min(az, bz), ax + t, max(az, bz), yb, yb + 0.15, top=rail, side=rail, rot=0,
|
|
solid=False)
|
|
|
|
def tree(self, x, z, y, height=2.5, crown=1.4, trunk='rinde', leaves='gras_dunkel', trunk_size=None):
|
|
"""Einfacher Klotz-Baum (Stamm + zwei Kronen-Wuerfel)."""
|
|
self.pillar(x, z, y, y + height, size=trunk_size or max(0.15, crown * 0.25), side=trunk, top=trunk)
|
|
c = crown / 2
|
|
self.box(x - c, z - c, x + c, z + c, y + height - 0.2, y + height + crown * 0.7, top=leaves, side=leaves,
|
|
bottom=leaves, coll=True)
|
|
c2 = c * 0.6
|
|
self.box(x - c2, z - c2, x + c2, z + c2, y + height + crown * 0.7, y + height + crown * 1.1, top=leaves,
|
|
side=leaves, coll=True)
|
|
|
|
def sea(self, x0, z0, x1, z1, y=-2.0, tex='meer', kill=None, step=4.0):
|
|
"""Wasser-/Grundflaeche ohne Kollision (grob unterteilt, step) + toedliche Ebene knapp
|
|
darueber. kill=(x0,z0,x1,z1): Bereich der Todesebene (Standard: ganze Flaeche, False: keine).
|
|
Die Todesebene wird nur in Zellen ohne durchgehenden Boden angelegt."""
|
|
self._hface(x0, z0, x1, z1, y, tex, True, step, 0, (1, 1, 1), False)
|
|
if kill is not False:
|
|
self.kill = (y + 0.05,) + tuple(kill or (x0, z0, x1, z1))
|
|
|
|
def obj(self, path, offset=(0, 0, 0), scale=1.0, tex='stein', coll=True):
|
|
"""OBJ-Datei (Spielkoordinaten, y oben). 'usemtl tex_N' bzw. Texturname als Material."""
|
|
V, cur = [], tex
|
|
for line in open(path, encoding='latin1'):
|
|
p = line.split()
|
|
if not p:
|
|
continue
|
|
if p[0] == 'v':
|
|
V.append(tuple(float(c) * scale + o for c, o in zip(p[1:4], offset)))
|
|
elif p[0] == 'usemtl':
|
|
m = p[1]
|
|
cur = int(m[4:]) if m.startswith('tex_') else (m if (m in TEX or m in FLAT) else tex)
|
|
elif p[0] == 'f':
|
|
ids = [int(t.split('/')[0]) for t in p[1:]]
|
|
pts = [V[i - 1 if i > 0 else len(V) + i] for i in ids]
|
|
if len(pts) == 4:
|
|
self.quad(pts, cur, coll=coll)
|
|
else:
|
|
for k in range(1, len(pts) - 1):
|
|
self.quad([pts[0], pts[k], pts[k + 1]], cur, coll=coll)
|
|
|
|
# ------------------------------------------------------------ Hoehe
|
|
def height_at(self, x, z, below=1e9):
|
|
"""Hoechste begehbare Flaeche bei (x,z) unterhalb 'below' (oder None)."""
|
|
best = None
|
|
for q in self.quads:
|
|
if not q.coll:
|
|
continue
|
|
p = q.p
|
|
xs = [a[0] for a in p]; zs = [a[2] for a in p]
|
|
if not (min(xs) - 1e-6 <= x <= max(xs) + 1e-6 and min(zs) - 1e-6 <= z <= max(zs) + 1e-6):
|
|
continue
|
|
n = _norm(_cross(_sub(p[1], p[0]), _sub(p[2], p[0])))
|
|
if abs(n[1]) < 0.4:
|
|
continue
|
|
for tri in ([p[0], p[1], p[2]], [p[0], p[2], p[3]]) if len(p) == 4 else ([p],):
|
|
if _in_tri_xz(x, z, tri):
|
|
a = tri[0]
|
|
y = a[1] - (n[0] * (x - a[0]) + n[2] * (z - a[2])) / n[1]
|
|
if y <= below + 1e-6 and (best is None or y > best):
|
|
best = y
|
|
return best
|
|
|
|
# ------------------------------------------------------------ Objekte
|
|
def strat(self, name, x, z, y=None, rot=None, params=None, wp='relative', variant=0, hidden=False):
|
|
"""Objekt platzieren. y=None: auf die hoechste Flaeche bei (x,z) stellen.
|
|
rot: Grad (None = wie im Original). params: Liste u32 (None = Original).
|
|
wp: 'relative' (Wegpunktkette des Originals mitverschieben, y auf Boden),
|
|
'here' (alle Wegpunkte auf das Objekt), 'none' (-1) oder Liste [(x,z[,y[,rot]]),...]."""
|
|
if name not in self.protos:
|
|
raise KeyError('Strat-Typ %r nicht im Basislevel (vorhanden: %s)' % (name, ', '.join(self.types())))
|
|
if y is None:
|
|
y = self.height_at(x, z)
|
|
if y is None:
|
|
y = 0.0
|
|
d = dict(name=name, x=x, y=y, z=z, rot=rot, params=params, wp=wp, variant=variant, hidden=hidden)
|
|
self.strats.append(d)
|
|
return d
|
|
|
|
def player(self, x, z, rot=0, y=None, cam_dist=8.0):
|
|
"""Spielerstart. Der erste Wegpunkt der Lama-Kette ist (im Original) die Startposition der
|
|
Kamera -> hinter das Lama legen (Abstand cam_dist), die uebrigen auf das Lama."""
|
|
d = self.strat('Llama', x, z, y, rot, wp='here')
|
|
a = math.radians(rot or 0)
|
|
cx, cz = x - cam_dist * math.sin(a), z - cam_dist * math.cos(a)
|
|
n = 1
|
|
k = self.protos['Llama'][0][13]
|
|
while k >= 0 and self.map['nodes'][k][0] and n < 200:
|
|
k = self.map['nodes'][k][0]; n += 1
|
|
d['wp'] = [(cx, cz, d['y'] - 0.4, rot or 0)] + [(x, z, d['y'], rot or 0)] * (n - 1)
|
|
return d
|
|
|
|
def checkpoint(self, x, z, rot=None, y=None):
|
|
return self.strat('CheckPointPost', x, z, y, rot)
|
|
|
|
def exit(self, x, z, rot=None, y=None, params=None):
|
|
return self.strat('LevelDoor', x, z, y, rot, params)
|
|
|
|
def coins(self, pts, y_off=0.0, name='Coin'):
|
|
for p in pts:
|
|
yy = self.height_at(p[0], p[1])
|
|
self.strat(name, p[0], p[1], (yy or 0) + y_off)
|
|
|
|
# ------------------------------------------------------------ Bauen
|
|
def build(self, out, verbose=True):
|
|
geo = self._geometry()
|
|
models, tiles = geo
|
|
text = self._text()
|
|
trak = G.build_trak(models)
|
|
mp = self._map(models, tiles)
|
|
ch = [[t, c] for t, c in self.ch]
|
|
G.wad_set(ch, 'TRAK', trak)
|
|
G.wad_set(ch, 'MAP', mp)
|
|
G.wad_set(ch, 'TEXT', text)
|
|
nm = G.wad_get(ch, 'NAME')
|
|
if nm is not None and self.name:
|
|
G.wad_set(ch, 'NAME', nm[:4] + self.name.encode('latin1') + b'\0')
|
|
G.wad_write(out, ch)
|
|
# Gegenprobe: MAP parst wieder vollstaendig
|
|
chk = M.parse(mp, self.wf, [len(m.verts) for m in models])
|
|
assert not chk['rest'] and len(chk['strats']) == len(self._srecs)
|
|
if verbose:
|
|
nf = sum(len(m.faces) for m in models)
|
|
nc = sum(len(m.coll[0]) for m in models)
|
|
print('%s: %d Modelle, %d Kacheln, %d Dreiecke, %d Kollisionspolygone, %d Strats, %d Wegpunkte, '
|
|
'TRAK %d KB, WAD %d KB' % (out, len(models), len(tiles), nf, nc, len(self._srecs),
|
|
len(self._nodes), len(trak) // 1024, os.path.getsize(out) // 1024))
|
|
return out
|
|
|
|
# -- Beleuchtung
|
|
def _light(self, P, N, tint):
|
|
"""P,N: (n,3) Spielkoordinaten; liefert (n,3) Farbe 1.0 = Texturfarbe."""
|
|
s = self.sun
|
|
az, el = math.radians(s['azimuth']), math.radians(s['elevation'])
|
|
L = np.array([math.cos(el) * math.sin(az), math.sin(el), math.cos(el) * math.cos(az)])
|
|
sky = np.array(s['sky']); gnd = np.array(s['ground']); sc = np.array(s['color'])
|
|
N0 = N
|
|
ndl = np.clip(N @ L, 0, None)
|
|
hemi = (N[:, 1:2] * 0.5 + 0.5)
|
|
amb = gnd + (sky - gnd) * hemi
|
|
B = np.array(self.solids, float) if self.solids else np.zeros((0, 6))
|
|
# Strahlen: Sonne + Halbkugel
|
|
dirs = _hemi_dirs()
|
|
shadow = np.ones(len(P))
|
|
ao = np.ones(len(P))
|
|
CH = 256
|
|
order = np.lexsort((P[:, 2] // 4, P[:, 0] // 4)) # raeumlich sortieren -> kleine Chunks
|
|
P, N = P[order], N[order]
|
|
res_shadow, res_ao = np.ones(len(P)), np.ones(len(P))
|
|
ndl = ndl[order]
|
|
topY = B[:, 4].max() if len(B) else 0
|
|
for i in range(0, len(P), CH):
|
|
p = P[i:i + CH] + N[i:i + CH] * 0.03
|
|
n = N[i:i + CH]
|
|
if len(B):
|
|
lo, hi = p.min(0), p.max(0)
|
|
r = s['ao_radius']
|
|
near = B[((B[:, 0] <= hi[0] + r) & (B[:, 3] >= lo[0] - r) & (B[:, 1] <= hi[1] + r) &
|
|
(B[:, 4] >= lo[1] - r) & (B[:, 2] <= hi[2] + r) & (B[:, 5] >= lo[2] - r))]
|
|
tm = max(0.0, (topY - lo[1]) / max(L[1], 0.05))
|
|
e = L * tm
|
|
slo, shi = np.minimum(lo, lo + e), np.maximum(hi, hi + e)
|
|
far = B[((B[:, 0] <= shi[0]) & (B[:, 3] >= slo[0]) & (B[:, 1] <= shi[1]) &
|
|
(B[:, 4] >= slo[1]) & (B[:, 2] <= shi[2]) & (B[:, 5] >= slo[2]))]
|
|
lit = ndl[i:i + CH] > 0
|
|
hit = _ray_boxes(p, np.broadcast_to(L, p.shape), far, 60.0) if len(far) else np.zeros(len(p), bool)
|
|
res_shadow[i:i + CH] = np.where(lit & hit, 0.0, 1.0)
|
|
# AO: Richtungen in der Halbkugel um n
|
|
w = np.clip(n @ dirs.T, 0, None) # (c, R)
|
|
occ = np.zeros(len(p))
|
|
for r in range(len(dirs)):
|
|
m = w[:, r] > 0.05
|
|
if not m.any():
|
|
continue
|
|
if not len(near):
|
|
continue
|
|
h = _ray_boxes(p[m], np.broadcast_to(dirs[r], p[m].shape), near, s['ao_radius'])
|
|
occ[np.where(m)[0]] += h * w[m, r]
|
|
res_ao[i:i + CH] = 1 - s['ao'] * occ / np.maximum(w.sum(1), 1e-6)
|
|
shadow = np.ones(len(P)); ao = np.ones(len(P))
|
|
shadow[order] = res_shadow; ao[order] = res_ao
|
|
ndl = np.clip(N0 @ L, 0, None)
|
|
sh = 1 - (1 - shadow) * (1 - s['shadow'])
|
|
col = amb * ao[:, None] + sc * (s['strength'] * ndl * sh)[:, None] * (0.6 + 0.4 * ao[:, None])
|
|
return col * np.asarray(tint)
|
|
|
|
# -- Kacheln/Modelle
|
|
def _geometry(self):
|
|
BAND, B0 = 10.0, -3.0 # Hoehenbaender je Kachel: [-3,7), [7,17), ...
|
|
MARGIN = 0.3
|
|
tris_by_tile = {}
|
|
coll_by_tile = {}
|
|
# Dreiecke (Exportraum) + Vertexfarben vorbereiten
|
|
allv, alln, allt = [], [], []
|
|
faces = []
|
|
for q in self.quads:
|
|
pts = q.p
|
|
n = tuple(-c for c in _norm(_cross(_sub(pts[1], pts[0]), _sub(pts[2], pts[0])))) # aussen
|
|
if len(pts) == 4:
|
|
tri = [((0, 1, 2), 0x820), ((2, 3, 0), 0x830)]
|
|
else:
|
|
tri = [((0, 1, 2), 0x820)]
|
|
for ids, fl in (tri if q.render else []):
|
|
P = [pts[k] for k in ids]
|
|
if _area(P) < 1e-8:
|
|
continue
|
|
cen = [sum(c[k] for c in P) / 3 for k in range(3)]
|
|
key = (math.floor(cen[0]), math.floor(cen[2]), math.floor((cen[1] - B0) / BAND))
|
|
vi = []
|
|
for p in P:
|
|
vi.append(len(allv)); allv.append(p); alln.append(n); allt.append(q.tint)
|
|
faces.append((key, vi, q.tex, fl | (0x400 if q.double else 0), q.shade))
|
|
if q.coll:
|
|
xs = [a[0] for a in pts]; zs = [a[2] for a in pts]
|
|
cy = sum(a[1] for a in pts) / len(pts)
|
|
band = math.floor((cy - B0) / BAND)
|
|
# Boeden: nur ueberdeckte Zellen; Waende/Schraegen: auch Nachbarzellen (Spielerradius)
|
|
mg = 0.0 if abs(n[1]) > 0.95 else MARGIN
|
|
for cx in range(math.floor(min(xs) - mg), math.floor(max(xs) + mg - 1e-6) + 1):
|
|
for cz in range(math.floor(min(zs) - mg), math.floor(max(zs) + mg - 1e-6) + 1):
|
|
coll_by_tile.setdefault((cx, cz, band), []).append((pts, q.mat))
|
|
P = np.array(allv, float); N = np.array(alln, float); T = np.array(allt, float)
|
|
if len(P):
|
|
key = np.round(np.hstack([P, N]), 4)
|
|
uk, inv = np.unique(key, axis=0, return_inverse=True)
|
|
C = self._light(uk[:, :3], uk[:, 3:], np.ones((len(uk), 3)))[inv.reshape(-1)] * T
|
|
else:
|
|
C = np.zeros((0, 3))
|
|
for key, vi, tex, fl, shade in faces:
|
|
tris_by_tile.setdefault(key, []).append((vi, tex, fl, shade))
|
|
models, tiles, cache = [], [], {}
|
|
kill_model = None
|
|
texinfo = lambda t: (self.texs[t]['page'], self.texs[t]['flags']) if t < len(self.texs) else (-1, 0x105)
|
|
for key in sorted(set(tris_by_tile) | set(coll_by_tile)):
|
|
cx, cz, band = key
|
|
if not (0 <= cx < self.W and 0 <= cz < self.H):
|
|
raise SystemExit('Geometrie ausserhalb des Rasters %dx%d: Zelle %d,%d' % (self.W, self.H, cx, cz))
|
|
ox, oy, oz = cx + 0.5, B0 + band * BAND + BAND / 2, cz + 0.5
|
|
m = G.Model(); m.dirty = True
|
|
vmap, cols, nacc = {}, [], []
|
|
flist = sorted(tris_by_tile.get(key, []), key=lambda f: (texinfo(f[1]), f[1]))
|
|
fdata = []
|
|
for vi, tex, fl, shade in flist:
|
|
ids = []
|
|
for k in vi:
|
|
lp = tuple(round(c, 5) for c in to_exp((P[k][0] - ox, P[k][1] - oy, P[k][2] - oz)))
|
|
col = C[k] if shade else np.ones(3)
|
|
cu = _col_u32(col)
|
|
kk = (lp, tuple(float(x) + 0.0 for x in np.round(N[k], 3)))
|
|
if kk not in vmap:
|
|
vmap[kk] = len(m.verts)
|
|
m.verts.append(lp + tuple(to_exp(N[k])))
|
|
cols.append(cu)
|
|
ids.append(vmap[kk])
|
|
if len(set(ids)) < 3:
|
|
continue
|
|
fdata.append((ids, tex, fl))
|
|
prev = None
|
|
for ids, tex, fl in fdata:
|
|
plane = G.face_plane(*[m.verts[i][:3] for i in ids])
|
|
st = texinfo(tex)
|
|
if st != prev or len(m.faces) % 256 == 0:
|
|
fl |= 0x8
|
|
prev = st
|
|
m.faces.append(dict(flags=fl, v=tuple(ids), tex=tex, pad=0, plane=plane))
|
|
for pts, mat in coll_by_tile.get(key, []):
|
|
loc = [to_exp((p[0] - ox, p[1] - oy, p[2] - oz)) for p in pts]
|
|
if max(abs(c) for p in loc for c in p) > 7.9:
|
|
continue
|
|
recs = []
|
|
r = G.coll_from_polygon(loc, mat)
|
|
if r is not None:
|
|
recs = [r]
|
|
for r in recs:
|
|
if struct.unpack_from('<b', r, 3)[0] < 0:
|
|
r = bytes([r[0] | 4]) + r[1:]
|
|
m.coll[0].append(r)
|
|
if not m.faces and not m.coll[0]:
|
|
continue
|
|
# Box der Kachel muss die Kollisionspolygone umfassen (unbenutzte Hilfsvertices)
|
|
cpts = [to_exp((p[0] - ox, p[1] - oy, p[2] - oz)) for pts, _ in coll_by_tile.get(key, []) for p in pts]
|
|
if cpts:
|
|
lo = [min(c[k] for c in cpts) for k in range(3)]
|
|
hi = [max(c[k] for c in cpts) for k in range(3)]
|
|
if m.verts:
|
|
lo = [min(lo[k], min(v[k] for v in m.verts)) for k in range(3)]
|
|
hi = [max(hi[k], max(v[k] for v in m.verts)) for k in range(3)]
|
|
vlo = [min(v[k] for v in m.verts) for k in range(3)]
|
|
vhi = [max(v[k] for v in m.verts) for k in range(3)]
|
|
need = any(lo[k] < vlo[k] - 1e-4 or hi[k] > vhi[k] + 1e-4 for k in range(3))
|
|
else:
|
|
need = True
|
|
if need:
|
|
for c in (lo, hi):
|
|
m.verts.append((round(c[0], 5), round(c[1], 5), round(c[2], 5), 0.0, 1.0, 0.0))
|
|
cols.append(0x01808080)
|
|
if len(m.verts) > 4000:
|
|
raise SystemExit('Kachel %s zu gross (%d Vertices)' % (key, len(m.verts)))
|
|
if not m.verts: # reine Kollisionskachel braucht keine Vertices
|
|
pass
|
|
h = _model_key(m)
|
|
if h not in cache:
|
|
cache[h] = len(models)
|
|
models.append(m)
|
|
tiles.append(dict(x=ox, y=oy, z=-oz, rot=0, model=cache[h], cols=cols))
|
|
# Todesebene (Original-Modell 0 des Dorfes: unsichtbar, Material 0x1e, y_lokal = -2.75)
|
|
if self.kill:
|
|
ky, x0, z0, x1, z1 = self.kill
|
|
km = self.trak0[0]
|
|
kill_model = len(models)
|
|
models.append(km)
|
|
ty = ky + 2.75
|
|
# nur Zellen ohne begehbaren Boden (spart Kacheln; unter Boden faellt man nicht)
|
|
covered = self._covered_cells()
|
|
for cx in range(max(0, math.floor(x0)), min(self.W, math.ceil(x1))):
|
|
for cz in range(max(0, math.floor(z0)), min(self.H, math.ceil(z1))):
|
|
if (cx, cz) in covered:
|
|
continue
|
|
tiles.append(dict(x=cx + 0.5, y=ty, z=-(cz + 0.5), rot=0, model=kill_model,
|
|
cols=[0x01404040] * len(km.verts)))
|
|
return models, tiles
|
|
|
|
def _covered_cells(self):
|
|
"""Zellen, die vollstaendig von nach oben zeigender Kollision (Boden) bedeckt sind."""
|
|
area = {}
|
|
for q in self.quads:
|
|
if not q.coll or len(q.p) != 4:
|
|
continue
|
|
p = q.p
|
|
n = _norm(_cross(_sub(p[1], p[0]), _sub(p[2], p[0])))
|
|
if abs(n[1]) < 0.7:
|
|
continue
|
|
xs = [a[0] for a in p]; zs = [a[2] for a in p]
|
|
for cx in range(math.floor(min(xs)), math.ceil(max(xs))):
|
|
for cz in range(math.floor(min(zs)), math.ceil(max(zs))):
|
|
ov = max(0, min(max(xs), cx + 1) - max(min(xs), cx)) * max(0, min(max(zs), cz + 1) - max(min(zs), cz))
|
|
area[(cx, cz)] = area.get((cx, cz), 0) + ov
|
|
return {k for k, v in area.items() if v > 0.999}
|
|
|
|
def _text(self):
|
|
d = G.wad_get(self.ch, 'TEXT')
|
|
if not self.flats_used:
|
|
return d
|
|
o = 0
|
|
npal = struct.unpack_from('<I', d, o)[0]; o += 4
|
|
for _ in range(npal):
|
|
n = struct.unpack_from('<I', d, o + 4)[0]; o += 8 + 3 * n
|
|
npg = struct.unpack_from('<I', d, o)[0]; o += 4
|
|
for _ in range(npg):
|
|
fl, w, h = struct.unpack_from('<3I', d, o); o += 12
|
|
if fl == 0:
|
|
o += w * h
|
|
elif fl & 0x80:
|
|
o += 4 + struct.unpack_from('<I', d, o)[0]
|
|
else:
|
|
o += w * h * 2
|
|
nt = struct.unpack_from('<I', d, o)[0]
|
|
assert nt == len(self.texs)
|
|
add = b''
|
|
for name, idx in sorted(self.flats_used.items(), key=lambda kv: kv[1]):
|
|
r, g, b = FLAT[name]
|
|
add += struct.pack('<HbbBBBB', 0x105, -1, -1, r, g, b, 0)
|
|
e = o + 4 + 8 * nt
|
|
return d[:o] + struct.pack('<I', nt + len(self.flats_used)) + d[o + 4:e] + add + d[e:]
|
|
|
|
def _map(self, models, tiles):
|
|
m = dict(self.map)
|
|
m['tiles'] = [(t['x'], t['y'], t['z'], 0, t['model'], 0) for t in tiles]
|
|
m['tiles2'] = [tuple(v & 0xffffffff for v in (0, 0, 0, int(round(t['x'] * FX)), int(round(t['y'] * FX)),
|
|
int(round(-t['z'] * FX)))) for t in tiles]
|
|
m['tilemodel'] = [t['model'] for t in tiles]
|
|
m['alts'] = []
|
|
m['vcol'] = b''.join(struct.pack('<%dI' % len(t['cols']), *t['cols']) for t in tiles)
|
|
for t in tiles:
|
|
assert len(t['cols']) == len(models[t['model']].verts)
|
|
tab = list(m['tab32'])
|
|
tab[0] = 0xffffffff
|
|
m['tab32'] = tuple(tab)
|
|
m['grid'] = (0,) * (self.W * self.H)
|
|
# Strats + Wegpunkte
|
|
pool = list(self.pool0)
|
|
nodes = []
|
|
recs = []
|
|
orig_nodes = self.map['nodes']
|
|
for d in self.strats:
|
|
proto = list(self.protos[d['name']][d['variant']])
|
|
s = list(proto)
|
|
if d['rot'] is not None:
|
|
s[1] = deg(d['rot'])
|
|
s[3], s[4], s[5] = int(round(d['x'] * FX)), int(round(d['y'] * FX)), int(round(d['z'] * FX))
|
|
prm = d['params']
|
|
if prm is None:
|
|
if d['name'] in self.foreign_params:
|
|
prm = self.foreign_params[d['name']]
|
|
else:
|
|
prm = self.pool0[proto[8]:proto[8] + proto[7]] if proto[7] else []
|
|
if len(prm) != proto[7]:
|
|
raise ValueError('%s braucht %d Parameter, nicht %d' % (d['name'], proto[7], len(prm)))
|
|
if prm:
|
|
s[8] = len(pool)
|
|
pool.extend(int(v) & 0xffffffff for v in prm)
|
|
else:
|
|
s[8] = -1
|
|
# Wegpunkte
|
|
wp = d['wp']
|
|
chain = []
|
|
if proto[13] >= 0:
|
|
k = proto[13]
|
|
while True:
|
|
chain.append(orig_nodes[k])
|
|
nx = orig_nodes[k][0]
|
|
if not nx or len(chain) > 200:
|
|
break
|
|
k = nx
|
|
new = []
|
|
if isinstance(wp, (list, tuple)):
|
|
for i, p in enumerate(wp):
|
|
px, pz = p[0], p[1]
|
|
py = p[2] if len(p) > 2 and p[2] is not None else (self.height_at(px, pz) or d['y'])
|
|
pr = deg(p[3]) if len(p) > 3 else s[1]
|
|
# Zusatzfelder (Wartezeit/Modus o.ae.) vom Original-Wegpunkt uebernehmen
|
|
a_, b_ = (chain[i][8], chain[i][9]) if i < len(chain) else (0, 0)
|
|
new.append([0, 0, 0, pr, 0, int(px * FX), int(py * FX), int(pz * FX), a_, b_])
|
|
elif chain and wp != 'none':
|
|
ox, oy, oz = proto[3], proto[4], proto[5]
|
|
for nd in chain:
|
|
nd = list(nd)
|
|
if wp == 'here':
|
|
nd[5], nd[6], nd[7] = s[3], s[4], s[5]
|
|
else:
|
|
nx_ = _s32(nd[5]) - ox + s[3]
|
|
nz_ = _s32(nd[7]) - oz + s[5]
|
|
ny_ = _s32(nd[6]) - oy + s[4]
|
|
g = self.height_at(nx_ / FX, nz_ / FX, below=ny_ / FX + 1.0)
|
|
if g is not None and abs(g - ny_ / FX) < 2.0:
|
|
ny_ = int(g * FX)
|
|
nd[5], nd[6], nd[7] = nx_, ny_, nz_
|
|
new.append(nd)
|
|
if new:
|
|
base = len(nodes)
|
|
for i, nd in enumerate(new):
|
|
nd = list(nd)
|
|
nd[0] = base + i + 1 if i < len(new) - 1 else 0
|
|
nd[5:8] = [v & 0xffffffff for v in nd[5:8]]
|
|
nodes.append(tuple(nd))
|
|
s[13] = base
|
|
else:
|
|
s[13] = -1
|
|
s[15] = 2 if d['hidden'] else 0
|
|
s[3], s[4], s[5] = (v for v in (s[3], s[4], s[5]))
|
|
recs.append(tuple(s))
|
|
m['list1'] = pool
|
|
m['nodes'] = nodes if nodes else [tuple([0] * 10)]
|
|
m['strats'] = recs
|
|
m['strat_x'] = max(self.map['strat_x'], len(recs) + 80)
|
|
self._srecs, self._nodes = recs, nodes
|
|
return M.build(m, self.wf)
|
|
|
|
|
|
# ---------------------------------------------------------------- Hilfsfunktionen
|
|
def _s32(v):
|
|
return v - (1 << 32) if v >= 1 << 31 else v
|
|
|
|
|
|
def _cuts(a, b, step, align=True):
|
|
a, b = min(a, b), max(a, b)
|
|
if not align:
|
|
n = max(1, int(round((b - a) / step)))
|
|
return [a + (b - a) * i / n for i in range(n + 1)]
|
|
out = [a]
|
|
k = math.floor(a / step + 1e-9) + 1
|
|
while k * step < b - 1e-6:
|
|
if k * step - out[-1] > 0.05:
|
|
out.append(k * step)
|
|
k += 1
|
|
if b - out[-1] < 0.05 and len(out) > 1:
|
|
out[-1] = b
|
|
else:
|
|
out.append(b)
|
|
return out
|
|
|
|
|
|
def _wall_normal(a, b):
|
|
dx, dz = b[0] - a[0], b[1] - a[1]
|
|
n = _norm((-dz, 0, dx))
|
|
return n
|
|
|
|
|
|
def _orient(p, normal):
|
|
"""Reihenfolge so, dass die Flaeche nach 'normal' (Spielraum) zeigt. Die Engine rechnet im
|
|
Exportraum (z gespiegelt): dort muss (p1-p0)x(p2-p0) nach aussen zeigen, im Spielraum also
|
|
nach innen (Spiegelung dreht den Umlaufsinn)."""
|
|
n = _cross(_sub(p[1], p[0]), _sub(p[2], p[0]))
|
|
if sum(a * b for a, b in zip(n, normal)) > 0:
|
|
# spiegeln, Ecke 0 bleibt unten-links -> [1,0,3,2]
|
|
p = [p[1], p[0], p[3], p[2]] if len(p) == 4 else [p[1], p[0], p[2]]
|
|
return p
|
|
|
|
|
|
def _aabb(pts):
|
|
xs = [p[0] for p in pts]; ys = [p[1] for p in pts]; zs = [p[2] for p in pts]
|
|
return (min(xs), min(ys), min(zs), max(xs), max(ys), max(zs))
|
|
|
|
|
|
def _area(P):
|
|
c = _cross(_sub(P[1], P[0]), _sub(P[2], P[0]))
|
|
return math.sqrt(sum(x * x for x in c)) / 2
|
|
|
|
|
|
def _in_tri_xz(x, z, t):
|
|
(ax, _, az), (bx, _, bz), (cx, _, cz) = t
|
|
d1 = (x - bx) * (az - bz) - (ax - bx) * (z - bz)
|
|
d2 = (x - cx) * (bz - cz) - (bx - cx) * (z - cz)
|
|
d3 = (x - ax) * (cz - az) - (cx - ax) * (z - az)
|
|
neg = d1 < -1e-9 or d2 < -1e-9 or d3 < -1e-9
|
|
pos = d1 > 1e-9 or d2 > 1e-9 or d3 > 1e-9
|
|
return not (neg and pos)
|
|
|
|
|
|
def _col_u32(c):
|
|
r, g, b = (max(0, min(255, int(round(v * 128)))) for v in c)
|
|
return (1 << 24) | (b << 16) | (g << 8) | r
|
|
|
|
|
|
def _model_key(m):
|
|
h = hashlib.sha1()
|
|
for v in m.verts:
|
|
h.update(struct.pack('<6f', *v))
|
|
for f in m.faces:
|
|
h.update(struct.pack('<5H', f['flags'], *f['v'], f['tex']))
|
|
for r in m.coll[0]:
|
|
h.update(r)
|
|
return h.hexdigest()
|
|
|
|
|
|
_HEMI = None
|
|
|
|
|
|
def _hemi_dirs():
|
|
global _HEMI
|
|
if _HEMI is None:
|
|
d = []
|
|
# Fibonacci-Kugel, 24 Richtungen
|
|
n = 24
|
|
for i in range(n):
|
|
y = 1 - 2 * (i + 0.5) / n
|
|
r = math.sqrt(1 - y * y)
|
|
a = i * math.pi * (3 - math.sqrt(5))
|
|
d.append((r * math.cos(a), y, r * math.sin(a)))
|
|
_HEMI = np.array(d)
|
|
return _HEMI
|
|
|
|
|
|
def _ray_boxes(p, d, B, tmax):
|
|
"""p,d: (n,3); B: (b,6). Liefert bool (n,): trifft irgendeine Box mit 0<t<tmax."""
|
|
if len(p) == 0:
|
|
return np.zeros(0, bool)
|
|
with np.errstate(divide='ignore', invalid='ignore'):
|
|
inv = 1.0 / np.where(np.abs(d) < 1e-9, 1e-9, d)
|
|
lo = B[None, :, 0:3]; hi = B[None, :, 3:6]
|
|
t1 = (lo - p[:, None, :]) * inv[:, None, :]
|
|
t2 = (hi - p[:, None, :]) * inv[:, None, :]
|
|
tn = np.minimum(t1, t2).max(2)
|
|
tf = np.maximum(t1, t2).min(2)
|
|
hit = (tf >= np.maximum(tn, 1e-4)) & (tn < tmax)
|
|
return hit.any(1)
|
|
|
|
|
|
# ---------------------------------------------------------------- CLI
|
|
def load_script(path):
|
|
spec = importlib.util.spec_from_file_location('levelscript', path)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument('level', nargs='?')
|
|
ap.add_argument('out', nargs='?')
|
|
ap.add_argument('--base', default=BASE)
|
|
ap.add_argument('--install', action='store_true', help='als TestLevel (T1L9M001.wad) installieren')
|
|
ap.add_argument('--types', action='store_true')
|
|
ap.add_argument('--textures', metavar='PNG')
|
|
ap.add_argument('--set', action='append', default=[], help='Variable fuer das Levelskript, z.B. --set teil=a')
|
|
a = ap.parse_args()
|
|
if a.types:
|
|
L = Level(a.base)
|
|
for n in L.types():
|
|
p = L.protos[n][0]
|
|
print('%-28s Varianten %d Parameter %d Wegpunkte %s' % (n, len(L.protos[n]), p[7],
|
|
'ja' if p[13] >= 0 else '-'))
|
|
return
|
|
if a.textures:
|
|
import subprocess
|
|
names = {v: k for k, v in TEX.items()}
|
|
subprocess.run([sys.executable, os.path.join(HERE, 'texsheet.py'), a.base, a.textures], check=True)
|
|
for i in sorted(names):
|
|
print('%4d %s' % (i, names[i]))
|
|
return
|
|
if not (a.level and a.out):
|
|
ap.error('LEVEL.py und OUT.wad angeben')
|
|
mod = load_script(a.level)
|
|
opts = dict(kv.split('=', 1) for kv in a.set)
|
|
L = Level(a.base, **getattr(mod, 'LEVEL_ARGS', {}))
|
|
L.opts = opts
|
|
mod.build(L)
|
|
L.build(a.out)
|
|
if hasattr(mod, 'report'):
|
|
mod.report(L)
|
|
if a.install:
|
|
import shutil
|
|
if not os.path.exists(MODWAD + '.orig'):
|
|
shutil.copy2(MODWAD, MODWAD + '.orig')
|
|
shutil.copyfile(a.out, MODWAD)
|
|
print('installiert ->', MODWAD)
|
|
kapitel_links()
|
|
|
|
|
|
def kapitel_links(n=49):
|
|
"""Kapitel 2..n von Welt 9 als Symlinks auf Kapitel 1 (WAD + Ladebild T1%d%d.COR) in der Mod.
|
|
Grund: jeder Ausgang (LevelDoor) schaltet im hoechsten freien Kapitel das naechste frei;
|
|
fehlt dessen WAD, stuerzt das Spiel bei der Auswahl ab ("Couldnt load wad")."""
|
|
wd = os.path.dirname(MODWAD)
|
|
mod = os.path.dirname(wd)
|
|
neu = 0
|
|
for k in range(2, n + 1):
|
|
for d, name, ziel in ((wd, 'T1L9M%03d.wad' % k, 'T1L9M001.wad'), (mod, 'T19%d.COR' % k, 'T191.COR')):
|
|
p = os.path.join(d, name)
|
|
if not os.path.lexists(p) and os.path.exists(os.path.join(d, ziel)):
|
|
os.symlink(ziel, p)
|
|
neu += 1
|
|
if neu:
|
|
import subprocess
|
|
print('%d Kapitel-Links angelegt -> lama-mod an' % neu)
|
|
subprocess.run([os.path.expanduser('~/.local/bin/lama-mod'), 'an'], check=True,
|
|
stdout=subprocess.DEVNULL)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|