Yet Another SimDOCS
GitHub Open simulator
On this page

The effects package#

src/effects/ holds the reusable visual vocabulary the renderer draws with: AoE footprint geometry, ground circles, glow rings, connecting lines, element patterns and bursts, procedural markers, and mesh-targeted glow. It is imported through the @effects and @effects/babylon aliases (see tsconfig.json).

The split from src/client/render/ is by what the code knows about, not by what it draws. Anything that needs to know about a mechanic, a player, the World, raid ids or the asset root stays in the renderer. The package receives visual properties, positions, times and explicit meshes.

Layout#

src/effects/
  index.ts       # shape and element types, FloorAoe, visibility, vfx option types
  sampling.ts    # uniform random points inside a footprint
  schema.ts      # zod validation for the authored vfx block
  babylon/       # everything that touches Babylon: geometry, materials, handles, prewarming

index.ts owns the canonical AOEShape, ElementGlyphKind and WaymarkId. src/shared/world/ foundation.ts re-exports them, so simulator code keeps importing @shared/types as before.

The import boundary#

@effects may import Babylon and @shared/math — pure { x, z } vector helpers, no simulator concepts. Nothing else from @shared, and nothing from src/client, src/engine or src/server. In the other direction the engine must never import Babylon or @effects/babylon, because it runs headless on the server.

Both rules are enforced by src/effects/__tests__/boundaries.test.ts, which scans import specifiers rather than relying on convention.

Who draws with what#

Each primitive exists because several renderer layers wanted the same visual, so a change to one lands everywhere it should.

PrimitiveDrawn by
createShapeMesh / createShapeOutlineMeshthe telegraph layer and every other footprint (the inverse "?" telegraph, gazes, stacks)
syncFloorTelegraphsevery layer that renders a FloorAoe
createGroundCirclespread/stack areas, forced-march zones, tower soak counters
createGlowRingboss target rings, player status rings, the "?" mechanic ring
createLine / updateLinetethers and line links
createMeshGlowany highlighted object; currently the Index weapon

Art that only one mechanic uses — the boss-side orbs, the forced-march arrow, the tower pillar and falling object — deliberately stays a separate factory in src/client/render/meshes/. Those factories call into the package for their shared parts and keep their own distinctive geometry.

Resource ownership#

A factory returns a small handle owning its own geometry and private material, and the caller disposes it. The exception is anything cached per scene: the element floor materials (shared per element/colour/alpha) and the burst dot texture belong to the scene, and a caller must never dispose them. syncFloorTelegraphs encodes this — it frees an outline's own material but leaves a shared element material alone, because other AoEs on screen are still drawing with it.

FloorAoe is plain data#

FloorAoe has no instance methods on purpose. The World is JSON.stringify'd for lockstep hashing (src/shared/worldHash.ts) and JSON.parse'd back out of replay files (src/server/replayReader.ts), so a FloorAoe embedded in World state has to keep working after a round trip that loses its prototype. Visibility therefore lives in the standalone isFloorAoeVisible function, and authored vfx overrides ride the FloorAoe so they survive both the round trip and a deferred cleave being re-anchored when a bait arms it.

The DEFAULT_* colour constants exist because color is required on FloorAoe — there is no implicit convention inside the class or the renderer. Engine construction sites pass one of them when a mechanic authors no colour of its own.

Shader prewarming#

The first time a material family appears mid-fight, Babylon compiles its shader synchronously on the main thread, which shows up as a hitch. prewarmShaders renders one throwaway representative of each family at load. Babylon caches compiled effects by their defines until the engine is disposed, so warming one instance covers every later one; the temporary mesh and material are disposed afterwards and the cached effect persists.

Add a family by appending one factory to the warmups array in src/effects/babylon/prewarm.ts. Model art loads asynchronously, so createMeshGlow warms separately: call MeshGlow.warm with whatever meshes have arrived whenever nothing is highlighted. A glow with no target meshes stays disabled — Babylon skips its include filter when the list is empty, which would otherwise light every emissive mesh in the scene.

Authoring controls#

The vfx block on an aoe event overrides the floor pattern, the impact burst and the weapon glow. schema.ts validates it centrally. The field-by-field reference lives with the rest of the raid format in Authoring Raids; raids/debug/vfx-test.yaml exercises every shape and every override.