Files
llamamod/levels/testwelt.py
T

219 lines
14 KiB
Python

"""Testwelt - custom test map for "Ein Koenigreich fuer ein Lama" (replaces TestLevel chapter 1).
Build: python3 tools/levelbuild.py levels/testwelt.py out/testwelt.wad [--install]
Options (--set name=value; the option values are German words):
galerie=alle|keine|<comma list of types> which objects stand in the gallery (all|none|list)
start=platz|reihe1..4|weg1..4|gehege|podest|parkour|pk1..pk5|ziel|ziel2 llama start point (tests):
plaza | gallery row | path in front of row | enclosure | podium | parkour | parkour stage | goal
tuer=1,9,1 parameters of the exit door (tribe, world, chapter of the next level)
Scale (measured in game): llama ~0.6 tall, walks ~1.1 units/s, jumps 0.35 high and, with a
run-up, ~1.3 far. Steps <= 0.3 (without a gap = stopper), gaps 0.3, otherwise stairs/ramps.
Layout (game coordinates: x east, z south, y up; rot 0 = facing +z), compact
(moved closer together on 2026-11-06: island 32.5x23.5 + parkour strip instead of 42x34):
* start plaza (x 15..21, z 12..19): llama start, checkpoint, Inca pillars, flower beds
* object gallery directly south (x 14.5..32.5, z 20.5..32): 4 rows (collectibles / crates+pots /
characters / mechanisms) 2.7 apart, columns 1.9, each object on a flat plinth, paths in between;
animals in the enclosure right next to it to the east (x 33..42, z 20.8..30) with a viewing podium
* parkour starts at the east edge of the plaza, all on the line z = 15.5: stairs, stopper step,
2 gaps, beam, checkpoint, ramp, stopper step, 3 stepping stones, narrow beam, stairs,
goal platform (y 4) with a gate over the exit (LevelDoor on "door floor", material 16).
Dimensions unchanged from the first version, only shifted by PX = -1.
Scripted run: levels/testwelt_parkour.seq (with start=parkour)
* everything on an L-shaped island, sea all around (death plane -> respawn at checkpoint)
"""
import random
LEVEL_ARGS = dict(name='Testwelt', seed=7)
# gallery: (category, colour of the row post, types). type = name or (name, variant)
GALERIE = [
('Collectibles', 'gelb', ['Coin', 'ChargeBooster', 'RubyIdol', 'HealthFull', 'HealthGenerator',
'ChacaChargePotion', 'Secret']),
('Crates/pots', 'grau', [('SmashPot', 0), ('SmashPot', 1), 'Jar', 'ObjectDontDropInBox',
'WaterReboundPoints', 'CheatTeleport']),
('Characters', 'blau', ['TipoVillage', 'ChacaVillage', 'PachaGivingInfo', 'KidOnLlama', 'Wompy']),
('Mechanisms', 'rot', ['PressurePadDoor', 'RubyIdolDoor', 'SwitchPlatformPadVillage',
'AppearingPlatformsVillage', 'RisingColumns', 'SinkingStatue', 'FallingTree',
'CheckPointPost']),
]
GEHEGE = ['Bunny', 'Bunny', 'Ladybird', 'FlockOfBirds', 'VillagerHerder']
# deliberately not placed (see notes/levelbau.md)
NICHT = {'Llama': 'is the player', 'LevelDoor': 'exit at the end of the parkour',
'CameraSide': 'camera zone (invisible)', 'CutSceneCameraDirectional': 'cutscene camera (invisible)',
'CutSceneTrigger': 'starts a cutscene'}
GX0, DX = 17.4, 1.9 # first gallery column, spacing (previously 17.0 / 2.5)
ROW_Z = [22.5, 25.2, 27.9, 30.6] # previously 25 / 28.5 / 32 / 35.5
TUER_MAT = 16 # collision material "door floor" (village: floor in the exit door)
PX = -1.0 # parkour offset relative to the old version (only shifted, same dimensions)
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))
# ================================================================ island + sea
# L-shaped island: main part (plaza, gallery, enclosure) + narrow strip below the end of the parkour
X0, Z0, X1, Z1 = 11, 9.5, 43.5, 33 # main part
XB = 49 # east end of the parkour strip (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')
# base surfaces (no overlap): plaza and gallery get their own floors
L.floor(X0, Z0, XB, 12, 0, 'gras') # north edge
L.floor(X0, 12, 15, 20.5, 0, 'gras') # west edge next to the plaza
L.floor(X0, 20.5, 14.5, Z1, 0, 'gras') # west edge next to the gallery
L.floor(21, 12, XB, 20.5, 0, 'gras2') # meadow below the parkour
L.floor(15, 19, 21, 20.5, 0, 'gras') # between plaza and gallery
L.floor(32.5, 20.5, X1, 32, 0, 'gras') # enclosure
L.floor(14.5, 32, X1, Z1, 0, 'gras') # south edge
# ================================================================ start plaza (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))
# ================================================================ gallery (directly south of the plaza)
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) # main path north-south
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) # path in front of the row
# row post (category colour) at the start of the path
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
# ================================================================ enclosure (animals/enemies), east of the gallery
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)
# viewing podium at the enclosure (stairs up, between rows 2 and 3; enclosure entrance north of it)
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
# The parkour lies on one line (z = 15.5, heading east) so it can be done without turning the camera.
# Two "stopper" steps (+0.3, no gap) stop the llama at a fixed spot: there you jump up from
# standing and then have enough run-up (important for --seq runs).
# Llama: walks ~1.1 units/s, jumps ~1.3 far with a run-up and 0.35 high.
# All x values below are those of the old version + PX (geometry unchanged, only shifted).
def X(v):
return v + PX
# 1) stairs from the plaza (east edge x=21) up to 0.8, small landing, stopper step 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) two gaps (0.3) with small steps (+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) beam (0.5 wide)
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) middle platform with 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) ramp 1.5 -> 2.4, landing, stopper step 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) three stepping stones (1.5 long, 1.3 wide, gaps 0.3, +0.2 each)
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) narrow beam (0.45 wide) with support
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) stairs up to 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) goal platform (overhanging the coast) with pillars, parapet and a gate over the exit
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)
# mosaic in the gate: top with collision material 16 = "door floor" (LevelDoor only triggers when the
# llama stands on a polygon with material 16 in the door's grid cell)
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)
# gate over the exit (LevelDoor at 50.5/15.5, cell 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)
# ================================================================ decoration (edges only, dense)
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)
# ================================================================ player, checkpoints, exit
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)
# exit: parameters (tribe, world, chapter) of the next level. (1,9,1) = reload chapter 1 (after
# the summary "TestLevel Kapitel 1 / Muenzen / Geheimnisse"). NOT (7,7,7) ("world complete" like the
# last chapters of the original worlds): that unlocks world 10 -> crash on the selection map.
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])
# coins along the parkour (placed on the surfaces)
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('gallery: %d objects: %s' % (len(placed), ', '.join(placed)))
if skipped:
print('not in base level: %s' % ', '.join(skipped))