"""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| 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))