Light, Sound & Vision
Light, sound, and vision as mechanics — how exposed you are, what you can see, and who can hear you past walls. The engine samples; your game owns the policy — there is deliberately no stealth, HP, or detection rule in the engine, only the primitives you read and threshold yourself (the same boundary as combat’s opaque payload). Frozen contract, ADR 0067. See the LSV demo, whose HUD meters exposure + noise like a real game.
All three share one occluder layer — bodies that should block line-of-sight, light, audio, and fog set
this layer, and every occlusion query defaults its mask to it (pass 0 for the default).
Illumination
Section titled “Illumination”Illumination.illuminanceAt(scene, x, y, z, mask)
Section titled “Illumination.illuminanceAt(scene, x, y, z, mask)”Returns: Num — a continuous 0..1 “how lit is this point”: the engine sums every scene light’s
falloff (matching the render, so reads-lit == looks-lit) with a masked line-of-sight occlusion test on
mask. 0 = fully hidden, 1 = fully lit. Bidirectional by nature — the lamp that lets you see also
lets things see you. The engine returns the raw value; a hard “spottable” stat is just illuminanceAt(...) > threshold,
or use it as a soft exposure meter or a × detectionRange multiplier into Ai.nearestVisibleTarget.
import "engine" for Illuminationvar exposure = Illumination.illuminanceAt(scene, px, 0.5, pz, 0) // 0 hidden … 1 litvar spottable = exposure > 0.35 // the GAME decides the thresholdFogOfWar
Section titled “FogOfWar”A world-XZ visibility field (Unknown / Explored / Visible) for a top-down co-op ARPG. configure bakes an
occluder grid from the scene’s static bodies (call after the level’s statics + a physics step). Each frame:
beginFrame greys stale vision to Explored (memory persists, even when a light dies), then reveal once per
viewer. Both team-shared and per-player vision are supported (the core is per-player-keyed).
FogOfWar.configure(scene, originX, originZ, cellSize, width, height)
Section titled “FogOfWar.configure(scene, originX, originZ, cellSize, width, height)”Build the field + bake the occluder grid. origin is the world corner of cell (0,0); the grid is width×height cells of cellSize.
FogOfWar.beginFrame(scene)
Section titled “FogOfWar.beginFrame(scene)”Grey stale Visible cells to Explored. Call once at the start of a frame’s reveal pass.
FogOfWar.reveal(scene, x, z, radius)
Section titled “FogOfWar.reveal(scene, x, z, radius)”Reveal line-of-sight cells from a viewer at (x, z) out to radius. Pass a radius already scaled by the
viewer’s illuminance — darkness shrinks your view (3 + illuminance * 7, say). A wall stops the reveal.
FogOfWar.state(scene, x, z)
Section titled “FogOfWar.state(scene, x, z)”Returns: Num — 0 Unknown, 1 Explored (remembered), 2 Visible (in view now).
FogOfWar.mark(scene, x, z, ttlSeconds) · tick(scene, dt) · isRevealed(scene, x, z, radius)
Section titled “FogOfWar.mark(scene, x, z, ttlSeconds) · tick(scene, dt) · isRevealed(scene, x, z, radius)”The wall-penetrating sonar reveal-tag channel — the inverse of line-of-sight. mark reveals a position
for ttlSeconds; tick ages the tags; isRevealed tests any live tag within radius. Range is a game
input (scale it by stillness for a “listen” ability).
NoiseBus (occlusion)
Section titled “NoiseBus (occlusion)”noiseBus.emitOccluded(scene, x, y, z, radius, mask)
Section titled “noiseBus.emitOccluded(scene, x, y, z, radius, mask)”Returns: List of heard listener ids — like NoiseBus.emit, but a wall on mask
between the sound and a listener blocks it. The same wall that muffles the audio shrinks what the AI
hears. Needs the scene’s physics (a masked raycast per listener).
SoundEmitter
Section titled “SoundEmitter”A pure-Wren helper unifying the audible side (a Source you play) with the audible-to-AI side (a
NoiseBus stimulus), so they stay consistent — a silenced weapon makes no noise; a silent pressure-plate
can still make noise. Auto-emit (any playing source auto-raising noise) is deliberately not offered.
SoundEmitter.new(source, bus)
Section titled “SoundEmitter.new(source, bus)”Returns: SoundEmitter — source may be null for a noise-only emitter; bus is a NoiseBus.
emitter.position(x, y, z) · emitter.radius=(r)
Section titled “emitter.position(x, y, z) · emitter.radius=(r)”Set the emitter’s world position and loudness radius.
emitter.emit() · emitter.emitOccluded(scene, mask)
Section titled “emitter.emit() · emitter.emitOccluded(scene, mask)”Play the clip (if any) and raise a NoiseBus stimulus at the emitter; emitOccluded blocks listeners a
wall on mask occludes. Returns: List of heard listener ids.
Source occlusion (the audible muffle)
Section titled “Source occlusion (the audible muffle)”source.setOcclusion(gain, gainHF)
Section titled “source.setOcclusion(gain, gainHF)”The audible half of emitOccluded: a wall between the listener and this Source muffles
it. gain (broadband) drops the volume; gainHF (0..1) drives a per-source low-pass — the muffle. Both
1 = unoccluded. Compute them from a masked listener→source ray (a Raycast on the occluder layer). Pairs
with NoiseBus.emitOccluded so the same wall muffles the audio and shrinks what the AI hears. (Degrades
to volume-only where the OpenAL EFX filter is unavailable.)