# Levelbau mit `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). ## Aufruf ``` 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/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). ## Level-Skript ```python LEVEL_ARGS = dict(name='Testwelt', seed=7) # NAME-Chunk, Zufallssaat (Texturdrehung) def build(L): # L = levelbuild.Level L.sea(0, 0, 88, 72, y=-2.0, kill=(6, 4, 80, 66)) # Meer + Todesebene 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) L.checkpoint(17, 18.5) L.exit(70.5, 12.5) L.strat('Bunny', 66, 34) L.coins([(27, 20), (28, 20)]) def report(L): # optional, nach dem Bauen ... ``` `L.opts` enthält die `--set`-Werte (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. ### Geometrie-Bausteine | Methode | Beschreibung | |---|---| | `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 | Texturen: Name aus `TEX` (Dorf: `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. ### Beleuchtung (Vertexfarben) `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. ### Objekte (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)` (`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. ### 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. ## 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. ## Ausgang (LevelDoor) – wie er funktioniert 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): ``` 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 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. ## 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. | Bereich | alt | neu | |---|---|---| | 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 | * **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, 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: ``` 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)" ``` (`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`. ### Nicht platzierte Dorf-Typen | Typ | Grund | |---|---| | 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`). ## 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`): 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 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.