Files
llamamod/levels/testwelt.py
T

218 lines
14 KiB
Python

"""Testwelt - eigene Test-Map fuer "Ein Koenigreich fuer ein Lama" (ersetzt TestLevel Kapitel 1).
Bauen: python3 tools/levelbuild.py levels/testwelt.py out/testwelt.wad [--install]
Optionen (--set name=wert):
galerie=alle|keine|<Komma-Liste von Typen> welche Objekte in der Galerie stehen
start=platz|reihe1..4|weg1..4|gehege|podest|parkour|pk1..pk5|ziel|ziel2 Startpunkt des Lamas (Tests)
tuer=1,9,1 Parameter des Ausgangs (Stamm, Welt, Kapitel des naechsten Levels)
Masstab (im Spiel gemessen): Lama ~0.6 hoch, laeuft ~1.1 Einheiten/s, springt 0.35 hoch und mit
Anlauf ~1.3 weit. Stufen <= 0.3 (ohne Luecke = Anschlag), Luecken 0.3, sonst Treppen/Rampen.
Aufbau (Spielkoordinaten: x nach Osten, z nach Sueden, y hoch; rot 0 = Blick nach +z), kompakt
(2026-11-06 zusammengerueckt: Insel 32.5x23.5 + Parkour-Streifen statt 42x34):
* Startplatz (x 15..21, z 12..19): Lama-Start, Checkpoint, Inka-Saeulen, Blumenbeete
* Objekt-Galerie direkt suedlich (x 14.5..32.5, z 20.5..32): 4 Reihen (Sammeln / Kisten+Toepfe /
Figuren / Mechanik) im Abstand 2.7, Spalten 1.9, jedes Objekt auf flachem Sockel, Wege dazwischen;
Tiere im Gehege gleich oestlich daneben (x 33..42, z 20.8..30) mit Aussichtspodest
* Parkour beginnt an der Ostkante des Platzes, alles auf der Linie z = 15.5: Treppe, Anschlag-Stufe,
2 Luecken, Balken, Checkpoint, Rampe, Anschlag-Stufe, 3 Trittsteine, schmaler Balken, Treppe,
Ziel-Plattform (y 4) mit Tor ueber dem Ausgang (LevelDoor auf "Tuerboden", Material 16).
Masse unveraendert gegenueber der ersten Fassung, nur um PX = -1 verschoben.
Skript-Durchlauf: levels/testwelt_parkour.seq (mit start=parkour)
* alles auf einer L-foermigen Insel, ringsum Meer (Todesebene -> Respawn am Checkpoint)
"""
import random
LEVEL_ARGS = dict(name='Testwelt', seed=7)
# Galerie: (Kategorie, Farbe des Reihen-Pfostens, Typen). Typ = Name oder (Name, Variante)
GALERIE = [
('Sammeln', 'gelb', ['Coin', 'ChargeBooster', 'RubyIdol', 'HealthFull', 'HealthGenerator',
'ChacaChargePotion', 'Secret']),
('Kisten/Toepfe', 'grau', [('SmashPot', 0), ('SmashPot', 1), 'Jar', 'ObjectDontDropInBox',
'WaterReboundPoints', 'CheatTeleport']),
('Figuren', 'blau', ['TipoVillage', 'ChacaVillage', 'PachaGivingInfo', 'KidOnLlama', 'Wompy']),
('Mechanik', 'rot', ['PressurePadDoor', 'RubyIdolDoor', 'SwitchPlatformPadVillage',
'AppearingPlatformsVillage', 'RisingColumns', 'SinkingStatue', 'FallingTree',
'CheckPointPost']),
]
GEHEGE = ['Bunny', 'Bunny', 'Ladybird', 'FlockOfBirds', 'VillagerHerder']
# bewusst nicht platziert (siehe notes/levelbau.md)
NICHT = {'Llama': 'ist der Spieler', 'LevelDoor': 'Ausgang am Parkour-Ende',
'CameraSide': 'Kamera-Zone (unsichtbar)', 'CutSceneCameraDirectional': 'Zwischensequenz-Kamera (unsichtbar)',
'CutSceneTrigger': 'startet eine Zwischensequenz'}
GX0, DX = 17.4, 1.9 # erste Galerie-Spalte, Abstand (vorher 17.0 / 2.5)
ROW_Z = [22.5, 25.2, 27.9, 30.6] # vorher 25 / 28.5 / 32 / 35.5
TUER_MAT = 16 # Kollisionsmaterial "Tuerboden" (Dorf: Boden in der Ausgangstuer)
PX = -1.0 # Versatz des Parkours gegenueber der alten Fassung (nur verschoben, Masse gleich)
def opt(L, k, d):
return getattr(L, 'opts', {}).get(k, d)
def baum(L, x, z, y=0.0, h=1.6, c=1.1):
L.tree(x, z, y, height=h, crown=c)
def build(L):
sel = opt(L, 'galerie', 'alle')
rnd = random.Random(3)
L.sun.update(azimuth=220, elevation=48, strength=0.72, sky=(0.47, 0.52, 0.62), ground=(0.27, 0.25, 0.21))
# ================================================================ Insel + Meer
# L-foermige Insel: Hauptteil (Platz, Galerie, Gehege) + schmaler Streifen unter dem Parkour-Ende
X0, Z0, X1, Z1 = 11, 9.5, 43.5, 33 # Hauptteil
XB = 49 # Ostende des Parkour-Streifens (z Z0..20.5)
L.sea(0, 0, 60, 42, y=-1.5, kill=(X0 - 4, Z0 - 4, 54, Z1 + 4))
L.box(X0, Z0, X1, Z1, -2.2, 0, top=None, side='erde2', side_top='graskante', solid=False)
L.box(X1, Z0, XB, 20.5, -2.2, 0, top=None, side='erde2', side_top='graskante', solid=False, sides='nes')
# Grundflaechen (ohne Ueberlappung): Platz, Galerie bekommen eigene Boeden
L.floor(X0, Z0, XB, 12, 0, 'gras') # Nordrand
L.floor(X0, 12, 15, 20.5, 0, 'gras') # Westrand neben dem Platz
L.floor(X0, 20.5, 14.5, Z1, 0, 'gras') # Westrand neben der Galerie
L.floor(21, 12, XB, 20.5, 0, 'gras2') # Wiese unter dem Parkour
L.floor(15, 19, 21, 20.5, 0, 'gras') # zwischen Platz und Galerie
L.floor(32.5, 20.5, X1, 32, 0, 'gras') # Gehege
L.floor(14.5, 32, X1, Z1, 0, 'gras') # Suedrand
# ================================================================ Startplatz (6 x 7)
L.box(15, 12, 21, 19, -0.25, 0.0, top='stein', side='stein_kante')
L.box(16.5, 14.0, 19.5, 17.0, 0.0, 0.08, top='inka', side='stein_kante', rot=0, side_coll=False)
for (x, z) in [(15.6, 12.6), (20.4, 12.6), (15.6, 18.4), (20.4, 18.4)]:
L.pillar(x, z, 0, 1.6, size=0.6, side='inka', top='stein')
L.box(x - 0.4, z - 0.4, x + 0.4, z + 0.4, 1.6, 1.8, top='stein', side='stein_kante', rot=0)
L.box(16.6, 19.2, 18.2, 20.1, 0, 0.1, top='blumen', side='holz_dunkel', side_coll=False)
L.box(18.9, 19.2, 20.7, 20.1, 0, 0.1, top='blumen2', side='holz_dunkel', side_coll=False)
for (x, z) in [(12.5, 10.6), (23.5, 10.6), (12.4, 14.2), (12.6, 18.2)]:
baum(L, x, z, h=rnd.uniform(1.3, 1.9))
# ================================================================ Galerie (direkt suedlich vom Platz)
L.box(14.5, 20.5, 32.5, 32, -0.2, 0.0, top=None, side='stein', solid=False)
L.floor(14.5, 20.5, 32.5, 32, 0, 'gras')
L.floor(15.6, 20.5, 16.3, 32, 0.02, 'sand', coll=False) # Hauptweg Nord-Sued
placed, skipped = [], []
for (cat, farbe, typen), zr in zip(GALERIE, ROW_Z):
xe = GX0 + (len(typen) - 1) * DX + 0.7
L.floor(16.3, zr - 1.3, xe, zr - 0.7, 0.02, 'sand', coll=False) # Weg vor der Reihe
# Reihen-Pfosten (Kategoriefarbe) am Weganfang
L.pillar(15.05, zr - 0.3, 0, 1.1, size=0.25, side=farbe, top=farbe)
L.box(14.85, zr - 0.5, 15.25, zr - 0.1, 1.1, 1.25, top=farbe, side='holz_dunkel', rot=0)
x = GX0
for t in typen:
name, var = (t, 0) if isinstance(t, str) else t
L.box(x - 0.45, zr - 0.45, x + 0.45, zr + 0.45, 0, 0.08, top='stein_gruen', side='stein_kante',
rot=0, side_coll=False)
if sel == 'alle' or name in sel.split(','):
if name in L.protos and var < len(L.protos[name]):
L.strat(name, x, zr, variant=var)
placed.append(name if not var else '%s#%d' % (name, var))
else:
skipped.append(name)
x += DX
# ================================================================ Gehege (Tiere/Gegner), oestlich der Galerie
gx0, gz0, gx1, gz1 = 33, 20.8, 42, 30
L.floor(gx0, gz0, gx1, gz1, 0.015, 'gras_streifen', coll=False)
L.fence([(gx0, gz0), (gx1, gz0), (gx1, gz1), (gx0, gz1), (gx0, gz0 + 3.2)], 0, height=0.45, thick=0.07,
posts_every=1.0)
baum(L, gx1 - 1.3, gz1 - 1.3, h=1.8)
baum(L, gx1 - 1.2, gz0 + 1.2, h=1.4)
for i, name in enumerate(GEHEGE):
if sel == 'alle' or name in sel.split(','):
L.strat(name, gx0 + 1.8 + (i % 3) * 2.7, gz0 + 2.3 + (i // 3) * 4.5)
placed.append(name)
# Aussichtspodest am Gehege (Treppe hoch, zwischen Reihe 2 und 3; Eingang ins Gehege noerdlich davon)
L.stairs(31.2, 25.3, 32.4, 26.9, 0, 0.6, 3, '+x', top='holz_maser', side='holz_dunkel')
L.box(32.4, 24.7, 33.0, 27.5, 0, 0.6, top='holz_maser', side='holz_dunkel', rot=0)
# ================================================================ Parkour
# Der Parkour liegt auf einer Linie (z = 15.5, nach Osten), damit man ohne Kameradrehen durchkommt.
# Zwei "Anschlag"-Stufen (+0.3, ohne Luecke) halten das Lama an einer festen Stelle an: dort
# springt man aus dem Stand hoch und hat danach genug Anlauf (wichtig fuer --seq-Laeufe).
# Lama: laeuft ~1.1 Einheiten/s, springt mit Anlauf ~1.3 weit und 0.35 hoch.
# Alle x-Werte unten sind die der alten Fassung + PX (Geometrie unveraendert, nur verschoben).
def X(v):
return v + PX
# 1) Treppe vom Platz (Ostkante x=21) auf 0.8, kleiner Absatz, Anschlag-Stufe 1 (+0.3)
L.stairs(X(22), 14.5, X(24), 16.5, 0, 0.8, 4, '+x', top='stein', side='stein2')
L.box(X(24), 14.5, X(24.6), 16.5, 0, 0.8, top='stein', side='stein2', rot=0)
L.box(X(24.6), 14.5, X(26), 16.5, 0, 1.1, top='gras', side='erde', side_top='graskante')
# 2) zwei Luecken (0.3) mit kleinen Stufen (+0.2)
L.box(X(26.3), 14.7, X(27.8), 16.3, 0, 1.3, top='holz_maser', side='holz_dunkel', rot=0)
L.box(X(28.1), 14.9, X(29.6), 16.1, 0, 1.5, top='holz_maser', side='holz_dunkel', rot=0)
# 3) Balken (0.5 breit)
L.box(X(29.6), 15.25, X(32.5), 15.75, 1.25, 1.5, top='holzbalken', side='holz_dunkel', bottom='holz_dunkel',
rot=0)
L.pillar(X(31.05), 15.5, 0, 1.25, size=0.2, side='rinde', top='rinde')
# 4) Mittelplattform mit Checkpoint
L.box(X(32.5), 14.0, X(35.0), 17.0, 0, 1.5, top='stein', side='stein_moos', side_top='stein_kante')
# 5) Rampe 1.5 -> 2.4, Absatz, Anschlag-Stufe 2 (+0.3)
L.ramp(X(35.0), 14.75, X(38.0), 16.25, 1.5, 2.4, '+x', top='sand', side='erde', base=0.0)
L.box(X(38.0), 14.75, X(38.6), 16.25, 0, 2.4, top='sand', side='erde', rot=0)
L.box(X(38.6), 14.6, X(39.8), 16.4, 0, 2.7, top='stein', side='stein_moos', side_top='stein_kante', rot=0)
# 6) drei Trittsteine (1.5 lang, 1.3 breit, Luecken 0.3, je +0.2)
stones = [(X(40.85), 15.5, 2.9), (X(42.65), 15.5, 3.1), (X(44.45), 15.5, 3.3)]
for (x, z, y) in stones:
L.box(x - 0.75, z - 0.65, x + 0.75, z + 0.65, 0, y, top='stein_gruen', side='stein_moos', rot=0)
# 7) schmaler Balken (0.45 breit) mit Stuetze
L.box(X(45.2), 15.275, X(47.4), 15.725, 3.05, 3.3, top='holzbalken', side='holz_dunkel', bottom='holz_dunkel',
rot=0)
L.pillar(X(46.3), 15.5, 0, 3.05, size=0.18, side='rinde', top='rinde')
# 8) Treppe auf 4.0
L.stairs(X(47.4), 14.7, X(48.6), 16.3, 3.3, 4.0, 3, '+x', top='stein', side='stein2', base=0.0)
# 9) Ziel-Plattform (ragt ueber die Kueste) mit Saeulen, Brustwehr und Tor ueber dem Ausgang
L.box(X(48.6), 12.6, X(53.6), 18.4, -2.2, 4.0, top='platte2', side='stein_moos', side_top='stein_kante')
for (x, z) in [(49.0, 13.0), (53.2, 13.0), (49.0, 18.0), (53.2, 18.0)]:
L.pillar(X(x), z, 4.0, 5.0, size=0.35, side='inka', top='stein')
L.box(X(53.35), 13.3, X(53.6), 17.7, 4.0, 4.45, top='stein', side='stein_kante', rot=0)
# Mosaik im Tor: Oberseite mit Kollisionsmaterial 16 = "Tuerboden" (LevelDoor loest nur aus, wenn das
# Lama in der Rasterzelle der Tuer auf einem Polygon mit Material 16 steht)
L.box(X(50.5), 14.3, X(52.3), 16.7, 4.0, 4.08, top='muster_lila', side='inka', rot=0, side_coll=False,
mat=TUER_MAT)
# Tor ueber dem Ausgang (LevelDoor bei 50.5/15.5, Zelle 50/15)
for z in (14.45, 16.55):
L.pillar(X(51.5), z, 4.08, 5.3, size=0.3, side='inka', top='stein')
L.box(X(51.25), 14.3, X(51.75), 16.7, 5.3, 5.55, top='stein', side='stein_kante', bottom='stein_kante', rot=0)
# ================================================================ Deko (nur am Rand, dicht)
for (x, z) in [(12.3, 22.5), (12.6, 27), (12.2, 31.5), (17.5, 32.6), (25, 32.7), (36, 32.5), (42.6, 32.3),
(42.8, 23), (29, 10.5), (35, 10.4), (41, 10.6), (46, 19.6), (45.5, 10.8)]:
baum(L, x, z, h=rnd.uniform(1.2, 2.0), c=rnd.uniform(0.9, 1.3))
for (x, z) in [(13.2, 25), (21.5, 32.5), (30.5, 32.6), (39, 32.6), (26.5, 19.6), (39.5, 19.4)]:
r = rnd.uniform(0.3, 0.5)
L.box(x - r, z - r * 0.8, x + r, z + r * 0.8, 0, r * 0.9, top='stein_moos', side='stein_blau')
for (x0, z0, w, d, t) in [(11.4, 19.4, 2, 1.8, 'blumen'), (14.8, 32.1, 2, 0.8, 'blumen2'),
(33.5, 32.1, 2.5, 0.8, 'blumen3'), (31, 11, 2, 1.0, 'blumen2'),
(40.5, 18.6, 2, 1.6, 'blumen'), (25, 10.4, 1.5, 1.4, 'blumen3')]:
L.floor(x0, z0, x0 + w, z0 + d, 0.015, t, coll=False)
# ================================================================ Spieler, Checkpoints, Ausgang
starts = dict(platz=(18, 14.6, 0), parkour=(X(20.5), 15.5, 90), pk1=(X(24.3), 15.5, 90), pk2=(X(33.0), 15.5, 90),
pk3=(X(38.3), 15.5, 90), pk4=(X(44.45), 15.5, 90), pk5=(X(46.0), 15.5, 90), ziel=(X(49.0), 15.5, 90),
ziel2=(X(51.5), 17.8, 180), gehege=(30.4, 26.1, 90),
podest=(32.7, 26.1, 270))
for i, zr in enumerate(ROW_Z):
starts['reihe%d' % (i + 1)] = (15.95, zr, 90)
starts['weg%d' % (i + 1)] = (15.95, zr - 1.0, 90)
sx, sz, srot = starts[opt(L, 'start', 'platz')]
L.player(sx, sz, rot=srot)
L.checkpoint(16.1, 17.4)
L.checkpoint(X(33.6), 14.6)
# Ausgang: Parameter (Stamm, Welt, Kapitel) des naechsten Levels. (1,9,1) = Kapitel 1 neu laden (nach
# der Bilanz "TestLevel Kapitel 1 / Muenzen / Geheimnisse"). NICHT (7,7,7) ("Welt fertig" wie die
# letzten Kapitel der Original-Welten): das schaltet Welt 10 frei -> Absturz auf der Auswahlkarte.
tuer = [int(v) for v in opt(L, 'tuer', '1,9,1').split(',')]
L.exit(X(51.5), 15.5, params=[v * 0x1000 for v in tuer])
# Muenzen entlang des Parkours (auf die Flaechen gestellt)
L.coins([(X(x), z) for x, z in [(22.5, 15.5), (23.5, 15.5), (25.3, 15.5), (27.0, 15.5), (28.85, 15.5),
(30.0, 15.5), (31.0, 15.5), (32.0, 15.5), (35.8, 15.5), (36.6, 15.5),
(37.4, 15.5), (39.2, 15.5)]] + [(x, z) for x, z, _ in stones] +
[(X(x), z) for x, z in [(45.8, 15.5), (46.8, 15.5), (48.0, 15.5)]])
L.coins([(17.0, 16.6), (19.0, 16.6)])
L._galerie = (placed, skipped)
def report(L):
placed, skipped = L._galerie
print('Galerie: %d Objekte: %s' % (len(placed), ', '.join(placed)))
if skipped:
print('nicht im Basislevel: %s' % ', '.join(skipped))