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:
1 parent
86fe26d786
commit
526f6072f6
36 files changed
+1553
-1544
No files matched your search
+109
-108
@@ -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)
|
||||
|
||||
|
||||
Reference in new issue
Block a user