translated the remaining german notes, python comments, help and messages to english (in-game strings, parsed report lines and cli values stay german)

This commit is contained in:
MineTec committed 2026-11-07 10:51:14 +01:00
1 parent 86fe26d786
commit 526f6072f6
36 files changed
+1553 -1544

No files matched your search

+109 -108
View File
@@ -1,19 +1,20 @@
#!/usr/bin/env python3
"""levelbuild - eigene Level fuer "Ein Koenigreich fuer ein Lama" bauen (Croc-2-Engine, PC).
"""levelbuild - build custom levels for "Ein Koenigreich fuer ein Lama" (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).
A level is described as a Python script containing a function ``build(L)``.
``L`` is a :class:`Level` with primitives (boxes, platforms, ramps, stairs, ...),
textures by name, automatic vertex colour lighting (sun, sky, shadows,
ambient occlusion) and object placement (strats by type name, parameters from the 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)
levelbuild.py --types [--base ...] placeable strat types of the base level
levelbuild.py --textures OUT.png texture contact sheet (with names/indices)
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
Coordinates (as in the game / for strats): x east, y up, z = grid row
(positive). 1 unit = 1 grid cell. The base level's grid is 96x96 (x,z in 0..96).
Angles in degrees (0..360) about the y axis.
Texture names (TEX/FLAT below) are German keys and part of the level script API.
Docs: ../notes/levelbau.md
"""
import argparse
import hashlib
@@ -38,7 +39,7 @@ 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
# texture names for the village (t1l1m001). Contact sheet: 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,
@@ -53,14 +54,14 @@ TEX = {
'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)
# additional flat colours (appended to the texture table): 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
# strat types present in the village code but not placed in the village:
# take type fields (nvars, slots, flags, extra) + parameters from another level
FOREIGN = {'RubyIdol': 't1l1m002.wad'}
@@ -68,14 +69,14 @@ def deg(a):
return int(round((a % 360) / 360 * 4096)) & 0xfff
# ---------------------------------------------------------------- Geometrie-Sammelbehaelter
# ---------------------------------------------------------------- geometry containers
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.p = p # 3 or 4 points (game coordinates)
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)
self.render = render # False: collision only (invisible)
def _sub(a, b):
@@ -92,19 +93,19 @@ def _norm(v):
def to_exp(p):
"""Spielraum (z positiv) -> Exportraum/Vertexraum (z negativ)."""
"""Game space (z positive) -> export/vertex space (z negative)."""
return (p[0], p[1], -p[2])
class Level:
"""Baukasten. Alle Methoden nehmen Spielkoordinaten (x, z = Rasterzelle, y oben)."""
"""Construction kit. All methods take game coordinates (x, z = grid cell, y up)."""
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.solids = [] # AABBs (x0,y0,z0,x1,y1,z1) for shadows/ambient occlusion
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,
@@ -113,7 +114,7 @@ class Level:
self.flats_used = {}
self._load()
# ------------------------------------------------------------ Basislevel
# ------------------------------------------------------------ base level
def _load(self):
self.ch = G.wad_read(self.base)
self.wf = G.wfpc_flags(self.ch)
@@ -125,7 +126,7 @@ class Level:
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]
self.protos = {} # name -> [instance tuple per code variant]
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, [])
@@ -153,7 +154,7 @@ class Level:
return sorted(self.protos)
def tex(self, t):
"""Texturname, Index (int) oder Flachfarbe ('meer' / (r,g,b))."""
"""Texture name, index (int) or flat colour ('meer' / (r,g,b))."""
if isinstance(t, int):
return t
if isinstance(t, tuple):
@@ -166,36 +167,36 @@ class Level:
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))))
raise KeyError('unknown texture %r (names: %s)' % (t, ', '.join(sorted(TEX) + sorted(FLAT))))
# ------------------------------------------------------------ Grundbausteine
# ------------------------------------------------------------ basic building blocks
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."""
"""A quad p = [bottom-left, bottom-right, top-right, top-left] seen from the front
(texture upright). rot = 0..3 rotates the texture in 90-degree steps."""
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'."""
"""Invisible collision surface (3/4 points), front side towards '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."""
"""Horizontal surface, subdivided into tiles (step), aligned to integers."""
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
r = self.rng.choice((0, 2)) if rot == 'random' else rot # 2 variants: fewer models
if up: # seen from above: bottom-left = (xa, zb)? -> normal +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)."""
"""Vertical wall from a=(x,z) to b=(x,z), outside = right of a->b (seen from above, z pointing down)."""
L = math.hypot(b[0] - a[0], b[1] - a[1])
if L < 1e-6 or y1 - y0 < 1e-6:
return
@@ -212,13 +213,13 @@ class Level:
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)."""
"""Flat floor surface (top side only)."""
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,
"""Cuboid. side_top: texture for the top row of the side walls (e.g. 'graskante').
bottom: draw the underside (for floating platforms). sides: which sides (n=z0,
s=z1, w=x0, e=x1)."""
x0, x1 = min(x0, x1), max(x0, x1)
z0, z1 = min(z0, z1), max(z0, z1)
@@ -229,7 +230,7 @@ class Level:
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
# walls oriented so that the outside is on the right
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:
@@ -243,18 +244,18 @@ class Level:
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."""
"""Floating platform: top edge y, corner (x,z), size 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)."""
"""Pillar centred at (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."""
"""Ramp over the rectangle; rises in 'direction' (+x,-x,+z,-z) from ya to yb.
Bottom edge 'base' (default: ya - 0.5); sides and front face are closed."""
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
@@ -271,7 +272,7 @@ class Level:
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
# sides: trapezoids in strips
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:
@@ -290,15 +291,15 @@ class Level:
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)."""
"""Stairs with n steps on the rectangle, rising in 'direction' (+x,-x,+z,-z) from y0 to y1.
walkable=True: collision as an invisible slope (the llama can walk up; the engine has
no step logic, even 0.12-high edges block), otherwise real steps (jump)."""
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
# map to local coordinates u (rise direction 0..1) / v (across 0..1)
def P(u, v, y):
if neg:
u = 1 - u
@@ -315,18 +316,18 @@ class Level:
yt = y0 + (y1 - y0) * (i + 1) / n
yl = base if i == 0 else y0 + (y1 - y0) * i / n
last = i == n - 1
# Trittflaeche
# tread
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
# riser (front face), foot down to 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)
# side walls of this step (down to 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
# back wall
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:
@@ -335,14 +336,14 @@ class Level:
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."""
"""Fence (posts + rail) along point list [(x,z),...] at height y. With collision."""
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
# rail as a thin cuboid
t = thick * 0.6
yb = y + height * 0.55
if abs(bx - ax) >= abs(bz - az):
@@ -353,7 +354,7 @@ class Level:
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)."""
"""Simple blocky tree (trunk + two crown cubes)."""
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,
@@ -363,15 +364,15 @@ class Level:
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."""
"""Water/ground surface without collision (coarsely subdivided, step) + deadly plane just
above it. kill=(x0,z0,x1,z1): area of the death plane (default: whole surface, False: none).
The death plane is only created in cells without continuous floor."""
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."""
"""OBJ file (game coordinates, y up). 'usemtl tex_N' or a texture name as material."""
V, cur = [], tex
for line in open(path, encoding='latin1'):
p = line.split()
@@ -391,9 +392,9 @@ class Level:
for k in range(1, len(pts) - 1):
self.quad([pts[0], pts[k], pts[k + 1]], cur, coll=coll)
# ------------------------------------------------------------ Hoehe
# ------------------------------------------------------------ height
def height_at(self, x, z, below=1e9):
"""Hoechste begehbare Flaeche bei (x,z) unterhalb 'below' (oder None)."""
"""Highest walkable surface at (x,z) below 'below' (or None)."""
best = None
for q in self.quads:
if not q.coll:
@@ -413,14 +414,14 @@ class Level:
best = y
return best
# ------------------------------------------------------------ Objekte
# ------------------------------------------------------------ objects
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]]),...]."""
"""Place an object. y=None: put it on the highest surface at (x,z).
rot: degrees (None = as in the original). params: list of u32 (None = original).
wp: 'relative' (move the original's waypoint chain along, y snapped to the ground),
'here' (all waypoints on the object), 'none' (-1) or list [(x,z[,y[,rot]]),...]."""
if name not in self.protos:
raise KeyError('Strat-Typ %r nicht im Basislevel (vorhanden: %s)' % (name, ', '.join(self.types())))
raise KeyError('strat type %r not in base level (available: %s)' % (name, ', '.join(self.types())))
if y is None:
y = self.height_at(x, z)
if y is None:
@@ -430,8 +431,8 @@ class Level:
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."""
"""Player start. The first waypoint of the llama chain is (in the original) the camera's
start position -> put it behind the llama (distance cam_dist), the rest on the llama."""
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)
@@ -453,7 +454,7 @@ class Level:
yy = self.height_at(p[0], p[1])
self.strat(name, p[0], p[1], (yy or 0) + y_off)
# ------------------------------------------------------------ Bauen
# ------------------------------------------------------------ build
def build(self, out, verbose=True):
geo = self._geometry()
models, tiles = geo
@@ -468,20 +469,20 @@ class Level:
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
# cross-check: MAP parses back completely
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, '
print('%s: %d models, %d tiles, %d triangles, %d collision polygons, %d strats, %d waypoints, '
'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
# -- lighting
def _light(self, P, N, tint):
"""P,N: (n,3) Spielkoordinaten; liefert (n,3) Farbe 1.0 = Texturfarbe."""
"""P,N: (n,3) game coordinates; returns (n,3) colour, 1.0 = texture colour."""
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)])
@@ -491,12 +492,12 @@ class Level:
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
# rays: sun + hemisphere
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
order = np.lexsort((P[:, 2] // 4, P[:, 0] // 4)) # sort spatially -> small chunks
P, N = P[order], N[order]
res_shadow, res_ao = np.ones(len(P)), np.ones(len(P))
ndl = ndl[order]
@@ -517,7 +518,7 @@ class Level:
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
# AO: directions in the hemisphere around n
w = np.clip(n @ dirs.T, 0, None) # (c, R)
occ = np.zeros(len(p))
for r in range(len(dirs)):
@@ -536,18 +537,18 @@ class Level:
col = amb * ao[:, None] + sc * (s['strength'] * ndl * sh)[:, None] * (0.6 + 0.4 * ao[:, None])
return col * np.asarray(tint)
# -- Kacheln/Modelle
# -- tiles/models
def _geometry(self):
BAND, B0 = 10.0, -3.0 # Hoehenbaender je Kachel: [-3,7), [7,17), ...
BAND, B0 = 10.0, -3.0 # height bands per tile: [-3,7), [7,17), ...
MARGIN = 0.3
tris_by_tile = {}
coll_by_tile = {}
# Dreiecke (Exportraum) + Vertexfarben vorbereiten
# prepare triangles (export space) + vertex colours
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
n = tuple(-c for c in _norm(_cross(_sub(pts[1], pts[0]), _sub(pts[2], pts[0])))) # outwards
if len(pts) == 4:
tri = [((0, 1, 2), 0x820), ((2, 3, 0), 0x830)]
else:
@@ -566,7 +567,7 @@ class Level:
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)
# floors: only covered cells; walls/slopes: neighbouring cells too (player radius)
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):
@@ -586,7 +587,7 @@ class Level:
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))
raise SystemExit('geometry outside the %dx%d grid: cell %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 = {}, [], []
@@ -629,7 +630,7 @@ class Level:
m.coll[0].append(r)
if not m.faces and not m.coll[0]:
continue
# Box der Kachel muss die Kollisionspolygone umfassen (unbenutzte Hilfsvertices)
# the tile's box must enclose the collision polygons (unused helper vertices)
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)]
@@ -647,22 +648,22 @@ class Level:
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
raise SystemExit('tile %s too large (%d vertices)' % (key, len(m.verts)))
if not m.verts: # a collision-only tile needs no 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)
# death plane (the village's original model 0: invisible, material 0x1e, y_local = -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)
# only cells without walkable floor (saves tiles; you can't fall below a floor)
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))):
@@ -673,7 +674,7 @@ class Level:
return models, tiles
def _covered_cells(self):
"""Zellen, die vollstaendig von nach oben zeigender Kollision (Boden) bedeckt sind."""
"""Cells fully covered by upward-facing collision (floor)."""
area = {}
for q in self.quads:
if not q.coll or len(q.p) != 4:
@@ -729,7 +730,7 @@ class Level:
tab[0] = 0xffffffff
m['tab32'] = tuple(tab)
m['grid'] = (0,) * (self.W * self.H)
# Strats + Wegpunkte
# strats + waypoints
pool = list(self.pool0)
nodes = []
recs = []
@@ -747,13 +748,13 @@ class Level:
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)))
raise ValueError('%s needs %d parameters, not %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
# waypoints
wp = d['wp']
chain = []
if proto[13] >= 0:
@@ -770,7 +771,7 @@ class Level:
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
# copy extra fields (wait time/mode etc.) from the original waypoint
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':
@@ -809,7 +810,7 @@ class Level:
return M.build(m, self.wf)
# ---------------------------------------------------------------- Hilfsfunktionen
# ---------------------------------------------------------------- helpers
def _s32(v):
return v - (1 << 32) if v >= 1 << 31 else v
@@ -839,12 +840,12 @@ def _wall_normal(a, b):
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)."""
"""Order the points so the surface faces 'normal' (game space). The engine works in
export space (z mirrored): there (p1-p0)x(p2-p0) must point outwards, i.e. inwards in
game space (mirroring reverses the winding)."""
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]
# mirror, corner 0 stays bottom-left -> [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
@@ -892,7 +893,7 @@ def _hemi_dirs():
global _HEMI
if _HEMI is None:
d = []
# Fibonacci-Kugel, 24 Richtungen
# Fibonacci sphere, 24 directions
n = 24
for i in range(n):
y = 1 - 2 * (i + 0.5) / n
@@ -904,7 +905,7 @@ def _hemi_dirs():
def _ray_boxes(p, d, B, tmax):
"""p,d: (n,3); B: (b,6). Liefert bool (n,): trifft irgendeine Box mit 0<t<tmax."""
"""p,d: (n,3); B: (b,6). Returns bool (n,): hits any box with 0<t<tmax."""
if len(p) == 0:
return np.zeros(0, bool)
with np.errstate(divide='ignore', invalid='ignore'):
@@ -931,17 +932,17 @@ def main():
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('--install', action='store_true', help='install as TestLevel (T1L9M001.wad)')
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')
ap.add_argument('--set', action='append', default=[], help='variable for the level script, e.g. --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 '-'))
print('%-28s variants %d parameters %d waypoints %s' % (n, len(L.protos[n]), p[7],
'yes' if p[13] >= 0 else '-'))
return
if a.textures:
import subprocess
@@ -951,7 +952,7 @@ def main():
print('%4d %s' % (i, names[i]))
return
if not (a.level and a.out):
ap.error('LEVEL.py und OUT.wad angeben')
ap.error('specify LEVEL.py and OUT.wad')
mod = load_script(a.level)
opts = dict(kv.split('=', 1) for kv in a.set)
L = Level(a.base, **getattr(mod, 'LEVEL_ARGS', {}))
@@ -965,14 +966,14 @@ def main():
if not os.path.exists(MODWAD + '.orig'):
shutil.copy2(MODWAD, MODWAD + '.orig')
shutil.copyfile(a.out, MODWAD)
print('installiert ->', MODWAD)
print('installed ->', 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")."""
"""Chapters 2..n of world 9 as symlinks to chapter 1 (WAD + loading image T1%d%d.COR) in the mod.
Reason: every exit (LevelDoor) in the highest unlocked chapter unlocks the next one;
if its WAD is missing, the game crashes on selecting it ("Couldnt load wad")."""
wd = os.path.dirname(MODWAD)
mod = os.path.dirname(wd)
neu = 0
@@ -984,7 +985,7 @@ def kapitel_links(n=49):
neu += 1
if neu:
import subprocess
print('%d Kapitel-Links angelegt -> lama-mod an' % neu)
print('%d chapter links created -> lama-mod an' % neu)
subprocess.run([os.path.expanduser('~/.local/bin/lama-mod'), 'an'], check=True,
stdout=subprocess.DEVNULL)