# Level editor – environment, lighting & visibility tools Plugin `editor.tools.environment.EnvironmentEditorPlugin` (phase 3, `notes/editor-ui.md`). Code: `src/main/java/com/eliasmueller/llamamod/editor/tools/environment/`, tests `src/test/.../editor/tools/environment/` (`EnvironmentToolsTest` needs `-Dllamamod.cd`; `EnvTour` = scripted screenshots, `EnvScenarios` = the in-game test projects). ![zones](img/editor/env_zone.jpg) ## What the user gets | Where | What | |---|---| | **Environment** panel (left dock) – *Zones* tab | Live fog preview (on/off, zone: camera / start / selected, detail Min/Medium/Max, status "showing … fog a → b cells"), list of all zones (start zone first; fog colour swatch, kind, trigger → target, fog end, music), + Teleport (starts the Zone tool), + Environment zone, delete, frame | | Inspector section "Environment zone" | trigger cell x/z/y + height test, *move the player here* (flag 4), target x/y/z, facing (°), step forward, fog mode, fog ramp bar, draw distance at Min/Medium/Max, the four fog values, fog/sky colour and ambient colour pickers (live preview while picking, one undo step), music dropdown (track names), sky model (STPC offsets used in the level) and sky yaw, *Make start zone*, *Duplicate*. Raw fields stay in the generic properties below | | **Zones** tool | no zone selected: click a cell → new teleport zone (environment copied from the start zone); then click/drag = target, Shift+drag = facing (22.5° steps), `[` `]` turn; *Trigger cell* mode moves the trigger. Viewport: target ring + facing arrow (all zones), selected zone: filled trigger cell, box and an arc to the target; the target is a pickable marker | | *Lights* tab | *Preview lights on objects* (engine lighting of all object models), list of lights (swatch, type, position, radius), + Point (Lights tool), + Directional, delete | | Inspector section "Light" | type (directional / point / negative point), colour picker + intensity (0–2, colour value 128 = 1.0), directional: azimuth/elevation, point: position, full radius, falloff radius, flicker | | **Lights** tool | click = point light 1 cell above the surface (colour preset: torch, fire, lamp, magic, moon, glow), drag = radius. Gizmos: directional = arrows above the level centre, point = radius rings, selected = spheres for both radii | | *Lighting* tab | bake: presets Day / Dusk / Night / Cave / Flat or the level's own sun, sun azimuth/elevation/strength/colour, sky + ground colour, AO amount/radius, shadow light; scope all / selected tiles; options: add LGHT point lights to the tiles, also set the level lighting for brushes, set the start zone's fog + ambient to the preset. One undo step. Display: textured / vertex colours only; button for the paint tool | | **Paint vertex colours** tool (V) | modes paint / lighten / darken / smooth, colour + brightness (1 = texture colour, 2 = max), radius, strength, falloff smooth/linear/constant, selection only; Ctrl+click = eyedropper; one undo step per stroke (`PropMapCommand` merges by stroke) | | **Visibility** panel (right dock) | 32 × 32 mask matrix (row = camera region, column = drawn region; click/drag toggles, *symmetric* option, unused regions dimmed, row label picks a region), preview *off / follow the camera / from region n* (hides the tiles and objects the game would not draw), All visible, Self only, Make symmetric, Auto-generate… (one region / blocks / connected areas + distance) with a triangle-cost warning, statistics (triangles drawn per camera region, max/mean) | | **Paint regions** tool | region (colour swatches) + brush 1…17 cells, Ctrl+click picks; switches the region layer on | | Level → *Level settings…* (replaces the built-in action) | name, author, description, TestLevel chapter (+ chapters used by other projects), base level, grid width/height (grows east/south, region grid copied), death plane height (KILL_PLANE / SEA brushes and material-30 no-floor tiles move together, or add one), WFPC flags as check boxes (layout flags read-only, unknown set bits listed raw), strat pool | | Level → Bake lighting / Environment zones / Visibility regions; View → Fog preview, Light objects, Vertex colours only (Shift+V) | shortcuts to the above | | Problems (Check/Build) | fog end < 3 or > 30 cells at detail Max, fog start ≥ end, no / several start zones, trigger outside the grid, teleport target outside the grid (error), teleport without exit floor or door object in the trigger cell (info), player start in a region that does not draw itself (error), regions never drawn from anywhere, region drawing > 30k triangles (info), tiles with flat grey / black vertex colours, point lights without radius, black lights, ignored light types | ## Engine facts used (groove.exe, see also mod-fog.md / mod-mirror.md) * **Zone records** (MAP block 4, 90 bytes; `EnvZone`): the start zone has flag 8 (`+0x20`) and target (1,1,1) raw – exactly one per original level, trigger at (3, y, 0) outside the playfield. Teleports: strat opcode **0x100** (`FUN_0054df30`, table 0x578ff4 + op·4) looks up the zone of the calling object's cell (`FUN_0054d560`: same cell x/z, smallest cellY ≥ player cell y (+1 with angle bit 0, −1 with bit 1)), then `FUN_0054de00`: unless flag 4, position = target, yaw = (angle & 0xff0) << 12; then `FUN_0041e1c0` switches fog, colours, music; if angle bit 1 is clear the player is pushed `(short)+0x38` along the facing. In the originals the trigger cells hold exit floors (collision material 16) or door objects (RubyIdolDoor, PressurePadDoor): house doors in the village. Flags 0x400/0x800 are read by strat opcode 0x81 (unknown use). * **Fog mode** (`+0x34` → DAT_005865cc, `FUN_00438180`): 0 none, 1 darkness with a white point light on the player with radius fog start…end (no original uses it), 2 table fog, 3 vertex fog. Fog fades to the fog colour (`+0x50`, also the backdrop clear colour); the far clip = fog end. `+0x4c` = ambient colour of object lighting (`FUN_0041bcf0`, byte/255). * **Music** ids: table at 0x578960 (20 bytes per entry, name pointer + 8) → `music/.ASF` (`FUN_00546b70`); ids 1–39 (VILLAGE … ENDGAME), 0 stops. `+0x56` u16 → `FUN_005460d0` (low nibble, bits 8–14; probably ambience, kept raw). * **Lights** (LGHT, `FUN_0042c180`): positions and directions are **game space** (z = grid row, the engine negates z when loading) – the editor previously treated them as export space (fixed in `SceneRenderer`, `Transformers.LIGHT`, `Edits.translate`, see "shared changes"). Colour factor 2c/255; type 4 = **negative** light −(1 + 2c/255)/2. Point light: full up to `a`, linear falloff to 0 at `b` (`FUN_0041b8a0` / `FUN_0041ba10`); byte c = 1 → random ×1.5/×0.5 every frame (DAT_005865d4 = random & 3) = flicker. Lights only light **object models** (strats, per vertex: ambient + all directional + nearest point lights, max 6, `FUN_0041bdb0`); tiles use baked vertex colours. 97 directional and 1729 point lights in the 31 levels, no type 4. * **Regions**: camera cell → mask (`FUN_0041e020`), mask 0 = everything, camera outside the grid = everything. Tiles (`FUN_0042bf40`) and strats (flag 0x40000 unless strat flag 0x800) in cells whose region bit is not set are skipped. The second table value + the top tile's y goes to DAT_00586424 (strat opcode 0x78; unknown use). * **WFPC** bits: 0x10 / 0x100 / 0x10000 / 0x100000 / 0x200000 / 0x4000000 / 0x10000000 change the data layout (read-only in the dialog); 0x800 renderer fog flag (`FUN_00424b90`), 0x80000 read by strat opcode 0x8e, 0x8000000 collision materials 17/18 ignored are editable; others raw. ## How the previews work * **Fog** (`EnvSession.updateFog`): zone = selected zone, else the start zone or the zone whose *area* is nearest (teleport targets / player start; the game switches zones only by teleports), fog start/end for the chosen detail exactly as `FUN_0041a480` (incl. the 1.5× loader rule). Renderer addition `Viewport.setFogOverride(FogOverride)`: fog ramp to the zone colour, geometry beyond the end discarded, clear colour = fog colour; distances are measured from the orbit pivot (the game camera follows the llama a few cells behind), so an orbiting camera shows the "visible bubble" around the point it looks at. When the camera is far from its pivot (overview) the preview pauses ("Zoom in to see the fog of …"). * **Shading** `Viewport.setShading(SHADE_VERTEX_COLORS)`: texture rgb replaced by white (cut-out alpha kept). * **Object lighting**: per-vertex colours of every OBJECT render instance from the engine formula (`ObjectLighting`, `EnvMath.shade`), re-applied after scene changes. * **Region preview**: hides tiles/objects through `ctx.setHidden` and restores exactly the nodes it hid (user-hidden nodes stay hidden). ## Bake (`VertexBake`) Same shading model as `levelbuild.Lighting` (sky/ground hemisphere by normal, sun Lambert × shadow ray, AO with 24 Fibonacci rays, `amb·ao + sun·f·(0.6 + 0.4·ao)`), but traced against the real triangles of all tiles (opaque faces; uniform 1-cell xz grid with a 2D DDA, per-cell max height skips upward rays). Optional point lights (engine falloff, shadowed, ×0.5). Writes frame 0, keeps byte 3 (frame count) and other frames. Village (1358 tiles, 26k triangles): **0.7 s**. Brushes are not tiles in the scene – their light comes from the level's Lighting settings, which the bake can set (the brush preview recompiles). ## Shared changes (minimal, backwards compatible) * `editor.render.Viewport`: `FogOverride` record, `setFogOverride/fogOverride`, `setShading/shading`, `SHADE_*`; `GlRenderer.FrameParams.fogOverride/shading`; shader uniforms `uFogMode`, `uFogOrigin`, `uFarClip`, `uShade` (default path unchanged: 40–160 cells, 0.85). * Light z sign (stored z = game z): `ui.view.SceneRenderer` (marker position, direction line), `ui.tools.Transformers.LIGHT`, `model.cmd.Edits.translate`; `model.Light` javadoc corrected. ## Verified in game (apptest, display :8, Wine 6, Village 1 edits built with `EnvScenarios`) See `img/editor/env_game_*.jpg`. * **New teleport zone (2026-11-07):** Village 1 edit with zones made by `ZoneTool.create` in the 2×3 cells east of the player start (all their floor polygons painted material 16, zone cellY 8, target 40.5/7.25/62.5, red fog 2–7 cells). Walking east from the original start: black fade, the llama stands at the target next to the checkpoint post, red fog active (`img/editor/env_game_teleport.jpg`). Teleports fire only while the llama *moves* onto the exit floor (player code checks a speed property > 0): spawning on a house-door pad (43.5/48.45) does nothing. * **Start positions around 34–35 / 58 crash the game while loading** (page fault 0x405527 in FUN_004054c0, floor-normal lookup), both with boot-to-level's start override and with the Llama moved in the project (`EnvScenarios` "village-teleport"); the original start is fine. Not investigated further. ## Fog preview while zooming Beyond max(10, 1.6 × fog end) cells from the orbit pivot the preview fades out over another half of that distance (fog radius grows ×(1 + 30u²), amount × (1 − u)); the overview (no override) starts when the fade is complete and ends again at 90 % of it. The override is recomputed on every camera change (the zone lookup still runs 120 ms after the camera stops), so zooming no longer pops between "fogged" and "whole level". ## Known limits * Zone "areas" for the fog preview are a heuristic (nearest teleport target); the game's real current zone depends on which door the player used last. * The bake is not identical to the original Argonaut lighting (their vertex colours were hand-tuned/radiosity); it reproduces levelbuild's look. * The object lighting preview ignores the flicker and the second light set (fog mode 1); objects whose model is rebuilt by the shell get their lit colours back after the next scene change. * Grid resizing only grows/shrinks at the east/far end; WFPC layout flags cannot be changed (would need converting the MAP blocks). * Region preview uses the editor's hidden flags (visible in the outliner while active).