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

+222 -222
View File
@@ -1,31 +1,31 @@
# Levelbau mit `tools/levelbuild.py`
# Level building with `tools/levelbuild.py`
Stand: 2026-11-06 (Ausgang + Parkour überarbeitet). Baut komplette Level-WADs aus einer Level-Beschreibung (Python-Skript).
Basis ist immer ein Original-Level (Standard: Dorf `t1l1m001.wad`): dessen Strat-Code/Objektmodelle
(STPC), Texturen (TEXT), Sound usw. werden übernommen; **Geometrie (TRAK), Kachelraster,
Vertexfarben, Objekte und Wegpunkte (MAP) werden komplett neu erzeugt.**
Ergebnis im Spiel geprüft: `out/testwelt.wad` (= installierte Testwelt, siehe unten).
As of: 2026-11-06 (exit + parkour reworked). Builds complete level WADs from a level description (Python script).
The base is always an original level (default: village `t1l1m001.wad`): its strat code/object models
(STPC), textures (TEXT), sound etc. are taken over; **geometry (TRAK), tile grid,
vertex colours, objects and waypoints (MAP) are generated completely from scratch.**
Result checked in game: `out/testwelt.wad` (= installed test world, see below).
## Aufruf
## Invocation
```
python3 tools/levelbuild.py levels/testwelt.py out/testwelt.wad # bauen
python3 tools/levelbuild.py levels/testwelt.py out/testwelt.wad --install # + als TestLevel installieren
python3 tools/levelbuild.py levels/testwelt.py out/t.wad --set start=ziel # Variablen ans Skript
python3 tools/levelbuild.py --types # platzierbare Strat-Typen des Basislevels
python3 tools/levelbuild.py --textures tex.png # Textur-Kontaktabzug + Namensliste
python3 tools/levelbuild.py levels/testwelt.py out/testwelt.wad # build
python3 tools/levelbuild.py levels/testwelt.py out/testwelt.wad --install # + install as TestLevel
python3 tools/levelbuild.py levels/testwelt.py out/t.wad --set start=ziel # pass variables to the script
python3 tools/levelbuild.py --types # placeable strat types of the base level
python3 tools/levelbuild.py --textures tex.png # texture contact sheet + name list
python3 tools/texsheet.py WAD out.png [--all] [--only 1,2,3] [--size 96]
```
`--install` kopiert nach `~/.wine-lama/mods/testlevel/wads/T1L9M001.wad` (legt einmalig
`.orig` an) und legt die Kapitel-Symlinks 2..49 an (s. „Ausgang“). Test im Spiel: `tools/lamatest.py run out/x.wad ordner` (s. testharness.md).
`--install` copies to `~/.wine-lama/mods/testlevel/wads/T1L9M001.wad` (creates
`.orig` once) and creates the chapter symlinks 2..49 (see "Exit"). Test in game: `tools/lamatest.py run out/x.wad outdir` (see testharness.md).
## Level-Skript
## Level script
```python
LEVEL_ARGS = dict(name='Testwelt', seed=7) # NAME-Chunk, Zufallssaat (Texturdrehung)
LEVEL_ARGS = dict(name='Testwelt', seed=7) # NAME chunk, random seed (texture rotation)
def build(L): # L = levelbuild.Level
L.sea(0, 0, 88, 72, y=-2.0, kill=(6, 4, 80, 66)) # Meer + Todesebene
L.sea(0, 0, 88, 72, y=-2.0, kill=(6, 4, 80, 66)) # sea + death plane
L.box(14, 12, 26, 24, -0.4, 0, top='stein', side='stein')
L.stairs(26, 18, 29, 22, 0, 1.5, 3, '+x')
L.player(20, 14.5, rot=0)
@@ -34,240 +34,240 @@ def build(L): # L = levelbuild.Level
L.strat('Bunny', 66, 34)
L.coins([(27, 20), (28, 20)])
def report(L): # optional, nach dem Bauen
def report(L): # optional, after building
...
```
`L.opts` enthält die `--set`-Werte (Strings).
`L.opts` contains the `--set` values (strings).
### Koordinaten
* Spielkoordinaten wie bei Strats: **x nach Osten, y nach oben, z = Rasterzeile (positiv,
"Süden")**, 1 Einheit = 1 Rasterzelle. Raster des Dorfes: 96×96 → alles muss in x,z ∈ [0,96) liegen.
* Winkel in **Grad**, 0° = Blick nach +z, 90° = Blick nach +x (im Spiel geprüft).
* Intern wird in den Exportraum (z gespiegelt) umgerechnet; Umlaufsinn wird automatisch
korrigiert (`_orient`), man gibt nur die gewünschte Außennormale an.
### Coordinates
* Game coordinates as for strats: **x towards east, y up, z = grid row (positive,
"south")**, 1 unit = 1 grid cell. Village grid: 96×96 → everything must lie within x,z ∈ [0,96).
* Angles in **degrees**, 0° = facing +z, 90° = facing +x (checked in game).
* Internally converted into export space (z mirrored); winding order is corrected
automatically (`_orient`), you only specify the desired outward normal.
### Geometrie-Bausteine
| Methode | Beschreibung |
### Geometry building blocks
| Method | Description |
|---|---|
| `floor(x0,z0,x1,z1,y, tex, rot='random', coll=True)` | waagerechte Fläche (in 1×1-Kacheln unterteilt) |
| `box(x0,z0,x1,z1,y0,y1, top, side, side_top=None, bottom=None, sides='nesw', side_coll=None)` | Quader; `side_top` = Textur der obersten Wandreihe (z. B. `'graskante'`), `side_coll=False` = Wände ohne Kollision (niedrige Sockel begehbar) |
| `platform(x,z,w,d,y, top, side, thick=0.5, side_top='graskante', bottom='erde')` | schwebende Plattform, Oberkante y |
| `pillar(x,z,y0,y1,size, side, top)` | Säule um Mittelpunkt |
| `ramp(x0,z0,x1,z1, ya, yb, '+x', top, side, base)` | Rampe, steigt in Richtung `+x/-x/+z/-z` |
| `stairs(x0,z0,x1,z1, y0, y1, n, '+x', top, side, base, walkable=True)` | Treppe; `walkable`: unsichtbare Schräge als Kollision |
| `fence(pts, y, height)` | Zaun (Pfosten + Latte) entlang Punktliste |
| `tree(x,z,y, height, crown)` | Klotz-Baum |
| `sea(x0,z0,x1,z1, y, tex='meer', kill=(...))` | Wasserfläche ohne Kollision + Todesebene |
| `quad([BL,BR,TR,TL], tex, coll, rot)` | einzelnes Viereck (Ecken von vorne gesehen) |
| `collider(pts, normal)` | unsichtbare Kollisionsfläche |
| `obj(path, offset, scale, tex)` | OBJ-Datei (Spielkoordinaten), `usemtl tex_N` oder Texturname |
| `height_at(x,z)` | Höhe der obersten begehbaren Fläche |
| `floor(x0,z0,x1,z1,y, tex, rot='random', coll=True)` | horizontal surface (subdivided into 1×1 tiles) |
| `box(x0,z0,x1,z1,y0,y1, top, side, side_top=None, bottom=None, sides='nesw', side_coll=None)` | cuboid; `side_top` = texture of the topmost wall row (e.g. `'graskante'`), `side_coll=False` = walls without collision (low plinths walkable) |
| `platform(x,z,w,d,y, top, side, thick=0.5, side_top='graskante', bottom='erde')` | floating platform, top edge y |
| `pillar(x,z,y0,y1,size, side, top)` | pillar around a centre point |
| `ramp(x0,z0,x1,z1, ya, yb, '+x', top, side, base)` | ramp, rises in direction `+x/-x/+z/-z` |
| `stairs(x0,z0,x1,z1, y0, y1, n, '+x', top, side, base, walkable=True)` | stairs; `walkable`: invisible slope as collision |
| `fence(pts, y, height)` | fence (posts + rail) along a point list |
| `tree(x,z,y, height, crown)` | blocky tree |
| `sea(x0,z0,x1,z1, y, tex='meer', kill=(...))` | water surface without collision + death plane |
| `quad([BL,BR,TR,TL], tex, coll, rot)` | single quad (corners seen from the front) |
| `collider(pts, normal)` | invisible collision surface |
| `obj(path, offset, scale, tex)` | OBJ file (game coordinates), `usemtl tex_N` or texture name |
| `height_at(x,z)` | height of the topmost walkable surface |
Texturen: Name aus `TEX` (Dorf: `gras, gras2, gras_dunkel, blumen, sand, erde, graskante,
Textures: name from `TEX` (village: `gras, gras2, gras_dunkel, blumen, sand, erde, graskante,
stein, stein_kante, stein_moos, platte, holz_dunkel, holz_maser, holzbalken, rinde, kiste,
inka, muster_lila, …`), direkter Index (int) oder Flachfarbe (`'meer'`, `'rot'`, … oder
`(r,g,b)`; wird als Flachfarb-Eintrag (Flags 0x105, Seite −1) an die Texturtabelle angehängt).
Kontaktabzug mit Indizes: `notes/img/texturen_dorf.png`.
**Jedes Dreieck zeigt eine ganze Texturkachel** (die Engine hat keine freien UVs) – daher wird
alles in 1×1-Stücke zerlegt; schmale/flache Wände zeigen die Textur gestaucht.
inka, muster_lila, …`), a direct index (int) or a flat colour (`'meer'`, `'rot'`, … or
`(r,g,b)`; appended to the texture table as a flat-colour entry (flags 0x105, page −1)).
Contact sheet with indices: `notes/img/texturen_dorf.png`.
**Every triangle shows a whole texture tile** (the engine has no free UVs) – so
everything is split into 1×1 pieces; narrow/flat walls show the texture squashed.
### Beleuchtung (Vertexfarben)
### Lighting (vertex colours)
`L.sun = dict(azimuth=215, elevation=50, color=(1,.93,.8), strength=.62, sky=(.5,.55,.66),
ground=(.3,.28,.24), ao=.55, ao_radius=1.3, shadow=.35)` – je Vertex:
Himmel/Boden-Halbkugellicht + Sonne (Lambert) mit **Schatten** (Strahl zur Sonne gegen alle
Quader) + **Umgebungsverdeckung** (24 Strahlen, Reichweite `ao_radius`). Quader, Säulen,
Treppen, Plattformen melden sich als Schattenwerfer an (`solids`). 1.0 = Texturfarbe
(Vertexfarbe 0x80), max. 2.0. `tint=(r,g,b)` je Baustein multipliziert.
ground=(.3,.28,.24), ao=.55, ao_radius=1.3, shadow=.35)` – per vertex:
sky/ground hemisphere light + sun (Lambert) with **shadows** (ray towards the sun against all
cuboids) + **ambient occlusion** (24 rays, range `ao_radius`). Cuboids, pillars,
stairs, platforms register as shadow casters (`solids`). 1.0 = texture colour
(vertex colour 0x80), max. 2.0. `tint=(r,g,b)` multiplies per building block.
### Objekte (Strats)
### Objects (strats)
`L.strat(name, x, z, y=None, rot=None, params=None, wp='relative', variant=0, hidden=False)`
* `y=None`: auf die oberste begehbare Fläche bei (x,z) stellen.
* Typfelder (nvars, Slots, Flags, Zusatzspeicher) und Parameter werden von der **ersten
Original-Instanz** des Typs kopiert (`params=[…]` überschreibt; Anzahl muss passen).
* `variant`: Typen mit mehreren Code-Einstiegen (z. B. SmashPot: 2 Spawn-Stubs).
* **Wegpunkte** (`wp`): `'relative'` = Wegpunktkette des Originals mitverschieben (Versatz zum
Objekt bleibt, y wird auf den Boden gesetzt, wenn dort einer ist), `'here'` = alle Punkte auf
das Objekt, `'none'` = keine (−1), oder eigene Liste `[(x,z[,y[,grad]]), …]` (Zusatzfelder
der Original-Wegpunkte werden übernommen). Ketten werden neu nummeriert (keine Verweise ins
alte Level).
* `player(x,z,rot)` (Strat `Llama`), `checkpoint(x,z)` (`CheckPointPost`), `exit(x,z)`
* `y=None`: place on the topmost walkable surface at (x,z).
* Type fields (nvars, slots, flags, extra memory) and parameters are copied from the **first
original instance** of the type (`params=[…]` overrides; count must match).
* `variant`: types with several code entry points (e.g. SmashPot: 2 spawn stubs).
* **Waypoints** (`wp`): `'relative'` = move the original's waypoint chain along (offset to the
object is kept, y is set to the floor if there is one), `'here'` = all points on
the object, `'none'` = none (−1), or your own list `[(x,z[,y[,deg]]), …]` (extra fields
of the original waypoints are taken over). Chains are renumbered (no references into the
old level).
* `player(x,z,rot)` (strat `Llama`), `checkpoint(x,z)` (`CheckPointPost`), `exit(x,z)`
(`LevelDoor`), `coins([(x,z),…])`.
* Typen, die im Dorf-Code stehen, aber dort nicht platziert sind, über `Strats.BORROWED` mit Typfeldern
aus einem anderen Level: derzeit `RubyIdol` (aus t1l1m002).
* `poolExtra` wird auf max(Original, Anzahl Strats + 80) gesetzt; der Parameterpool des
Originals bleibt erhalten, neue Parameter werden angehängt.
* Types that are in the village code but not placed there, via `Strats.BORROWED` with type fields
from another level: currently `RubyIdol` (from t1l1m002).
* `poolExtra` is set to max(original, number of strats + 80); the original's parameter pool
is kept, new parameters are appended.
### Was `build()` erzeugt
1. Alle Dreiecke werden nach (Rasterzelle, Höhenband 10 Einheiten ab y = −3) auf Kacheln
verteilt; Kachelursprung = Zellmitte/Bandmitte (Kollisions-d muss in ±8 liegen).
2. **Kollision**: jedes Viereck mit `coll=True` wird ein Kollisionspolygon; Böden nur in der
eigenen Zelle, Wände/Schrägen zusätzlich in Nachbarzellen bis 0,3 Abstand (die Engine prüft
nur die Kacheln der Zelle, in der der Spieler steht). Unbenutzte Hilfsvertices sorgen dafür,
dass die Kachel-Box die Kollisionspolygone umfasst.
3. Identische Kachelmodelle werden zusammengefasst (Vertexfarben stehen in MAP je Kachel).
4. **Todesebene**: Original-Modell 0 des Dorfes (unsichtbar, Material 0x1e) in jeder Zelle
des `kill`-Bereichs, die nicht vollständig von Boden bedeckt ist.
5. MAP: Zonenraster = 0, Zone 0 sieht alles (Maske 0xffffffff), keine Kachel-Wechsel (Alt-Block
leer), Kachelblöcke A/B/Modelle/Vertexfarben konsistent, Strats + Wegpunkte neu, Licht-/
Umgebungszonen (90-Byte-Block) und Rest unverändert.
6. TEXT: angehängte Flachfarben. NAME: `LEVEL_ARGS['name']`.
7. Gegenprobe: MAP wird nach dem Schreiben wieder vollständig geparst.
### What `build()` generates
1. All triangles are distributed onto tiles by (grid cell, height band of 10 units from y = −3);
tile origin = cell centre/band centre (collision d must be within ±8).
2. **Collision**: every quad with `coll=True` becomes a collision polygon; floors only in their
own cell, walls/slopes additionally in neighbouring cells up to 0.3 distance (the engine only checks
the tiles of the cell the player stands in). Unused helper vertices make sure
the tile box encloses the collision polygons.
3. Identical tile models are merged (vertex colours are stored per tile in MAP).
4. **Death plane**: original model 0 of the village (invisible, material 0x1e) in every cell
of the `kill` area that is not completely covered by floor.
5. MAP: zone grid = 0, zone 0 sees everything (mask 0xffffffff), no tile swaps (alt block
empty), tile blocks A/B/models/vertex colours consistent, strats + waypoints new, light/
environment zones (90-byte block) and rest unchanged.
6. TEXT: appended flat colours. NAME: `LEVEL_ARGS['name']`.
7. Cross-check: MAP is fully parsed again after writing.
## Erkenntnisse aus den Spieltests (lamatest, Xvfb)
* **Maßstab**: Lama ≈ 0,6 Einheiten hoch, läuft ≈ 1,1 Einheiten/s (Münz-Messbahn, s. u.; die
früher notierten 0,85 waren zu niedrig), braucht ~1 s zum Beschleunigen. Sprung (X) überwindet
0,35 hohe Kanten, 0,5 nicht. **Mit Anlauf fliegt es ~1,3 weit** (horizontal gleich schnell wie
beim Laufen), auf +0,2 landet es nach ~1,05; aus dem Stand deutlich kürzer (0,5-Lücke nicht
geschafft). → Parkour: Lücken 0,3 mit +0,2, Plattformen davor/dahinter ≥ 1,5 lang (nach der
Landung muss Platz für den nächsten Absprung sein), größere Höhen über Treppen/Rampen.
Messlevel: `levels/sprunglabor.py` (`--set spur=a..g|m`; `m` = Münz-Messbahn: Münze alle 0,25 →
Münzzähler × 0,25 = gelaufene Strecke; `f` = Weitsprung über Wasser).
* **Keine Stufen-Logik**: schon eine 0,12 hohe Kante *mit Wandkollision* blockiert das Lama.
Ohne Wandkollision (`side_coll=False`) läuft es auf niedrige Sockel hinauf; Treppen mit
unsichtbarer Schräge (`stairs(walkable=True)`) und Rampen sind begehbar (geprüft).
* **Steuerung ist kamerarelativ** (Hoch = Kamerablickrichtung). Die Startkamera steht hinter
dem Lama; steht direkt dahinter ein Hindernis (z. B. Pfosten), weicht sie seitlich aus →
Startpunkte hinten freihalten.
* Rot 0° = Blick +z, 90° = Blick +x.
* **Todesebene funktioniert**: Sturz ins Meer → Lebensenergie weg, Respawn am letzten
Checkpoint (bzw. Start).
* Spieler-Wegpunktkette (14 Punkte beim Dorf-Lama, Bedeutung unklar): `player()` legt den
ersten 8 Einheiten hinter das Lama, den Rest auf das Lama, Zusatzfelder vom Original.
* **LevelDoor** (Ausgang) – gelöst, siehe Abschnitt „Ausgang (LevelDoor)“ unten.
* Größen, die laufen: TRAK 0,6–1,0 MB, bis 1100 Modelle, 5200 Kacheln, WAD bis 6,9 MB
(Originale: TRAK ≤ 1 MB, ≤ 5800 Kacheln, WAD ≤ 6,85 MB). Testwelt: 628 Modelle,
2474 Kacheln, TRAK 646 KB, WAD 6,3 MB.
## Findings from the game tests (lamatest, Xvfb)
* **Scale**: llama ≈ 0.6 units tall, walks ≈ 1.1 units/s (coin measuring track, see below; the
0.85 noted earlier was too low), needs ~1 s to accelerate. A jump (X) clears
0.35 high edges, 0.5 not. **With a run-up it flies ~1.3 far** (horizontally as fast as
when walking), onto +0.2 it lands after ~1.05; from standing much shorter (0.5 gap not
cleared). → Parkour: gaps 0.3 with +0.2, platforms before/after ≥ 1.5 long (after
landing there must be room for the next take-off), larger heights via stairs/ramps.
Measuring level: `levels/sprunglabor.py` (`--set spur=a..g|m`; `m` = coin measuring track: a coin every 0.25 →
coin counter × 0.25 = distance walked; `f` = long jump over water).
* **No step logic**: even a 0.12 high edge *with wall collision* blocks the llama.
Without wall collision (`side_coll=False`) it walks up onto low plinths; stairs with
an invisible slope (`stairs(walkable=True)`) and ramps are walkable (checked).
* **Controls are camera-relative** (Up = camera view direction). The start camera is behind
the llama; if there is an obstacle right behind it (e.g. a post), it moves aside →
keep the area behind start points clear.
* Rot 0° = facing +z, 90° = facing +x.
* **Death plane works**: falling into the sea → health gone, respawn at the last
checkpoint (or start).
* Player waypoint chain (14 points for the village llama, meaning unclear): `player()` puts the
first one 8 units behind the llama, the rest on the llama, extra fields from the original.
* **LevelDoor** (exit) – solved, see section "Exit (LevelDoor)" below.
* Sizes that work: TRAK 0.6–1.0 MB, up to 1100 models, 5200 tiles, WAD up to 6.9 MB
(originals: TRAK ≤ 1 MB, ≤ 5800 tiles, WAD ≤ 6.85 MB). Test world: 628 models,
2474 tiles, TRAK 646 KB, WAD 6.3 MB.
## Ausgang (LevelDoor) – wie er funktioniert
## Exit (LevelDoor) – how it works
Strat-Bytecode (Dorf, STPC 0x430332, 3 Parameter, unsichtbar, kein Modell), dekodiert über die
VM `FUN_0054d180` (Opcode < 0x45: Tabelle 0x5796bc mit 16-Bit-Operand, sonst 0x578ff4+op·4):
Strat bytecode (village, STPC 0x430332, 3 parameters, invisible, no model), decoded via the
VM `FUN_0054d180` (opcode < 0x45: table 0x5796bc with 16-bit operand, otherwise 0x578ff4+op·4):
```
g65,g66,g67 = Param 0,1,2 ; Stamm, Welt, Kapitel des nächsten Levels (20.12)
loop: if g21 == 3 ; Spielzustand "läuft"
and Spieler.Boden.Material == 16 ; Eigenschaft 0x11 = *(strat+0xc0)+4 = Material-Byte des
; Kollisionspolygons unter dem Spieler (FUN_00403020)
and floor(Spieler.x) == floor(Tür.x) and floor(Spieler.z) == floor(Tür.z) ; gleiche Rasterzelle
-> op 0x192 (FUN_0054d620): Levelwechsel(Param), g1 += 1, Ende
g65,g66,g67 = param 0,1,2 ; tribe, world, chapter of the next level (20.12)
loop: if g21 == 3 ; game state "running"
and player.floor.material == 16 ; property 0x11 = *(strat+0xc0)+4 = material byte of the
; collision polygon below the player (FUN_00403020)
and floor(player.x) == floor(door.x) and floor(player.z) == floor(door.z) ; same grid cell
-> op 0x192 (FUN_0054d620): level change(param), g1 += 1, end
yield
```
* Die Tür ist also nur ein Auslöser; im Dorf liegt im Türdurchgang ein Bodenpolygon mit
**Material 16** (8 Stück im Dorf, auch in allen anderen Levels). Unsere Testwelt hatte überall
Material 0 → nichts passierte. **Fix:** Mosaik im Tor mit `mat=16` (`box(..., mat=TUER_MAT)`),
Tür in dieselbe Rasterzelle, Spielerweg führt mitten hindurch.
* **Nur Kacheln der Türzelle zählen** (Zellliste, FUN_004036d0), die Höhe nicht (t1l3m002–4: Boden
3 Einheiten unter der Tür). **Dorf 1 & Co.:** In der Türzelle liegt zunächst *kein* Material 16 –
es steckt im **Wechselmodell** der Kachel (MAP-Kachel-Wechsel, WFPC 0x10000): die RubyIdolDoor in
derselben Zelle tauscht beim Öffnen (Op 0x11a/0x11b) Kachel 420 von Modell 48 auf 49, und erst
Modell 49 hat das Ausgangspolster (42,25–42,75 / 49,23–49,50, y 3,25). Im Spiel geprüft
(2026-11-07): Modell 49 eingesetzt + Tür entfernt, Lama bei 42,5/3,5/49,37 → sofort Bilanz
„Dorf Kapitel 1“; Original (Tür zu) → nichts. Betrifft 8 der 31 Original-Türen (Ruby-,
PressurePad-, Lab-Türen); das Polster bei 43,5/48,45 (Zelle 43,48) ist dagegen ein Hauseingang
(Zonen-Teleport). t1l3m003: 2 der 4 Türen (Zellen 71,48/71,49) haben gar kein Polster.
* Zweiter Weg (Strat-Code bei 0x4252aa, Spielerlogik [wahrsch.]): Boden-Material 16 und Zelle =
g104/g105 (setzt die LevelDoor beim Start, Llama setzt sie auf −1) → Levelwechsel(g101..g103).
* Parameter in den Originalen: (1, Welt, Kapitel+1) = nächstes Kapitel; im letzten Kapitel einer
Welt **(7,7,7)** = „Welt geschafft“ → zurück zur Auswahlkarte, nächste Welt freischalten.
* `FUN_0054d620`: merkt aktuelles Level (0x585008..14), setzt nächstes (0x584eb4/b8/bc), zeigt die
Bilanz (Münzen x/55, Geheimnisse) mit „EINGABETASTE = speichern / ESC = weiter“. Fortschritt
0x585018/19 (max. Welt/Kapitel, im KSV +0x42/+0x43): ist man im höchsten freien Kapitel, wird
das nächste Kapitel freigeschaltet; bei (7,7,7) die nächste Welt.
* **(7,7,7) in Welt 9 stürzt ab**: schaltet „Welt 10“ frei → nach dem Speichern steht der
Kartencursor auf Welt 10, Weltnamen-Zeiger 0x57bd40[9] = NULL → Page Fault bei 0x41B5CC.
* **Lösung Testwelt: Parameter (1,9,1)** – nach der Bilanz wird Kapitel 1 neu geladen (kein
Absturz, getestet mit Speichern). Nebenwirkung: Kapitel 2 wird freigeschaltet (und von
Kapitel n aus Kapitel n+1). Fehlt dessen WAD, stürzt die Auswahl ab („Couldnt load wad“).
Deshalb legt `levelbuild.py --install` (Funktion `kapitel_links()`) in der Mod
**Symlinks T1L9M002..049.wad → T1L9M001.wad und T192..T1949.COR → T191.COR** an (Welt 9 hat
49 Kapitel-Plätze, Tabelle 0x572420) und ruft `lama-mod an` auf. `lama-mod` übernimmt dafür jetzt
auch Symlinks aus Mod-Ordnern (`find … -type f -o -type l`). Geprüft: Kapitel 2 auf der Karte
wählbar, lädt die Testwelt.
* Die Auswahlkarte zeigt für Welt 9 keinen Hintergrund (nur Text) und 772 als Münzziel
(Münztabelle 0x5722f4 hat nur Welten 1–8) – kosmetisch.
* So the door is only a trigger; in the village there is a floor polygon with
**material 16** in the doorway (8 of them in the village, also in all other levels). Our test world had
material 0 everywhere → nothing happened. **Fix:** mosaic in the gate with `mat=16` (`box(..., mat=TUER_MAT)`),
door in the same grid cell, the player's path leads right through it.
* **Only tiles of the door cell count** (cell list, FUN_004036d0), not the height (t1l3m002–4: floor
3 units below the door). **Village 1 & co.:** the door cell initially contains *no* material 16 –
it is in the **swap model** of the tile (MAP tile swap, WFPC 0x10000): the RubyIdolDoor in
the same cell swaps tile 420 from model 48 to 49 when opening (op 0x11a/0x11b), and only
model 49 has the exit pad (42.25–42.75 / 49.23–49.50, y 3.25). Checked in game
(2026-11-07): model 49 inserted + door removed, llama at 42.5/3.5/49.37 → immediately the summary
"Dorf Kapitel 1"; original (door closed) → nothing. Affects 8 of the 31 original doors (Ruby,
PressurePad, Lab doors); the pad at 43.5/48.45 (cell 43,48), on the other hand, is a house entrance
(zone teleport). t1l3m003: 2 of the 4 doors (cells 71,48/71,49) have no pad at all.
* Second path (strat code at 0x4252aa, player logic [likely]): floor material 16 and cell =
g104/g105 (set by the LevelDoor at start, Llama sets them to −1) → level change(g101..g103).
* Parameters in the originals: (1, world, chapter+1) = next chapter; in the last chapter of a
world **(7,7,7)** = "world completed" → back to the selection map, unlock the next world.
* `FUN_0054d620`: remembers the current level (0x585008..14), sets the next (0x584eb4/b8/bc), shows the
summary (coins x/55, secrets) with "EINGABETASTE = speichern / ESC = weiter" (Enter = save / Esc = continue). Progress
0x585018/19 (max. world/chapter, in KSV +0x42/+0x43): if you are in the highest unlocked chapter,
the next chapter is unlocked; with (7,7,7) the next world.
* **(7,7,7) in world 9 crashes**: unlocks "world 10" → after saving, the
map cursor is on world 10, world name pointer 0x57bd40[9] = NULL → page fault at 0x41B5CC.
* **Solution for the test world: parameters (1,9,1)** – after the summary chapter 1 is reloaded (no
crash, tested with saving). Side effect: chapter 2 is unlocked (and from
chapter n, chapter n+1). If its WAD is missing, the selection crashes ("Couldnt load wad").
That is why `levelbuild.py --install` (function `kapitel_links()`) creates in the mod
**symlinks T1L9M002..049.wad → T1L9M001.wad and T192..T1949.COR → T191.COR** (world 9 has
49 chapter slots, table 0x572420) and calls `lama-mod an`. For this, `lama-mod` now also takes
symlinks from mod folders (`find … -type f -o -type l`). Checked: chapter 2 on the map is
selectable and loads the test world.
* The selection map shows no background for world 9 (only text) and 772 as the coin target
(coin table 0x5722f4 only has worlds 1–8) – cosmetic.
## Die Testwelt (`levels/testwelt.py` → `out/testwelt.wad`, installiert als T1L9M001.wad)
**2026-11-06 kompakt umgebaut** („alles näher zusammen“): L-förmige Insel, Hauptteil 32,5×23,5
(x 11..43,5, z 9,5..33) + Streifen unter dem Parkour-Ende (x 43,5..49, z 9,5..20,5), vorher
42×34 (x 10..52, z 8..42). Galerie, Gehege und Parkour grenzen direkt aneinander, keine leeren
Wiesen mehr. NAME-Chunk "Testwelt" (wird im Spiel nirgends angezeigt).
Vorschaubilder (Handy, 1280 px): `notes/img/vorschau/` (01–03 Blender-Renderings mit
Beschriftung, 04–11 Spielfotos, 12 Montage). Ältere Bilder `notes/img/testwelt_*.jpg` zeigen
noch das alte, weitläufige Layout.
## The test world (`levels/testwelt.py` → `out/testwelt.wad`, installed as T1L9M001.wad)
**Rebuilt compactly on 2026-11-06** ("everything closer together"): L-shaped island, main part 32.5×23.5
(x 11..43.5, z 9.5..33) + strip below the parkour end (x 43.5..49, z 9.5..20.5), previously
42×34 (x 10..52, z 8..42). Gallery, enclosure and parkour adjoin each other directly, no empty
meadows any more. NAME chunk "Testwelt" (not shown anywhere in the game).
Preview images (phone, 1280 px): `notes/img/vorschau/` (01–03 Blender renderings with
labels, 04–11 game shots, 12 montage). Older images `notes/img/testwelt_*.jpg` still show
the old, sprawling layout.
| Bereich | alt | neu |
| Area | old | new |
|---|---|---|
| Startplatz | x 14..22, z 11..19 (8×8) | x 15..21, z 12..19 (6×7) |
| Galerie | z 22..38, Spalten 2,5, Reihen z 25/28,5/32/35,5 | x 14,5..32,5, z 20,5..32, Spalten 1,9, Reihen z 22,5/25,2/27,9/30,6 (Abstand 2,7) |
| Gehege | x 39..49, z 24..35 | x 33..42, z 20,8..30 (direkt östlich der Galerie) |
| Parkour | x 22..53,6 | x 21..52,6 (nur um −1 in x verschoben, Maße gleich) |
| Ausgang | LevelDoor (51,5/15,5) | LevelDoor (50,5/15,5), Zelle 50/15, Mosaik mit Material 16 |
| Start square | x 14..22, z 11..19 (8×8) | x 15..21, z 12..19 (6×7) |
| Gallery | z 22..38, columns 2.5, rows z 25/28.5/32/35.5 | x 14.5..32.5, z 20.5..32, columns 1.9, rows z 22.5/25.2/27.9/30.6 (spacing 2.7) |
| Enclosure | x 39..49, z 24..35 | x 33..42, z 20.8..30 (directly east of the gallery) |
| Parkour | x 22..53.6 | x 21..52.6 (only shifted by −1 in x, same dimensions) |
| Exit | LevelDoor (51.5/15.5) | LevelDoor (50.5/15.5), cell 50/15, mosaic with material 16 |
* **Startplatz** (x 15..21, z 12..19): Steinplatz mit Inka-Mosaik, 4 Inka-Säulen, Blumenbeete,
Bäume, 2 Münzen, Checkpoint (16,1/17,4). Lama startet bei (18, 14,6) mit Blick nach Süden
(Galerie direkt dahinter, Parkour-Treppe links im Bild).
* **Galerie** (z 20,5..32): 4 Reihen, je Objekt ein flacher Sockel (0,9 breit, 0,08 hoch,
begehbar), Lücke 1,0 zwischen Sockeln, Sandweg (0,6) vor jeder Reihe, farbiger Pfosten am
Reihenanfang (gelb/grau/blau/rot), Hauptweg Nord–Süd bei x 15,6..16,3:
1. Sammeln: Coin, ChargeBooster, RubyIdol, HealthFull, HealthGenerator, ChacaChargePotion, Secret
2. Kisten/Töpfe: SmashPot (2 Varianten), Jar, ObjectDontDropInBox, WaterReboundPoints, CheatTeleport
3. Figuren: TipoVillage, ChacaVillage, PachaGivingInfo, KidOnLlama, Wompy
4. Mechanik: PressurePadDoor, RubyIdolDoor, SwitchPlatformPadVillage, AppearingPlatformsVillage,
* **Start square** (x 15..21, z 12..19): stone square with Inca mosaic, 4 Inca pillars, flower beds,
trees, 2 coins, checkpoint (16.1/17.4). The llama starts at (18, 14.6) facing south
(gallery directly behind it, parkour stairs on the left of the picture).
* **Gallery** (z 20.5..32): 4 rows, each object on a flat plinth (0.9 wide, 0.08 high,
walkable), gap 1.0 between plinths, sand path (0.6) in front of each row, coloured post at the
start of the row (yellow/grey/blue/red), main path north–south at x 15.6..16.3:
1. Collectables: Coin, ChargeBooster, RubyIdol, HealthFull, HealthGenerator, ChacaChargePotion, Secret
2. Crates/pots: SmashPot (2 variants), Jar, ObjectDontDropInBox, WaterReboundPoints, CheatTeleport
3. Characters: TipoVillage, ChacaVillage, PachaGivingInfo, KidOnLlama, Wompy
4. Mechanics: PressurePadDoor, RubyIdolDoor, SwitchPlatformPadVillage, AppearingPlatformsVillage,
RisingColumns, SinkingStatue, FallingTree, CheckPointPost
* **Gehege** (x 33..42, z 20,8..30, Zaun, Eingang Westseite z 20,8..24): Bunny ×2, Ladybird,
FlockOfBirds, VillagerHerder; Aussichtspodest (0,6) mit Treppe an der Westseite zwischen
Galerie-Reihe 2 und 3.
* **Parkour** (beginnt an der Ostkante des Platzes, **alles auf der Linie z = 15,5 nach Osten**,
damit man ohne Kameradrehen durchkommt – Drehen per A/S oder Links/Rechts war im Skript nicht
reproduzierbar). Geometrie im Skript als „alte x + PX“ (PX = −1) geschrieben:
Treppe 0→0,8 · **Anschlag-Stufe** +0,3 (ohne Lücke: Lama läuft dagegen und steht still) ·
2 Lücken 0,3 mit je +0,2 auf 1,5 lange Holzklötze · Balken 0,5 breit · Mittelplattform (1,5) mit
Checkpoint · Sandrampe 1,5→2,4 · **Anschlag-Stufe** +0,3 · 3 Trittsteine (1,5 lang, Lücken 0,3,
je +0,2) · schmaler Balken 0,45 · Treppe 3,3→4,0 · Ziel-Plattform (4,0; x 47,6..52,6, ragt über
die Küste) mit Säulen, Brustwehr und Tor; darunter Mosaik mit Material 16, LevelDoor bei
(50,5 / 15,5). 18 Münzen entlang. Unter dem Parkour liegt Wiese (Herunterfallen ist harmlos).
* **Skript-Durchlauf** (Start → Ziel → Ausgang → Bilanz), alte Fassung 4/4, kompakte Fassung 2/2
Läufe erfolgreich (Bilanz 18/55 Münzen); die .seq ist zeitbasiert/relativ und blieb unverändert:
* **Enclosure** (x 33..42, z 20.8..30, fence, entrance on the west side z 20.8..24): Bunny ×2, Ladybird,
FlockOfBirds, VillagerHerder; viewing pedestal (0.6) with stairs on the west side between
gallery rows 2 and 3.
* **Parkour** (starts at the east edge of the square, **everything on the line z = 15.5 going east**,
so it can be done without rotating the camera – rotating via A/S or Left/Right was not
reproducible in scripts). Geometry is written in the script as "old x + PX" (PX = −1):
stairs 0→0.8 · **stop step** +0.3 (no gap: the llama runs into it and stands still) ·
2 gaps of 0.3 with +0.2 each onto 1.5 long wooden blocks · beam 0.5 wide · middle platform (1.5) with
checkpoint · sand ramp 1.5→2.4 · **stop step** +0.3 · 3 stepping stones (1.5 long, gaps 0.3,
+0.2 each) · narrow beam 0.45 · stairs 3.3→4.0 · goal platform (4.0; x 47.6..52.6, juts out over
the coast) with pillars, parapet and gate; below it a mosaic with material 16, LevelDoor at
(50.5 / 15.5). 18 coins along the way. Below the parkour there is meadow (falling down is harmless).
* **Scripted run** (start → goal → exit → summary), old version 4/4, compact version 2/2
runs successful (summary 18/55 coins); the .seq is time-based/relative and stayed unchanged:
```
python3 tools/levelbuild.py levels/testwelt.py out/testwelt_parkour.wad --set start=parkour
lamatest.py run out/testwelt_parkour.wad AUSGABE --seq "$(cat levels/testwelt_parkour.seq)"
lamatest.py run out/testwelt_parkour.wad OUTDIR --seq "$(cat levels/testwelt_parkour.seq)"
```
(`run:T:x@t,…` hält Up T s und drückt X zu festen Zeiten; die Anschlag-Stufen setzen den
Zeitfehler zurück. Fotos (`shot@`) blockieren ~0,4 s → nie kurz vor einem Sprung.)
* Im Spiel geprüft: Laden ohne Absturz (viele Läufe), alle 31 Galerie-Objekte erscheinen
(Münze/Secret/… reagieren: Münzzähler, "GEHEIMNIS!"), Lama läuft Galerie/Gehege ab,
Treppe hoch, Sprung auf Plattform B und auf Trittstein 1, Rampe hoch, Treppe zur
Ziel-Plattform, Meer = Respawn, kompletter Parkour per Skript, Ausgang → Bilanz → Kapitel 1 neu.
Bilder: aktuell `notes/img/vorschau/` (kompaktes Layout); `notes/img/testwelt_*.jpg` =
altes weitläufiges Layout (1–6 + Montage sogar noch mit altem Parkour).
* Startpunkt für Tests umschaltbar: `--set start=platz|reihe1..4|weg1..4|gehege|podest|parkour|pk1..pk5|ziel|ziel2`
(`podest` = auf dem Aussichtspodest, Blick nach Westen über die Galerie);
Ausgangsparameter `--set tuer=1,9,1`.
(`run:T:x@t,…` holds Up for T s and presses X at fixed times; the stop steps reset the
timing error. Shots (`shot@`) block ~0.4 s → never shortly before a jump.)
* Checked in game: loading without a crash (many runs), all 31 gallery objects appear
(coin/secret/… react: coin counter, "GEHEIMNIS!"), the llama walks through gallery/enclosure,
up the stairs, jumps onto platform B and onto stepping stone 1, up the ramp, stairs to the
goal platform, sea = respawn, complete parkour by script, exit → summary → chapter 1 again.
Images: current `notes/img/vorschau/` (compact layout); `notes/img/testwelt_*.jpg` =
old sprawling layout (1–6 + montage even still with the old parkour).
* Start point for tests is switchable: `--set start=platz|reihe1..4|weg1..4|gehege|podest|parkour|pk1..pk5|ziel|ziel2`
(`podest` = on the viewing pedestal, looking west over the gallery);
exit parameters `--set tuer=1,9,1`.
### Nicht platzierte Dorf-Typen
| Typ | Grund |
### Village types not placed
| Type | Reason |
|---|---|
| Llama | ist der Spieler (Start) |
| LevelDoor | Ausgang am Parkour-Ende (braucht Boden mit Material 16, s. o.) |
| CameraSide | Kamera-Zone, unsichtbar |
| CutSceneCameraDirectional | Zwischensequenz-Kamera, unsichtbar |
| CutSceneTrigger | startet Zwischensequenz |
Kein platzierbarer Typ hat das Spiel zum Absturz gebracht (keine Bisektion nötig).
Hinweis: Mechanik-Objekte (Türen, Säulen, Statue) gehören im Dorf zu bestimmter Geometrie /
Kachel-Wechseln; in der Galerie stehen sie "nackt" (z. B. schwebendes RubyIdolDoor-Schild).
Typen, die nur im Code stehen (BouncingCoin, Sparkles, RubyIdolStatue …), haben keine
Original-Instanz mit Typfeldern und werden nicht angeboten (Ausnahme RubyIdol über `Strats.BORROWED`).
| Llama | is the player (start) |
| LevelDoor | exit at the end of the parkour (needs a floor with material 16, see above) |
| CameraSide | camera zone, invisible |
| CutSceneCameraDirectional | cutscene camera, invisible |
| CutSceneTrigger | starts a cutscene |
No placeable type crashed the game (no bisection needed).
Note: mechanics objects (doors, pillars, statue) belong to specific geometry /
tile swaps in the village; in the gallery they stand "bare" (e.g. a floating RubyIdolDoor sign).
Types that only exist in the code (BouncingCoin, Sparkles, RubyIdolStatue …) have no
original instance with type fields and are not offered (exception: RubyIdol via `Strats.BORROWED`).
## Vorschau-Renderings (Blender)
`/usr/bin/blender` (Debian 3.0.1): **Cycles rendert nur Schwarz** (auch die Standardszene), OCIO
fehlt („Color management: fallback“, nur View „Standard“). **Eevee geht**, braucht aber ein
X-Display → eigenes `Xvfb :7` (nie :0, :5 gehört lamatest). Ablauf (Skripte in
`tools/vorschau/`: `strats_json.py` (aus levelbau/ aufrufen), `render_level.py`, `label.py`):
## Preview renderings (Blender)
`/usr/bin/blender` (Debian 3.0.1): **Cycles renders only black** (even the default scene), OCIO
is missing ("Color management: fallback", only the "Standard" view). **Eevee works**, but needs an
X display → own `Xvfb :7` (never :0, :5 belongs to lamatest). Procedure (scripts in
`tools/vorschau/`: `strats_json.py` (call from levelbau/), `render_level.py`, `label.py`):
1. `geom_export.py out/testwelt.wad GE` → level.obj/mtl + page_NN.png
2. Strat-Positionen: Levelskript mit `levelbuild.Level` ausführen (ohne `L.build`), `L.strats` als JSON
2. Strat positions: run the level script with `levelbuild.Level` (without `L.build`), `L.strats` as JSON
3. `DISPLAY=:7 STRATS=strats.json VIEWS=schraeg,schraeg2,oben blender -b -P render_level.py -- GE r 64`:
eigener OBJ-Leser (der 3.0-Importer kennt keine Vertexfarben), Spiegelung OBJ→Spiel (y_blender =
−Spiel-z, Umlaufsinn drehen), Material = Textur (Closest) × Vertexfarbe, etwas Emission,
Sonne + Himmel, Marker für Strats (Münzen gold, Lama-Start oranger Kegel, Galerie-Objekte
in Reihenfarbe, Tiere weiß, Ausgang lila Ring); schreibt je Ansicht PNG + JSON mit
Bildkoordinaten der Bereichsnamen.
4. PIL: auf 1280 px skalieren, Beschriftungen (Start/Galerie/Gehege/Parkour/Ziel), JPG q85.
own OBJ reader (the 3.0 importer does not know vertex colours), mirroring OBJ→game (y_blender =
−game z, flip winding), material = texture (closest) × vertex colour, a little emission,
sun + sky, markers for strats (coins gold, llama start orange cone, gallery objects
in row colour, animals white, exit purple ring); writes PNG + JSON per view with
image coordinates of the area names.
4. PIL: scale to 1280 px, labels (Start/Galerie/Gehege/Parkour/Ziel), JPG q85.