@adatechnology/game-weather-three 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/sky.ts","../src/fog.ts","../src/headlights.ts","../src/heat-haze.ts","../src/lightning.ts","../../game-vfx/src/area-cap.ts","../../game-vfx/src/budget.ts","../../game-vfx/src/data/explosion-aliases.json","../../game-vfx/src/data/explosions.json","../../game-vfx/src/catalog.ts","../../game-vfx/src/color.ts","../../game-vfx/src/constants.ts","../../game-vfx/src/pool.ts","../../game-vfx/src/pools/chunks.ts","../../game-vfx/src/vector.ts","../../game-vfx/src/pools/glows.ts","../../game-vfx/src/pools/puffs.ts","../../game-vfx/src/pools/sparks.ts","../../game-vfx/src/spawn.ts","../../game-vfx/src/emitters/fire.ts","../../game-vfx/src/effects/dsl.ts","../../game-vfx/src/effects/steps.ts","../../game-vfx/src/effects/creatures/bloater-cloud.ts","../../game-vfx/src/effects/creatures/blood-hit.ts","../../game-vfx/src/effects/creatures/corpse-decay.ts","../../game-vfx/src/effects/creatures/eyes-in-fog.ts","../../game-vfx/src/effects/creatures/dismember.ts","../../game-vfx/src/effects/creatures/headshot.ts","../../game-vfx/src/effects/creatures/reanimation.ts","../../game-vfx/src/effects/creatures/screamer-shriek.ts","../../game-vfx/src/effects/props.ts","../../game-vfx/src/effects/weapons/impact-by-material.ts","../../game-vfx/src/effects/weapons/penetration.ts","../../game-vfx/src/effects/weapons/casings-and-magazine.ts","../../game-vfx/src/effects/weapons/molotov.ts","../../game-vfx/src/effects/weapons/grenade.ts","../../game-vfx/src/effects/weapons/explosive-barrel.ts","../../game-vfx/src/effects/weapons/muzzle-flash.ts","../../game-vfx/src/effects/destruction/shopfront-glass.ts","../../game-vfx/src/effects/vector-math.ts","../../game-vfx/src/effects/destruction/door-stages.ts","../../game-vfx/src/effects/destruction/building-collapse.ts","../../game-vfx/src/effects/destruction/block-wall.ts","../../game-vfx/src/effects/destruction/spot.ts","../../game-vfx/src/effects/destruction/metal-door.ts","../../game-vfx/src/effects/destruction/iron-railing.ts","../../game-vfx/src/effects/destruction/wooden-fence.ts","../../game-vfx/src/effects/destruction/hit-point-breakage.ts","../../game-vfx/src/effects/destruction/pressure-cylinders.ts","../../game-vfx/src/effects/world/building-fire.ts","../../game-vfx/src/effects/world/fog-and-ash.ts","../../game-vfx/src/workshop/rain.ts","../../game-vfx/src/effects/world/rain.ts","../../game-vfx/src/effects/vehicles/car.ts","../../game-vfx/src/effects/vehicles/headlights.ts","../../game-vfx/src/effects/vehicles/scraping-sparks.ts","../../game-vfx/src/effects/vehicles/engine-smoke.ts","../../game-vfx/src/effects/vehicles/boom.ts","../../game-vfx/src/effects/vehicles/fuel-leak.ts","../../game-vfx/src/effects/vehicles/car-explosion.ts","../../game-vfx/src/effects/vehicles/windshield.ts","../../game-vfx/src/effects/screen/damage-and-bite.ts","../../game-vfx/src/effects/screen/infection-phases.ts","../../game-vfx/src/effects/screen/panic.ts","../../game-vfx/src/effects/screen/bloater-blindness.ts","../../game-vfx/src/effects/screen/campfire.ts","../../game-vfx/src/effects/screen/cough-and-breath.ts","../../game-vfx/src/effects/events.ts","../../game-vfx/src/effects/weapons/viewmodel.ts","../../game-vfx/src/effects/vehicles/game-effects.ts","../../game-vfx/src/effects/catalog.ts","../../game-vfx/src/random.ts","../../game-vfx/src/effects/builders/impact-table.ts","../../game-vfx/src/effects/builders/blood.ts","../../game-vfx/src/effects/behaviors/blocks.ts","../../game-vfx/src/effects/behaviors/car.ts","../../game-vfx/src/effects/behaviors/cracks.ts","../../game-vfx/src/effects/builders/shared.ts","../../game-vfx/src/effects/behaviors/cylinder.ts","../../game-vfx/src/effects/behaviors/plate.ts","../../game-vfx/src/effects/behaviors/rain.ts","../../game-vfx/src/effects/behaviors/walkers.ts","../../game-vfx/src/effects/behaviors.ts","../../game-vfx/src/schedule.ts","../../game-vfx/src/effects/expression.ts","../../game-vfx/src/effects/builders/fire.ts","../../game-vfx/src/effects/builders/weapon.ts","../../game-vfx/src/effects/macros.ts","../../game-vfx/src/effects/instance.ts","../../game-vfx/src/effects/player.ts","../../game-vfx/src/lights.ts","../../game-vfx/src/lod.ts","../../game-vfx/src/marks.ts","../../game-vfx/src/pools/rings.ts","../../game-vfx/src/pools/shocks.ts","../../game-vfx/src/explosion.ts","../../game-vfx/src/pools/chunk-motion.ts","../../game-vfx/src/screen.ts","../../game-vfx/src/sprites/kind-pool.ts","../../game-vfx/src/data/sprite-kinds.json","../../game-vfx/src/sprites/kinds.ts","../../game-vfx/src/sprites/system.ts","../../game-vfx/src/workshop/direction.ts","../../game-vfx/src/workshop/debris.ts","../../game-vfx/src/workshop/decals.ts","../../game-vfx/src/workshop/meshes.ts","../../game-vfx/src/workshop/sprites.ts","../../game-vfx/src/workshop/streaks.ts","../../game-vfx/src/workshop/waves.ts","../../game-vfx/src/workshop/engine.ts","../../game-vfx/src/types.ts","../../game-vfx/src/world.ts","../../game-vfx/src/data/event-effects.json","../../game-vfx/src/effects/event-map.ts","../../game-vfx/src/locales/en.ts","../../game-vfx/src/locales/pt-BR.ts","../../game-vfx/src/locale.ts","../../game-vfx/src/effects/camera.ts","../../game-vfx/src/effects/runner.ts","../../game-vfx/src/ground.ts","../src/rain-near.ts","../src/rain-curtain.ts","../src/renderer.ts","../../game-contracts/src/blast-shove.ts","../../game-contracts/src/impact.ts","../../game-contracts/src/body.ts","../../game-contracts/src/spec.ts","../../game-contracts/src/weapons.ts","../../game-contracts/src/ignition.ts","../../game-contracts/src/animation.ts","../../game-contracts/src/tool-animation.ts","../../game-contracts/src/registry.ts","../../game-contracts/src/bench.ts","../../game-contracts/src/environment.ts","../../game-contracts/src/categories.ts","../../game-contracts/src/origin.ts","../../game-contracts/src/footprint.ts","../src/bench/text.ts","../src/bench/visual.ts"],"sourcesContent":["/**\n * @adatechnology/game-weather-three: the picture of `@adatechnology/game-weather` in three.js. The game's rain curtain (14 000 GPU points in a 70 m box), the near rain\n * and its splashes and rings, mist and ash on the pools of `@adatechnology/game-vfx`, linear and exponential fog with zone fog, the sky that dims in rain, lightning,\n * headlights in rain and a heat haze, plus the layers a weather bench shows.\n */\nexport { createWeatherRenderer, WEATHER_LAYERS } from './renderer'\nexport type { WeatherLayerId, WeatherRenderer, WeatherRendererOptions } from './renderer'\nexport { createRainCurtain, rainCurtainPositions, RAIN_CURTAIN, RAIN_CURTAIN_FRAGMENT, RAIN_CURTAIN_FULL_AT, RAIN_CURTAIN_PROFILES, RAIN_CURTAIN_VERTEX } from './rain-curtain'\nexport type { RainCurtain, RainCurtainOptions } from './rain-curtain'\nexport { createNearRain, SPLASH_FROM_MM } from './rain-near'\nexport type { NearRain, NearRainOptions } from './rain-near'\nexport { ASH, createFog, densityForFar, emitAirborne, FOG_EXP_FROM, FOG_NEAR, FOG_ZONE_NEAR, fogOf, MIST } from './fog'\nexport type { Fog, FogValues } from './fog'\nexport { createSky, SKY_DAY, SKY_NIGHT, ZONE_FOG_DEFAULTS, ZONE_LIGHT } from './sky'\nexport type { Sky } from './sky'\nexport { createLightning, LIGHTNING } from './lightning'\nexport type { Lightning } from './lightning'\nexport { createHeadlights, defaultHeadlightRule, HEADLIGHTS } from './headlights'\nexport type { HeadlightRules, Headlights } from './headlights'\nexport { createHeatHaze, HEAT_HAZE, HEAT_HAZE_FRAGMENT, HEAT_HAZE_VERTEX } from './heat-haze'\nexport type { HeatHaze } from './heat-haze'\nexport { createWeatherVisual } from './bench/visual'\nexport type { RigVehicles, WeatherVisualOptions } from './bench/visual'\nexport { weatherVisualTextOf } from './bench/text'\nexport type { WeatherVisualText } from './bench/text'\nexport type { ThreeLib, Vec3, WeatherProfileName, WeatherSource, ZoneFog } from './types'\n","import type * as THREE_NS from 'three'\nimport type { WeatherSample } from '@adatechnology/game-weather'\nimport type { ThreeLib, ZoneFog } from './types'\n\n/** The colours of the game's `skyUpdate` (jogo.js `_skyN` and `_skyD`): the sky goes from the night one to the day one with the sun height. */\nexport const SKY_NIGHT = 0x0b0f18\nexport const SKY_DAY = 0x8796a6\n/** The zone fog colour is lit by `0.35 + 0.65 s` (the page's `ZONA_NEBLINA` blend). */\nexport const ZONE_LIGHT = { base: 0.35, sun: 0.65 } as const\n/** The page's zone fog (`ZONA_NEBLINA`): colour and far distance; its weight is the game's to set. */\nexport const ZONE_FOG_DEFAULTS = { color: 0x3a4a3a, far: 60 } as const\n\nexport type Sky = {\n /** The sky colour for this weather: night to day by `sample.sun`, dimmed by `sample.skyScale` (the page's 0.8 in rain), pulled to the zone colour by its weight. */\n update(sample: WeatherSample, zone?: ZoneFog): THREE_NS.Color\n readonly color: THREE_NS.Color\n}\n\n/** Writes the sky into `scene.background` (a `Color`; made when the scene has none) and hands the colour to the fog. */\nexport function createSky(THREE: ThreeLib, scene: { background: unknown }): Sky {\n const night = new THREE.Color(SKY_NIGHT)\n const day = new THREE.Color(SKY_DAY)\n const zoneColor = new THREE.Color(ZONE_FOG_DEFAULTS.color)\n const tint = new THREE.Color()\n const color = new THREE.Color()\n return {\n color,\n update(sample, zone) {\n color.lerpColors(night, day, sample.sun)\n color.multiplyScalar(sample.skyScale)\n if (zone && zone.weight > 0) {\n zoneColor.setHex(zone.color ?? ZONE_FOG_DEFAULTS.color)\n color.lerp(tint.copy(zoneColor).multiplyScalar(ZONE_LIGHT.base + sample.sun * ZONE_LIGHT.sun), zone.weight)\n }\n const background = scene.background as THREE_NS.Color | null | undefined\n if (background && (background as { isColor?: boolean }).isColor) background.copy(color)\n else scene.background = color.clone()\n return color\n },\n }\n}\n","import type * as THREE_NS from 'three'\nimport type { WeatherSample } from '@adatechnology/game-weather'\nimport type { WorkshopEngine } from '@adatechnology/game-vfx'\nimport { ZONE_FOG_DEFAULTS } from './sky'\nimport type { ThreeLib, Vec3, ZoneFog } from './types'\n\n/** The page's linear fog starts at 25 m and, in the zone, at 4 m (`scene.fog.near = lerp(25, 4, weight)`). */\nexport const FOG_NEAR = 25\nexport const FOG_ZONE_NEAR = 4\n/** Below this exponential-squared density the weather uses the linear fog (the clear weathers have 0). */\nexport const FOG_EXP_FROM = 0.0005\n/** exp2 density that sees as far as a linear fog of `far` metres (2 % left): used to blend the zone fog in the exponential case. */\nexport const densityForFar = (far: number): number => Math.sqrt(Math.log(50)) / far\n\nexport type FogValues = { readonly mode: 'linear'; readonly near: number; readonly far: number } | { readonly mode: 'exp2'; readonly density: number }\n\n/** The fog the weather and the zone ask for. Pure. */\nexport function fogOf(sample: WeatherSample, zone?: ZoneFog, expFrom: number = FOG_EXP_FROM): FogValues {\n const weight = zone?.weight ?? 0\n const zoneFar = zone?.far ?? ZONE_FOG_DEFAULTS.far\n if (sample.fogDensity >= expFrom) return { mode: 'exp2', density: sample.fogDensity + (densityForFar(zoneFar) - sample.fogDensity) * weight }\n return { mode: 'linear', near: FOG_NEAR + (FOG_ZONE_NEAR - FOG_NEAR) * weight, far: sample.fogFar + (zoneFar - sample.fogFar) * weight }\n}\n\nexport type Fog = {\n /** Puts the weather's fog on the scene (the page's `fog.far` and `fog.near`, or the exponential-squared one) with the sky's colour. */\n update(sample: WeatherSample, sky: THREE_NS.Color, zone?: ZoneFog): void\n /** Takes the fog off the scene (a layer switched off). */\n clear(): void\n}\n\nexport function createFog(THREE: ThreeLib, scene: { fog: unknown }, options: { readonly expFrom?: number } = {}): Fog {\n // the fog the host had before: it comes back when the layer is switched off (the first update changes it in place, so a copy is kept)\n const original = scene.fog\n const kept = original && (original as { isFog?: boolean }).isFog ? (original as THREE_NS.Fog).clone() : original\n return {\n update(sample, sky, zone) {\n const values = fogOf(sample, zone, options.expFrom)\n const current = scene.fog as (THREE_NS.Fog & THREE_NS.FogExp2 & { isFog?: boolean; isFogExp2?: boolean }) | null | undefined\n if (values.mode === 'linear') {\n if (current?.isFog) {\n current.color.copy(sky)\n current.near = values.near\n current.far = values.far\n } else scene.fog = new THREE.Fog(sky.getHex(), values.near, values.far)\n } else if (current?.isFogExp2) {\n current.color.copy(sky)\n current.density = values.density\n } else scene.fog = new THREE.FogExp2(sky.getHex(), values.density)\n const fog = scene.fog as { color: THREE_NS.Color }\n fog.color.copy(sky)\n },\n clear() {\n scene.fog = kept && (kept as { isFog?: boolean }).isFog ? (kept as THREE_NS.Fog).clone() : (kept ?? null)\n },\n }\n}\n\n/** The Workshop's mist (W1073: low, slow, 3 to 5 m wide) and ash (fine, falling in a spiral), in a box that follows the camera. */\nexport const MIST = { box: { x: [-14, 10], y: [0.2, 1.1], z: [-3, 8] }, drift: [0.15, 0.35], life: [9, 13], size: [3, 5], grow: 1.4, color: 0x7d8aa0, alpha: 0.13, fadeIn: 0.35 } as const\nexport const ASH = { box: { x: [-10, 8], y: [4, 6], z: [-3, 7] }, fall: -0.35, life: [8, 12], size: [0.025, 0.04], color: 0xa09a90, alpha: 0.75, fadeIn: 0.1 } as const\n\n/**\n * Feeds the `normal` sprite pool of the Workshop with `mistPerSecond` puffs of mist and `ashPerSecond` flakes of ash a second (the page's `emit('fog', ...)`\n * and `emit('ash', ...)`, at the weather's rate instead of 5 and 14), around `camera`. The renderer of game-vfx-three draws them.\n */\nexport function emitAirborne(engine: WorkshopEngine, sample: WeatherSample, dt: number, camera: Vec3): void {\n const { random } = engine\n const around = (box: { readonly x: readonly number[]; readonly y: readonly number[]; readonly z: readonly number[] }): [number, number, number] => [\n camera[0] + random.between(box.x[0]!, box.x[1]!),\n random.between(box.y[0]!, box.y[1]!),\n camera[2] + random.between(box.z[0]!, box.z[1]!),\n ]\n if (sample.mistPerSecond > 0) {\n engine.emit('weather-mist', 'normal', sample.mistPerSecond, dt, () => ({\n p: around(MIST.box),\n v: [random.between(MIST.drift[0], MIST.drift[1]), 0, random.between(-0.05, 0.05)],\n sp: 0,\n life: MIST.life,\n size: MIST.size,\n grow: MIST.grow,\n c0: MIST.color,\n a: MIST.alpha,\n fi: MIST.fadeIn,\n }))\n }\n if (sample.ashPerSecond > 0) {\n engine.emit('weather-ash', 'normal', sample.ashPerSecond, dt, () => ({\n p: around(ASH.box),\n v: [random.between(0.1, 0.4), ASH.fall, random.between(-0.1, 0.1)],\n sp: [0.05, 0.15],\n life: ASH.life,\n size: ASH.size,\n c0: ASH.color,\n a: ASH.alpha,\n fi: ASH.fadeIn,\n gnd: 1,\n }))\n }\n}\n","import type * as THREE_NS from 'three'\nimport type { WorkshopEngine } from '@adatechnology/game-vfx'\nimport type { WeatherSample } from '@adatechnology/game-weather'\nimport type { ThreeLib, Vec3 } from './types'\n\n/** The Workshop's car lamps (W101-103) and the rain that glows inside their beam (W1161-1170). */\nexport const HEADLIGHTS = {\n spot: { color: 0xfff1d0, intensity: 2.4, distance: 26, angle: 0.33, penumbra: 0.5, decay: 1.6 },\n /** Lamp position on the car (x is mirrored), where the spot points, and the cone drawn in the air. */\n lamp: [0.65, 0.74, 2.15],\n aim: [0.65, 0.2, 12],\n cone: { radius: 2.4, length: 9, segments: 18, color: 0xfff1d0, opacity: 0.06 },\n lens: { on: 0xfff1d0, off: 0x3a3a34 },\n /** Lit drops a second, thrown into the beam 1 to 13 m ahead (W1176). */\n lit: { rate: 600, depth: [1, 13], spread: 0.33, color: 0xcfdcf0, alpha: 0.9, life: 0.06, len: 0.035, velocity: [0.8, -14, 0] },\n /** Beyond this many metres a car keeps only the cone: no real light (W-b text of `farol`). */\n lightRange: 40,\n} as const\n\nexport type HeadlightRules = {\n /** Whether the lamps are lit in this weather and light (default: night, rain or fog). */\n readonly lit?: (sample: WeatherSample) => boolean\n readonly lightRange?: number\n}\n\n/** Night (the sun below the page's 0.3 night factor), rain or fog make a driver turn the lamps on. */\nexport const defaultHeadlightRule = (sample: WeatherSample): boolean => sample.nightFactor > 0.3 || sample.rain > 0 || sample.fogDensity > 0.02 || sample.visibility < 150\n\ntype Rig = { readonly car: THREE_NS.Object3D; readonly spots: THREE_NS.SpotLight[]; readonly cones: THREE_NS.Mesh[]; readonly lens: THREE_NS.MeshBasicMaterial | undefined; readonly targets: THREE_NS.Object3D[] }\n\nexport type Headlights = {\n /** Gives `car` its two spot lights and two beam cones (`lens`, when given, is the material of the lamp glass, lit with them). Returns the function that takes them off. */\n attach(car: THREE_NS.Object3D, lens?: THREE_NS.MeshBasicMaterial): () => void\n /** Lights the lamps of the cars attached, the real light only within range of `camera`; throws the lit drops of the beams into the Workshop's streak pool. */\n update(sample: WeatherSample, dt: number, camera: Vec3, engine?: WorkshopEngine, enabled?: boolean): void\n count(): number\n dispose(): void\n}\n\nexport function createHeadlights(THREE: ThreeLib, rules: HeadlightRules = {}): Headlights {\n const rigs = new Set<Rig>()\n const cone = HEADLIGHTS.cone\n const lampWorld = new THREE.Vector3()\n const aimWorld = new THREE.Vector3()\n const forward = new THREE.Vector3()\n const rule = rules.lit ?? defaultHeadlightRule\n const range = rules.lightRange ?? HEADLIGHTS.lightRange\n\n function attach(car: THREE_NS.Object3D, lens?: THREE_NS.MeshBasicMaterial): () => void {\n const spots: THREE_NS.SpotLight[] = []\n const cones: THREE_NS.Mesh[] = []\n const targets: THREE_NS.Object3D[] = []\n for (const side of [-1, 1]) {\n const spot = new THREE.SpotLight(HEADLIGHTS.spot.color, 0, HEADLIGHTS.spot.distance, HEADLIGHTS.spot.angle, HEADLIGHTS.spot.penumbra, HEADLIGHTS.spot.decay)\n spot.position.set(side * HEADLIGHTS.lamp[0], HEADLIGHTS.lamp[1], HEADLIGHTS.lamp[2])\n const target = new THREE.Object3D()\n target.position.set(side * HEADLIGHTS.aim[0], HEADLIGHTS.aim[1], HEADLIGHTS.aim[2])\n car.add(spot, target)\n spot.target = target\n const geometry = new THREE.ConeGeometry(cone.radius, cone.length, cone.segments, 1, true)\n geometry.translate(0, -cone.length / 2, 0)\n geometry.rotateX(-Math.PI / 2)\n const material = new THREE.MeshBasicMaterial({ color: cone.color, transparent: true, opacity: cone.opacity, blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide, fog: false })\n const beam = new THREE.Mesh(geometry, material)\n beam.position.set(side * HEADLIGHTS.lamp[0], HEADLIGHTS.lamp[1], HEADLIGHTS.lamp[2])\n beam.visible = false\n car.add(beam)\n spots.push(spot)\n cones.push(beam)\n targets.push(target)\n }\n const rig: Rig = { car, spots, cones, lens, targets }\n rigs.add(rig)\n return () => {\n rigs.delete(rig)\n for (const object of [...spots, ...cones, ...targets]) car.remove(object)\n for (const beam of cones) {\n beam.geometry.dispose()\n ;(beam.material as THREE_NS.Material).dispose()\n }\n for (const spot of spots) spot.dispose()\n lens?.color.setHex(HEADLIGHTS.lens.off)\n }\n }\n\n return {\n attach,\n update(sample, dt, camera, engine, enabled = true) {\n const on = enabled && rule(sample)\n let index = 0\n for (const rig of rigs) {\n rig.car.updateWorldMatrix(true, false)\n lampWorld.set(0, HEADLIGHTS.lamp[1], HEADLIGHTS.lamp[2]).applyMatrix4(rig.car.matrixWorld)\n const near = Math.hypot(lampWorld.x - camera[0], lampWorld.z - camera[2]) <= range\n for (const spot of rig.spots) spot.intensity = on && near ? HEADLIGHTS.spot.intensity : 0\n for (const beam of rig.cones) beam.visible = on\n rig.lens?.color.setHex(on ? HEADLIGHTS.lens.on : HEADLIGHTS.lens.off)\n if (!on || !engine || sample.rain <= 0 || !near) {\n index++\n continue\n }\n aimWorld.set(0, HEADLIGHTS.lamp[1], HEADLIGHTS.lamp[2] + 1).applyMatrix4(rig.car.matrixWorld)\n forward.copy(aimWorld).sub(lampWorld).normalize()\n const side: [number, number] = [forward.z, -forward.x]\n const { random } = engine\n const lit = HEADLIGHTS.lit\n engine.emit(`weather-lit-${index}`, 'streak', lit.rate, dt, () => {\n const depth = random.between(lit.depth[0], lit.depth[1])\n const spread = depth * lit.spread * Math.sqrt(random.next())\n const turn = random.between(0, 6.283)\n return {\n p: [lampWorld.x + forward.x * depth + side[0] * Math.cos(turn) * spread, lampWorld.y + forward.y * depth + Math.sin(turn) * spread * 0.6, lampWorld.z + forward.z * depth + side[1] * Math.cos(turn) * spread],\n v: lit.velocity as unknown as [number, number, number],\n sp: 0,\n life: lit.life,\n len: lit.len,\n c0: lit.color,\n a: lit.alpha,\n }\n })\n index++\n }\n },\n count: () => rigs.size,\n dispose() {\n for (const rig of [...rigs]) {\n rigs.delete(rig)\n for (const object of [...rig.spots, ...rig.cones, ...rig.targets]) rig.car.remove(object)\n }\n },\n }\n}\n","import type * as THREE_NS from 'three'\nimport type { WeatherSample } from '@adatechnology/game-weather'\nimport type { ThreeLib } from './types'\n\n/** Peak opacity of the shimmer at full haze, and how fast the bands rise (SDK-made: no team delivered heat haze, see the open question). */\nexport const HEAT_HAZE = { opacity: 0.09, bands: 38, rise: 1.4, color: [1, 0.93, 0.82] } as const\n\nexport const HEAT_HAZE_VERTEX = 'varying vec2 vUv; void main(){ vUv = uv; gl_Position = vec4(position.xy, 0., 1.); }'\nexport const HEAT_HAZE_FRAGMENT =\n 'uniform float uTime; uniform float uHaze; uniform vec3 uColor; varying vec2 vUv; void main(){ float low = 1. - smoothstep(0.15, 0.7, vUv.y); float wave = sin(vUv.y*38. - uTime*1.4 + sin(vUv.x*11. + uTime*.7)*2.2) * .5 + .5; gl_FragColor = vec4(uColor, uHaze*low*wave*.09); }'\n\nexport type HeatHaze = {\n readonly object: THREE_NS.Mesh\n update(sample: WeatherSample, time: number): void\n dispose(): void\n}\n\n/**\n * The heat shimmer of `extreme_heat` (`WeatherSample.haze`, 0..1): a screen-space layer of warm bands that rise over the lower half of the view. It is a layer, not a\n * distortion of the picture, because bending the picture needs a render target that only the host owns.\n */\nexport function createHeatHaze(THREE: ThreeLib): HeatHaze {\n const uniforms = { uTime: { value: 0 }, uHaze: { value: 0 }, uColor: { value: new THREE.Vector3(...HEAT_HAZE.color) } }\n const material = new THREE.ShaderMaterial({ uniforms, vertexShader: HEAT_HAZE_VERTEX, fragmentShader: HEAT_HAZE_FRAGMENT, transparent: true, depthTest: false, depthWrite: false })\n const object = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material)\n object.frustumCulled = false\n object.renderOrder = 999\n object.name = 'weather-heat-haze'\n object.visible = false\n return {\n object,\n update(sample, time) {\n uniforms.uHaze.value = sample.haze\n uniforms.uTime.value = time\n object.visible = sample.haze > 0\n },\n dispose() {\n object.geometry.dispose()\n material.dispose()\n },\n }\n}\n","import type * as THREE_NS from 'three'\nimport type { VfxWorld } from '@adatechnology/game-vfx'\nimport type { WeatherSample } from '@adatechnology/game-weather'\nimport type { ThreeLib } from './types'\n\n/**\n * What the Workshop's double flash does to the scene (W1072): the hemisphere light gains `I * 3.2`, the background gains `I * (.25, .27, .32)` and the screen goes\n * white by `I * .12`, where `I` is the flash the weather samples (1, then .2, then .8 for 0.26 s).\n */\nexport const LIGHTNING = { light: 3.2, background: [0.25, 0.27, 0.32], white: 0.12, skyColor: 0x3a4c6a, groundColor: 0x0b0b0c } as const\n\nexport type Lightning = {\n readonly light: THREE_NS.HemisphereLight\n /**\n * Lights the scene with the flash of `sample`. `background`, when given, is the sky just written for this frame (the renderer passes it only when it owns the sky),\n * and gains the flash; a background the host keeps for itself is left alone.\n */\n update(sample: WeatherSample, background?: THREE_NS.Color): void\n dispose(): void\n}\n\nexport function createLightning(THREE: ThreeLib, scene: { add(object: THREE_NS.Object3D): unknown; remove(object: THREE_NS.Object3D): unknown }, vfx?: VfxWorld): Lightning {\n const light = new THREE.HemisphereLight(LIGHTNING.skyColor, LIGHTNING.groundColor, 0)\n light.name = 'weather-lightning'\n scene.add(light)\n return {\n light,\n update(sample, background) {\n const flash = sample.flash\n light.intensity = flash * LIGHTNING.light\n if (flash <= 0) return\n if (background) {\n background.r += flash * LIGHTNING.background[0]!\n background.g += flash * LIGHTNING.background[1]!\n background.b += flash * LIGHTNING.background[2]!\n }\n const screen = vfx?.workshop().screen\n if (screen) screen.white = Math.max(screen.white, flash * LIGHTNING.white)\n },\n dispose() {\n scene.remove(light)\n light.dispose()\n },\n }\n}\n","import type { AreaCap } from './types'\n\n/** The game's cap (G68194-68202): from the 7th explosion within 40 m in 4 s, the extra ones are made at 0.3. */\nexport const AREA_CAP_GAME: AreaCap = { radius: 40, window: 4, count: 6, excessScale: 0.3 }\n\nexport interface AreaCapTracker {\n /** Registers an explosion at (`x`, `z`) at `time` seconds and answers the scale it gets: 1, or the cap's `excessScale` when `count` or more are already recent in the area. */\n register(time: number, x: number, z: number): number\n clear(): void\n}\n\ninterface Recent {\n time: number\n x: number\n z: number\n}\n\nexport function createAreaCap(cap: AreaCap): AreaCapTracker {\n const recent: Recent[] = []\n const spare: Recent[] = []\n const radiusSquared = cap.radius * cap.radius\n return {\n register(time, x, z) {\n let kept = 0\n for (const entry of recent) {\n if (time - entry.time < cap.window) recent[kept++] = entry\n else spare.push(entry)\n }\n recent.length = kept\n let near = 0\n for (const entry of recent) if ((entry.x - x) ** 2 + (entry.z - z) ** 2 < radiusSquared) near++\n const entry = spare.pop() ?? { time: 0, x: 0, z: 0 }\n entry.time = time\n entry.x = x\n entry.z = z\n recent.push(entry)\n return near >= cap.count ? cap.excessScale : 1\n },\n clear() {\n for (const entry of recent) spare.push(entry)\n recent.length = 0\n },\n }\n}\n","import type { ExplosionDef, VfxProfile, VfxProfileName } from './types'\n\n/**\n * The pool sizes of the three profiles. `high` is the bench and the goldens (the Lab's caps L131/240/295/367 and the Workshop's W221), `medium` the\n * game's default (G67747/67762), `low` close to the game's own budgets (G68074, G68108).\n */\nexport const PROFILES: Readonly<Record<VfxProfileName, VfxProfile>> = {\n high: { puffs: 2600, sparks: 3200, glows: 320, chunks: 300, sprites: { add: 9000, normal: 9000 }, streaks: 3000, debris: 500 },\n medium: { puffs: 1300, sparks: 1600, glows: 160, chunks: 150, sprites: { add: 1500, normal: 600 }, streaks: 1000, debris: 120 },\n low: { puffs: 260, sparks: 800, glows: 80, chunks: 120, sprites: { add: 600, normal: 300 }, streaks: 400, debris: 60 },\n}\n\n/** The Lab's per-event meters (L1043-1044): 400 GPU particles, 40 physics debris, and 4 + 2 lights. */\nexport const EVENT_BUDGET = { particles: 400, debris: 40, blastLights: 4, fireLights: 2 } as const\n\n/** Particles an explosion makes on the GPU, the number of the Lab's effect sheet (L1033): fireballs, smoke, dust, shrapnel, the mushroom and the embers. */\nexport function estimateCost(def: ExplosionDef): number {\n const mushroom = def.mushroom ? def.mushroom.count + def.mushroom.stem : 0\n return def.fireballs + def.smoke.count + def.dust.count + def.shrapnel.count + mushroom + (def.shrapnel.embers ?? 0)\n}\n","{\n \"carl_gustaf_84mm\": \"at4_84mm\",\n \"vog_25\": \"m79_40mm\"\n}\n","{\n \"m67\": {\n \"id\": \"m67\",\n \"lethal\": 5,\n \"injury\": 15,\n \"visualRadius\": 1.3,\n \"fireballs\": 16,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 6972767,\n \"count\": 18,\n \"life\": 5.5\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 30\n },\n \"shrapnel\": {\n \"count\": 150,\n \"kind\": 0,\n \"speed\": 34\n },\n \"chunks\": {\n \"count\": 14,\n \"colors\": [\n 3881528,\n 2894634,\n 4867650\n ],\n \"hot\": 0.25\n },\n \"mark\": {\n \"scorch\": 1.7,\n \"crater\": 0.9,\n \"dent\": 0.25\n },\n \"flash\": {\n \"light\": 45,\n \"life\": 0.45,\n \"color\": 16752720,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.1,\n \"push\": 9,\n \"sources\": [\n \"lab:L796\",\n \"lab:L793\"\n ]\n },\n \"molotov\": {\n \"id\": \"molotov\",\n \"lethal\": 0,\n \"injury\": 4,\n \"visualRadius\": 1.1,\n \"fireballs\": 24,\n \"fuel\": true,\n \"heat\": 0.95,\n \"heatLifeScale\": 1.5,\n \"smoke\": {\n \"color\": 3814449,\n \"count\": 3,\n \"life\": 3.5\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 0\n },\n \"shrapnel\": {\n \"count\": 0,\n \"kind\": 0,\n \"speed\": 28\n },\n \"chunks\": {\n \"count\": 0,\n \"colors\": [\n 3881528,\n 2894634,\n 4867650\n ],\n \"hot\": 0.3\n },\n \"mark\": {\n \"scorch\": 2.4\n },\n \"flash\": {\n \"light\": 22,\n \"life\": 1.2,\n \"color\": 16747066,\n \"intensity\": 6\n },\n \"shock\": false,\n \"shake\": 0.25,\n \"push\": 0,\n \"ignite\": 4.2,\n \"sources\": [\n \"lab:L797\",\n \"lab:L793\"\n ]\n },\n \"pipe_bomb\": {\n \"id\": \"pipe_bomb\",\n \"lethal\": 3,\n \"injury\": 10,\n \"visualRadius\": 1,\n \"fireballs\": 8,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 11841704,\n \"count\": 26,\n \"life\": 6,\n \"kind\": 3\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 22\n },\n \"shrapnel\": {\n \"count\": 220,\n \"kind\": 1,\n \"speed\": 30,\n \"width\": 0.03\n },\n \"chunks\": {\n \"count\": 8,\n \"colors\": [\n 5132886,\n 3816768\n ],\n \"plate\": true,\n \"hot\": 0.2\n },\n \"mark\": {\n \"scorch\": 1.3,\n \"crater\": 0.7,\n \"dent\": 0.18\n },\n \"flash\": {\n \"light\": 30,\n \"life\": 0.45,\n \"color\": 16752720,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1,\n \"push\": 7,\n \"sources\": [\n \"lab:L798\",\n \"lab:L793\"\n ]\n },\n \"propane_p13\": {\n \"id\": \"propane_p13\",\n \"lethal\": 5,\n \"injury\": 14,\n \"visualRadius\": 2.2,\n \"fireballs\": 34,\n \"fuel\": true,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 3814449,\n \"count\": 10,\n \"life\": 8\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 26\n },\n \"shrapnel\": {\n \"count\": 70,\n \"kind\": 0,\n \"speed\": 28,\n \"embers\": 30\n },\n \"chunks\": {\n \"count\": 8,\n \"colors\": [\n 3891098,\n 2835824\n ],\n \"plate\": true,\n \"hot\": 0.5\n },\n \"mark\": {\n \"scorch\": 2.6\n },\n \"flash\": {\n \"light\": 70,\n \"life\": 1.1,\n \"color\": 16751168,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.4,\n \"push\": 8,\n \"mushroom\": {\n \"height\": 7,\n \"radius\": 2.6,\n \"count\": 36,\n \"stem\": 16,\n \"life\": 8\n },\n \"residualFires\": [\n {\n \"radius\": 1.4,\n \"life\": 7,\n \"rate\": 20\n }\n ],\n \"ignite\": 5,\n \"hotParts\": true,\n \"sources\": [\n \"lab:L799\",\n \"lab:L793\"\n ]\n },\n \"dynamite\": {\n \"id\": \"dynamite\",\n \"lethal\": 4,\n \"injury\": 12,\n \"visualRadius\": 1.4,\n \"fireballs\": 12,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 8019006,\n \"count\": 24,\n \"life\": 7\n },\n \"dust\": {\n \"color\": 8019006,\n \"count\": 46\n },\n \"shrapnel\": {\n \"count\": 90,\n \"kind\": 0,\n \"speed\": 28\n },\n \"chunks\": {\n \"count\": 26,\n \"colors\": [\n 6965806,\n 8018490,\n 4863526\n ],\n \"hot\": 0.15,\n \"speed\": 1.2\n },\n \"mark\": {\n \"scorch\": 2.4,\n \"crater\": 1.8,\n \"dent\": 0.7\n },\n \"flash\": {\n \"light\": 55,\n \"life\": 0.45,\n \"color\": 16752720,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.5,\n \"push\": 10,\n \"sources\": [\n \"lab:L800\",\n \"lab:L793\"\n ]\n },\n \"c4\": {\n \"id\": \"c4\",\n \"lethal\": 6,\n \"injury\": 18,\n \"visualRadius\": 1.6,\n \"fireballs\": 20,\n \"heat\": 1.25,\n \"smoke\": {\n \"color\": 6972767,\n \"count\": 22,\n \"life\": 7\n },\n \"dust\": {\n \"color\": 9407106,\n \"count\": 50\n },\n \"shrapnel\": {\n \"count\": 200,\n \"kind\": 0,\n \"speed\": 42\n },\n \"chunks\": {\n \"count\": 32,\n \"colors\": [\n 9078140,\n 7301730,\n 3881528\n ],\n \"hot\": 0.3,\n \"speed\": 1.4\n },\n \"mark\": {\n \"scorch\": 3,\n \"crater\": 2.3,\n \"dent\": 0.9\n },\n \"flash\": {\n \"light\": 90,\n \"life\": 0.35,\n \"color\": 16773328,\n \"intensity\": 9\n },\n \"shock\": {\n \"duration\": 0.28\n },\n \"shake\": 2.2,\n \"push\": 13,\n \"sources\": [\n \"lab:L801\",\n \"lab:L793\"\n ]\n },\n \"claymore\": {\n \"id\": \"claymore\",\n \"lethal\": 15,\n \"injury\": 50,\n \"visualRadius\": 0.7,\n \"fireballs\": 6,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 6972767,\n \"count\": 8,\n \"life\": 4\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 34\n },\n \"shrapnel\": {\n \"count\": 700,\n \"kind\": 1,\n \"speed\": 55,\n \"width\": 0.025\n },\n \"chunks\": {\n \"count\": 6,\n \"colors\": [\n 3881528,\n 2894634,\n 4867650\n ],\n \"hot\": 0.3\n },\n \"mark\": {\n \"scorch\": 0.9\n },\n \"flash\": {\n \"light\": 28,\n \"life\": 0.45,\n \"color\": 16752720,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1,\n \"push\": 8,\n \"cone\": {\n \"angle\": 0,\n \"half\": 0.5235987755982988,\n \"length\": 50\n },\n \"directionBias\": 2,\n \"sources\": [\n \"lab:L802\",\n \"lab:L793\"\n ]\n },\n \"flashbang\": {\n \"id\": \"flashbang\",\n \"lethal\": 0,\n \"injury\": 10,\n \"visualRadius\": 0.5,\n \"fireballs\": 0,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 11841704,\n \"count\": 10,\n \"life\": 5,\n \"kind\": 3\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 0\n },\n \"shrapnel\": {\n \"count\": 60,\n \"kind\": 2,\n \"speed\": 12\n },\n \"chunks\": {\n \"count\": 0,\n \"colors\": [\n 3881528,\n 2894634,\n 4867650\n ],\n \"hot\": 0.3\n },\n \"mark\": {\n \"scorch\": 0.4\n },\n \"flash\": {\n \"light\": 160,\n \"life\": 0.2,\n \"color\": 16054015,\n \"intensity\": 14,\n \"blinding\": true\n },\n \"shock\": false,\n \"shake\": 0.4,\n \"push\": 0,\n \"sources\": [\n \"lab:L803\",\n \"lab:L793\"\n ]\n },\n \"m79_40mm\": {\n \"id\": \"m79_40mm\",\n \"lethal\": 4,\n \"injury\": 12,\n \"visualRadius\": 1.1,\n \"fireballs\": 14,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 6972767,\n \"count\": 14,\n \"life\": 5\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 24\n },\n \"shrapnel\": {\n \"count\": 130,\n \"kind\": 0,\n \"speed\": 28\n },\n \"chunks\": {\n \"count\": 12,\n \"colors\": [\n 3881528,\n 2894634,\n 4867650\n ],\n \"hot\": 0.3\n },\n \"mark\": {\n \"scorch\": 1.5,\n \"crater\": 0.8,\n \"dent\": 0.22\n },\n \"flash\": {\n \"light\": 40,\n \"life\": 0.45,\n \"color\": 16752720,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1,\n \"push\": 8,\n \"sources\": [\n \"lab:L804\",\n \"lab:L793\"\n ]\n },\n \"m72_66mm\": {\n \"id\": \"m72_66mm\",\n \"lethal\": 5,\n \"injury\": 15,\n \"visualRadius\": 1.5,\n \"fireballs\": 22,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 6972767,\n \"count\": 16,\n \"life\": 6\n },\n \"dust\": {\n \"color\": 9407106,\n \"count\": 30\n },\n \"shrapnel\": {\n \"count\": 180,\n \"kind\": 0,\n \"speed\": 40\n },\n \"chunks\": {\n \"count\": 24,\n \"colors\": [\n 9078140,\n 7301730\n ],\n \"hot\": 0.3\n },\n \"mark\": {\n \"scorch\": 2\n },\n \"flash\": {\n \"light\": 60,\n \"life\": 0.45,\n \"color\": 16752720,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.5,\n \"push\": 11,\n \"directionBias\": 1.4,\n \"sources\": [\n \"lab:L805\",\n \"lab:L793\"\n ]\n },\n \"at4_84mm\": {\n \"id\": \"at4_84mm\",\n \"lethal\": 5,\n \"injury\": 16,\n \"visualRadius\": 1.7,\n \"fireballs\": 26,\n \"heat\": 1.15,\n \"smoke\": {\n \"color\": 6972767,\n \"count\": 18,\n \"life\": 6.5\n },\n \"dust\": {\n \"color\": 9407106,\n \"count\": 34\n },\n \"shrapnel\": {\n \"count\": 220,\n \"kind\": 0,\n \"speed\": 46\n },\n \"chunks\": {\n \"count\": 30,\n \"colors\": [\n 9078140,\n 7301730\n ],\n \"hot\": 0.35\n },\n \"mark\": {\n \"scorch\": 2.2,\n \"crater\": 1.4,\n \"dent\": 0.5\n },\n \"flash\": {\n \"light\": 70,\n \"life\": 0.45,\n \"color\": 16752720,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.7,\n \"push\": 12,\n \"directionBias\": 1.6,\n \"sources\": [\n \"lab:L806\",\n \"lab:L793\"\n ]\n },\n \"pg7v\": {\n \"id\": \"pg7v\",\n \"lethal\": 5,\n \"injury\": 16,\n \"visualRadius\": 1.8,\n \"fireballs\": 28,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 6972767,\n \"count\": 18,\n \"life\": 7\n },\n \"dust\": {\n \"color\": 9407106,\n \"count\": 34\n },\n \"shrapnel\": {\n \"count\": 200,\n \"kind\": 0,\n \"speed\": 42,\n \"embers\": 20\n },\n \"chunks\": {\n \"count\": 28,\n \"colors\": [\n 9078140,\n 7301730\n ],\n \"hot\": 0.4\n },\n \"mark\": {\n \"scorch\": 2.2\n },\n \"flash\": {\n \"light\": 70,\n \"life\": 0.45,\n \"color\": 16752720,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.7,\n \"push\": 12,\n \"directionBias\": 1.2,\n \"sources\": [\n \"lab:L807\",\n \"lab:L793\"\n ]\n },\n \"fuel_barrel\": {\n \"id\": \"fuel_barrel\",\n \"lethal\": 4,\n \"injury\": 12,\n \"visualRadius\": 1.9,\n \"fireballs\": 28,\n \"fuel\": true,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 3814449,\n \"count\": 8,\n \"life\": 8\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 22\n },\n \"shrapnel\": {\n \"count\": 60,\n \"kind\": 0,\n \"speed\": 28,\n \"embers\": 24\n },\n \"chunks\": {\n \"count\": 10,\n \"colors\": [\n 10165272,\n 5902864\n ],\n \"plate\": true,\n \"hot\": 0.6\n },\n \"mark\": {\n \"scorch\": 2.4\n },\n \"flash\": {\n \"light\": 60,\n \"life\": 1,\n \"color\": 16748608,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.2,\n \"push\": 8,\n \"mushroom\": {\n \"height\": 6,\n \"radius\": 2.1,\n \"count\": 30,\n \"stem\": 14,\n \"life\": 8\n },\n \"residualFires\": [\n {\n \"dx\": 1.6,\n \"dz\": 0.4,\n \"radius\": 0.9,\n \"life\": 6\n },\n {\n \"dx\": -1.1,\n \"dz\": 1.2,\n \"radius\": 0.7,\n \"life\": 5.5\n },\n {\n \"dx\": -0.4,\n \"dz\": -1.5,\n \"radius\": 0.8,\n \"life\": 6.5\n }\n ],\n \"ignite\": 4,\n \"hotParts\": true,\n \"sources\": [\n \"lab:L808\",\n \"lab:L793\"\n ]\n },\n \"car_fuel_tank\": {\n \"id\": \"car_fuel_tank\",\n \"lethal\": 5,\n \"injury\": 14,\n \"visualRadius\": 2.3,\n \"fireballs\": 36,\n \"fuel\": true,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 3814449,\n \"count\": 12,\n \"life\": 9\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 30\n },\n \"shrapnel\": {\n \"count\": 90,\n \"kind\": 0,\n \"speed\": 28,\n \"embers\": 36\n },\n \"chunks\": {\n \"count\": 14,\n \"colors\": [\n 3881528,\n 2894634,\n 4867650\n ],\n \"plate\": true,\n \"hot\": 0.5\n },\n \"mark\": {\n \"scorch\": 3.2\n },\n \"flash\": {\n \"light\": 80,\n \"life\": 1.2,\n \"color\": 16748608,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.8,\n \"push\": 9,\n \"mushroom\": {\n \"height\": 7.5,\n \"radius\": 2.8,\n \"count\": 40,\n \"stem\": 16,\n \"life\": 9\n },\n \"ignite\": 5,\n \"hotParts\": true,\n \"sources\": [\n \"lab:L809\",\n \"lab:L793\"\n ]\n },\n \"fuel_pump\": {\n \"id\": \"fuel_pump\",\n \"lethal\": 4,\n \"injury\": 12,\n \"visualRadius\": 1.7,\n \"fireballs\": 24,\n \"fuel\": true,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 3814449,\n \"count\": 6,\n \"life\": 7\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 16\n },\n \"shrapnel\": {\n \"count\": 60,\n \"kind\": 0,\n \"speed\": 28,\n \"embers\": 16\n },\n \"chunks\": {\n \"count\": 12,\n \"colors\": [\n 14209732,\n 2058810,\n 9407106\n ],\n \"plate\": true,\n \"hot\": 0.5\n },\n \"mark\": {\n \"scorch\": 2.4\n },\n \"flash\": {\n \"light\": 55,\n \"life\": 0.9,\n \"color\": 16748608,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.2,\n \"push\": 8,\n \"mushroom\": {\n \"height\": 5,\n \"radius\": 1.8,\n \"count\": 22,\n \"stem\": 10,\n \"life\": 7\n },\n \"residualFires\": [\n {\n \"radius\": 1.1,\n \"life\": 14,\n \"rate\": 22\n }\n ],\n \"ignite\": 4,\n \"hotParts\": true,\n \"sources\": [\n \"lab:L810\",\n \"lab:L793\"\n ]\n },\n \"fuel_station_tank\": {\n \"id\": \"fuel_station_tank\",\n \"lethal\": 12,\n \"injury\": 30,\n \"visualRadius\": 4.5,\n \"fireballs\": 44,\n \"fuel\": true,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 3814449,\n \"count\": 20,\n \"life\": 11\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 46\n },\n \"shrapnel\": {\n \"count\": 160,\n \"kind\": 0,\n \"speed\": 40,\n \"embers\": 60\n },\n \"chunks\": {\n \"count\": 30,\n \"colors\": [\n 9407106,\n 3881528,\n 2058810\n ],\n \"hot\": 0.5\n },\n \"mark\": {\n \"scorch\": 6,\n \"crater\": 4,\n \"dent\": 1.6\n },\n \"flash\": {\n \"light\": 140,\n \"life\": 1.8,\n \"color\": 16748608,\n \"intensity\": 8\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 3,\n \"push\": 14,\n \"mushroom\": {\n \"height\": 16,\n \"radius\": 5.5,\n \"count\": 60,\n \"stem\": 26,\n \"life\": 12,\n \"riseRate\": 0.42\n },\n \"residualFires\": [\n {\n \"radius\": 3.2,\n \"life\": 20,\n \"rate\": 26,\n \"smoke\": 1.4\n },\n {\n \"dx\": 4,\n \"dz\": 3,\n \"radius\": 1.4,\n \"life\": 16\n },\n {\n \"dx\": -4,\n \"dz\": 2,\n \"radius\": 1.2,\n \"life\": 15\n }\n ],\n \"ignite\": 12,\n \"hotParts\": true,\n \"sources\": [\n \"lab:L811\",\n \"lab:L793\"\n ]\n },\n \"generic\": {\n \"id\": \"generic\",\n \"lethal\": 5,\n \"injury\": 15,\n \"visualRadius\": 1.3,\n \"fireballs\": 16,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 6972767,\n \"count\": 18,\n \"life\": 5.5\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 30\n },\n \"shrapnel\": {\n \"count\": 150,\n \"kind\": 0,\n \"speed\": 34\n },\n \"chunks\": {\n \"count\": 14,\n \"colors\": [\n 3881528,\n 2894634,\n 4867650\n ],\n \"hot\": 0.25\n },\n \"mark\": {\n \"scorch\": 1.7,\n \"crater\": 0.9,\n \"dent\": 0.25\n },\n \"flash\": {\n \"light\": 45,\n \"life\": 0.45,\n \"color\": 16752720,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.1,\n \"push\": 9,\n \"sources\": [\n \"lab:L796\",\n \"lab:L793\",\n \"derived: the M67 profile, for a part with no explosive id; the caller scales it by injury / 15\"\n ]\n }\n}\n","import aliasData from './data/explosion-aliases.json'\nimport explosionData from './data/explosions.json'\nimport type { ExplosionDef } from './types'\n\n/**\n * The 16 profiles of the Explosion Lab (`PROF`, L795-812) keyed by the id of the explosive in `game-weapons` (`m67`, `molotov`, `pipe_bomb`,\n * `propane_p13`, `dynamite`, `c4`, `claymore`, `flashbang`, `m79_40mm`, `m72_66mm`, `at4_84mm`, `pg7v`, `fuel_barrel`, `car_fuel_tank`, `fuel_pump`,\n * `fuel_station_tank`), plus `generic` (the M67 for a part with no explosive id: scale it by `injury / 15`). Each one lists the Lab lines it comes from.\n */\nexport const EXPLOSIONS = explosionData as unknown as Readonly<Record<string, ExplosionDef>>\n\n/** Explosives with no profile of their own: the Carl Gustaf blasts like the AT4, the VOG-25 like the M79. */\nexport const EXPLOSION_ALIASES: Readonly<Record<string, string>> = aliasData\n\n/** A copy of a catalogue with some profiles changed (a partial replaces the fields it names) or removed (`null`); a game adds, tunes or drops effects without touching the core. */\nexport function patchExplosions(base: Readonly<Record<string, ExplosionDef>>, changes: Readonly<Record<string, Partial<ExplosionDef> | ExplosionDef | null>>): Record<string, ExplosionDef> {\n const result: Record<string, ExplosionDef> = { ...base }\n for (const [id, change] of Object.entries(changes)) {\n if (change === null) delete result[id]\n else result[id] = { ...(base[id] ?? {}), ...change, id } as ExplosionDef\n }\n return result\n}\n","export type Rgb = readonly [number, number, number]\n\nconst BYTE_MAX = 255\nconst SRGB_LINEAR_SLOPE = 0.0773993808\nconst SRGB_THRESHOLD = 0.04045\n\n/** The channels of `0xRRGGBB` exactly as three.js r128 reads them (no colour-space conversion): the numbers the Lab's shaders and lights were tuned with. */\nexport function hexToRgb(hex: number): Rgb {\n const whole = Math.floor(hex)\n return [((whole >> 16) & BYTE_MAX) / BYTE_MAX, ((whole >> 8) & BYTE_MAX) / BYTE_MAX, (whole & BYTE_MAX) / BYTE_MAX]\n}\n\nexport function srgbChannelToLinear(channel: number): number {\n return channel < SRGB_THRESHOLD ? channel * SRGB_LINEAR_SLOPE : Math.pow(channel * 0.9478672986 + 0.0521327014, 2.4)\n}\n\n/** `new Color(hex).convertSRGBToLinear()` of r128: the smoke, dust and fire colours the puff shader multiplies the light with. Converted once, at spawn. */\nexport function hexToLinearRgb(hex: number): Rgb {\n const [red, green, blue] = hexToRgb(hex)\n return [srgbChannelToLinear(red), srgbChannelToLinear(green), srgbChannelToLinear(blue)]\n}\n\nexport function linearRgbFromRaw(color: Rgb): Rgb {\n return [srgbChannelToLinear(color[0]), srgbChannelToLinear(color[1]), srgbChannelToLinear(color[2])]\n}\n","import { hexToLinearRgb } from './color'\n\n/** The Lab's smoke colours (L211-214), converted to linear once. Colours of an explosion in data are `0xRRGGBB` and go through `hexToLinearRgb` the same way. */\nexport const SMOKE_COLORS = {\n grey: hexToLinearRgb(0x6a655f),\n dark: hexToLinearRgb(0x4a4440),\n black: hexToLinearRgb(0x3a3431),\n brown: hexToLinearRgb(0x7a5c3e),\n dust: hexToLinearRgb(0x7d7368),\n concrete: hexToLinearRgb(0x8f8a82),\n white: hexToLinearRgb(0xb4b0a8),\n blood: hexToLinearRgb(0x5a0b0b),\n bile: hexToLinearRgb(0x7a8a2a),\n gas: hexToLinearRgb(0xe8ecef),\n} as const\n\nexport const DEFAULT_GRAVITY = 9.8\nexport const DEFAULT_WIND = { x: 0.55, y: 0, z: 0.2 } as const\nexport const FRAME_STEP_LIMIT = 0.05\n","/**\n * A pool that keeps its live records in the order they were added, reuses dead records instead of allocating, and compacts in place. When it is full\n * the oldest record goes (the Lab's `P.shift()`); a pool may choose which one instead (the chunks drop a resting one first).\n */\nexport class OrderedPool<T extends object> {\n /** The live records, oldest first. */\n readonly items: T[] = []\n private readonly spare: T[] = []\n\n constructor(\n readonly capacity: number,\n private readonly create: () => T,\n private readonly release?: (item: T) => void,\n ) {}\n\n get count(): number {\n return this.items.length\n }\n\n /** A record to fill in. At capacity the record `victim` picks (default: the oldest) is dropped and reused. */\n acquire(victim?: (items: readonly T[]) => number): T {\n if (this.items.length >= this.capacity) this.removeAt(victim ? victim(this.items) : 0)\n const record = this.spare.pop() ?? this.create()\n this.items.push(record)\n return record\n }\n\n removeAt(index: number): void {\n const record = this.items[index]\n if (record === undefined) return\n for (let shifted = index; shifted < this.items.length - 1; shifted++) this.items[shifted] = this.items[shifted + 1]!\n this.items.length -= 1\n this.release?.(record)\n this.spare.push(record)\n }\n\n /** Keeps the records `keep` accepts, in the same order, without allocating. `context` goes to `keep` so no closure is needed per frame. */\n compact<TContext>(keep: (item: T, context: TContext) => boolean, context: TContext): void {\n let write = 0\n for (let read = 0; read < this.items.length; read++) {\n const record = this.items[read]!\n if (keep(record, context)) {\n this.items[write++] = record\n continue\n }\n this.release?.(record)\n this.spare.push(record)\n }\n this.items.length = write\n }\n\n clear(): void {\n for (const record of this.items) {\n this.release?.(record)\n this.spare.push(record)\n }\n this.items.length = 0\n }\n}\n","import type { Rgb } from '../color'\nimport { OrderedPool } from '../pool'\nimport type { RandomStream } from '../random'\nimport type { Glow } from './glows'\n\n/** Debris with embers (L366-400). The last block is what the renderer reads: the colour already blended towards the glow of a hot piece. */\nexport interface Chunk {\n x: number\n y: number\n z: number\n vx: number\n vy: number\n vz: number\n rx: number\n ry: number\n rz: number\n ax: number\n ay: number\n az: number\n sx: number\n sy: number\n sz: number\n color: Rgb\n plate: boolean\n hot: boolean\n rest: boolean\n blood: boolean\n splat: boolean\n t: number\n trail: number\n glow: Glow | null\n rng: RandomStream\n outColor: [number, number, number]\n}\n\nexport type ChunkInit = Partial<Pick<Chunk, 'x' | 'y' | 'z' | 'vx' | 'vy' | 'vz' | 'sx' | 'sy' | 'sz' | 'color' | 'plate' | 'hot' | 'blood'>>\n\nconst DEFAULT_COLOR: Rgb = [0x55 / 255, 0x55 / 255, 0x55 / 255]\n\n/** A glow that follows a chunk keeps the spot the chunk had when the chunk leaves the pool, as the Lab's does (the object stays where it was). */\nfunction freezeFollowingGlow(chunk: Chunk): void {\n const glow = chunk.glow\n chunk.glow = null\n if (!glow || glow.follow !== chunk) return\n glow.x = chunk.x + glow.fx\n glow.y = chunk.y + glow.fy\n glow.z = chunk.z + glow.fz\n glow.fx = glow.fy = glow.fz = 0\n glow.follow = null\n}\n\nexport function createChunkPool(capacity: number, placeholder: RandomStream): OrderedPool<Chunk> {\n return new OrderedPool<Chunk>(\n capacity,\n () => ({\n x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, rx: 0, ry: 0, rz: 0, ax: 0, ay: 0, az: 0, sx: 0.2, sy: 0.2, sz: 0.2, color: DEFAULT_COLOR, plate: false, hot: false, rest: false,\n blood: false, splat: false, t: 0, trail: 0, glow: null, rng: placeholder, outColor: [0, 0, 0],\n }),\n freezeFollowingGlow,\n )\n}\n\nfunction firstResting(chunks: readonly Chunk[]): number {\n const index = chunks.findIndex((chunk) => chunk.rest)\n return index >= 0 ? index : 0\n}\n\n/** `chunk()` (L368-376). Draws rotation and spin first (six numbers, always), then lets the caller add what it wants; a full pool drops a resting piece, else the oldest. */\nexport function spawnChunk(pool: OrderedPool<Chunk>, random: RandomStream, init: ChunkInit): Chunk {\n const chunk = pool.acquire(firstResting)\n chunk.x = init.x ?? 0\n chunk.y = init.y ?? 0\n chunk.z = init.z ?? 0\n chunk.vx = init.vx ?? 0\n chunk.vy = init.vy ?? 0\n chunk.vz = init.vz ?? 0\n chunk.rx = random.next() * 6\n chunk.ry = random.next() * 6\n chunk.rz = random.next() * 6\n chunk.ax = random.between(-12, 12)\n chunk.ay = random.between(-12, 12)\n chunk.az = random.between(-12, 12)\n chunk.sx = init.sx ?? 0.2\n chunk.sy = init.sy ?? 0.2\n chunk.sz = init.sz ?? 0.2\n chunk.color = init.color ?? DEFAULT_COLOR\n chunk.plate = init.plate ?? false\n chunk.hot = init.hot ?? false\n chunk.rest = false\n chunk.blood = init.blood ?? false\n chunk.splat = false\n chunk.t = 0\n chunk.trail = 0\n chunk.glow = null\n chunk.rng = random\n return chunk\n}\n\nexport const HOT_COLOR: Rgb = [1, 0.42, 0.1]\nexport const HOT_SECONDS = 1.6\nconst HOT_BLEND = 0.85\n\n/** `_hotCol` (L399-400): the colour of a hot piece, blended towards the glow while it is young. */\nexport function colorOfChunk(chunk: Chunk): readonly [number, number, number] {\n if (!chunk.hot || chunk.t >= HOT_SECONDS) {\n chunk.outColor[0] = chunk.color[0]\n chunk.outColor[1] = chunk.color[1]\n chunk.outColor[2] = chunk.color[2]\n return chunk.outColor\n }\n const amount = Math.max(0, 1 - chunk.t / HOT_SECONDS) * HOT_BLEND\n chunk.outColor[0] = chunk.color[0] + (HOT_COLOR[0] - chunk.color[0]) * amount\n chunk.outColor[1] = chunk.color[1] + (HOT_COLOR[1] - chunk.color[1]) * amount\n chunk.outColor[2] = chunk.color[2] + (HOT_COLOR[2] - chunk.color[2]) * amount\n return chunk.outColor\n}\n","import type { MutableVec3 } from './random'\n\n/** Operations on `{ x, y, z }` written the way three.js r128 does them (`normalize` multiplies by the reciprocal), so a port repeats the Lab's numbers bit for bit. */\n\nexport function createVec3(x = 0, y = 0, z = 0): MutableVec3 {\n return { x, y, z }\n}\n\nexport function addScaled(target: MutableVec3, direction: MutableVec3, scale: number): MutableVec3 {\n target.x += direction.x * scale\n target.y += direction.y * scale\n target.z += direction.z * scale\n return target\n}\n\nexport function normalize(target: MutableVec3): MutableVec3 {\n const length = Math.sqrt(target.x * target.x + target.y * target.y + target.z * target.z) || 1\n const inverse = 1 / length\n target.x *= inverse\n target.y *= inverse\n target.z *= inverse\n return target\n}\n\nexport function distanceBetween(ax: number, ay: number, az: number, bx: number, by: number, bz: number): number {\n const dx = ax - bx\n const dy = ay - by\n const dz = az - bz\n return Math.sqrt(dx * dx + dy * dy + dz * dz)\n}\n\nexport function lerp(from: number, to: number, amount: number): number {\n return from + (to - from) * amount\n}\n\nexport function clamp(value: number, min: number, max: number): number {\n return value < min ? min : value > max ? max : value\n}\n","import type { Rgb } from '../color'\nimport { lerp } from '../vector'\nimport { OrderedPool } from '../pool'\n\n/** The position a glow follows (a chunk, or anything with `x`, `y`, `z`). */\nexport interface FollowPoint {\n readonly x: number\n readonly y: number\n readonly z: number\n}\n\n/** An additive halo (L294-318). The last block is what the renderer reads after `update`: where it is, how big and how bright. */\nexport interface Glow {\n x: number\n y: number\n z: number\n s: number\n s1: number | null\n i: number\n t: number\n life: number\n color: Rgb\n curve: number\n flick: boolean\n follow: FollowPoint | null\n fx: number\n fy: number\n fz: number\n outX: number\n outY: number\n outZ: number\n outSize: number\n outIntensity: number\n}\n\nexport type GlowInit = Partial<Omit<Glow, 't' | 'outX' | 'outY' | 'outZ' | 'outSize' | 'outIntensity'>>\n\nconst DEFAULT_COLOR: Rgb = [1, 0.6, 0.25]\n\nexport function createGlowPool(capacity: number): OrderedPool<Glow> {\n return new OrderedPool<Glow>(capacity, () => ({\n x: 0, y: 0, z: 0, s: 1, s1: null, i: 1, t: 0, life: 0.3, color: DEFAULT_COLOR, curve: 2, flick: false, follow: null, fx: 0, fy: 0, fz: 0,\n outX: 0, outY: 0, outZ: 0, outSize: 0, outIntensity: 0,\n }))\n}\n\nexport function spawnGlow(pool: OrderedPool<Glow>, init: GlowInit): Glow {\n const glow = pool.acquire()\n glow.x = init.x ?? 0\n glow.y = init.y ?? 0\n glow.z = init.z ?? 0\n glow.s = init.s ?? 1\n glow.s1 = init.s1 ?? null\n glow.i = init.i ?? 1\n glow.t = 0\n glow.life = init.life ?? 0.3\n glow.color = init.color ?? DEFAULT_COLOR\n glow.curve = init.curve ?? 2\n glow.flick = init.flick ?? false\n glow.follow = init.follow ?? null\n glow.fx = init.fx ?? 0\n glow.fy = init.fy ?? 0\n glow.fz = init.fz ?? 0\n return glow\n}\n\nfunction ageOrKeep(glow: Glow, dt: number): boolean {\n glow.t += dt\n return glow.t < glow.life\n}\n\nconst FLICKER_RATE = 40\nconst FLICKER_SPREAD = 9\n\n/** `updateGlows` (L305-318). */\nexport function updateGlows(pool: OrderedPool<Glow>, dt: number): void {\n pool.compact(ageOrKeep, dt)\n for (const glow of pool.items) {\n const age = glow.t / glow.life\n let x = glow.x\n let y = glow.y\n let z = glow.z\n if (glow.follow) {\n x = glow.follow.x + glow.fx\n y = glow.follow.y + glow.fy\n z = glow.follow.z + glow.fz\n }\n glow.outX = x\n glow.outY = y\n glow.outZ = z\n glow.outSize = glow.s1 !== null ? lerp(glow.s, glow.s1, 1 - Math.pow(1 - age, 3)) : glow.s\n const flicker = glow.flick ? 0.75 + 0.25 * Math.sin(glow.t * FLICKER_RATE + glow.s * FLICKER_SPREAD) : 1\n glow.outIntensity = glow.i * Math.pow(1 - age, glow.curve) * flicker\n }\n}\n","import type { Rgb } from '../color'\nimport { SMOKE_COLORS } from '../constants'\nimport { OrderedPool } from '../pool'\nimport type { RandomStream } from '../random'\n\n/** The ring a mushroom cloud's puffs travel on (L722-735). */\nexport interface Ring {\n x: number\n z: number\n y0: number\n rise: number\n rk: number\n R0: number\n R1: number\n r: number\n roll: number\n t: number\n life: number\n}\n\n/**\n * Fire, smoke, dust, blood and gas (L193-238). `kind`: 0 fire that turns into smoke, 1 dust, 2 blood, 3 gas or white powder smoke, 4 bile.\n * The last block is what the renderer reads after `update`.\n */\nexport interface Puff {\n x: number\n y: number\n z: number\n vx: number\n vy: number\n vz: number\n s0: number\n s1: number\n sg: number\n gk: number\n life: number\n t: number\n h0: number\n hl: number\n drag: number\n buoy: number\n grav: number\n kind: number\n seed: number\n wind: number\n color: Rgb\n ring: Ring | null\n ph: number\n th: number\n rng: RandomStream\n age: number\n heat: number\n size: number\n}\n\nexport type PuffInit = Partial<Omit<Puff, 'seed' | 'age' | 'heat' | 'size' | 't' | 'rng'>>\n\nconst FLOOR_FRACTION = 0.4\nconst FLOOR_BOUNCE = -0.2\nconst ARRIVAL_FRAME = 30\nconst ARRIVAL_BASE = 0.3\nconst GROWTH_CAP_SECONDS = 3.5\n\nexport function createPuffPool(capacity: number): OrderedPool<Puff> {\n const placeholder: RandomStream = { next: () => 0, between: (min) => min, direction: (out) => out, state: 0 }\n return new OrderedPool<Puff>(capacity, () => ({\n x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, s0: 0.2, s1: 1, sg: 0.1, gk: 6, life: 3, t: 0, h0: 0, hl: 0.4, drag: 3, buoy: 0.5, grav: 0, kind: 0, seed: 0, wind: 1,\n color: SMOKE_COLORS.grey, ring: null, ph: 0, th: 0, rng: placeholder, age: 0, heat: 0, size: 0,\n }))\n}\n\n/** `puff()` of the Lab: draws `seed` first, always, even when the caller gives one (L196), and drops the oldest puff when the pool is full. */\nexport function spawnPuff(pool: OrderedPool<Puff>, random: RandomStream, init: PuffInit): Puff {\n const puff = pool.acquire()\n puff.x = init.x ?? 0\n puff.y = init.y ?? 0\n puff.z = init.z ?? 0\n puff.vx = init.vx ?? 0\n puff.vy = init.vy ?? 0\n puff.vz = init.vz ?? 0\n puff.s0 = init.s0 ?? 0.2\n puff.s1 = init.s1 ?? 1\n puff.sg = init.sg ?? 0.1\n puff.gk = init.gk ?? 6\n puff.life = init.life ?? 3\n puff.t = 0\n puff.h0 = init.h0 ?? 0\n puff.hl = init.hl ?? 0.4\n puff.drag = init.drag ?? 3\n puff.buoy = init.buoy ?? 0.5\n puff.grav = init.grav ?? 0\n puff.kind = init.kind ?? 0\n puff.seed = random.next()\n puff.wind = init.wind ?? 1\n puff.color = init.color ?? SMOKE_COLORS.grey\n puff.ring = init.ring ?? null\n puff.ph = init.ph ?? 0\n puff.th = init.th ?? 0\n puff.rng = random\n puff.age = 0\n puff.heat = 0\n puff.size = 0\n return puff\n}\n\nfunction ageOrKeep(puff: Puff, dt: number): boolean {\n puff.t += dt\n return puff.t < puff.life\n}\n\nfunction moveOnRing(puff: Puff, ring: Ring, wind: { x: number; z: number }): void {\n const ringTime = ring.t\n const centerY = ring.y0 + ring.rise * (1 - Math.exp(-ringTime * ring.rk))\n const majorRadius = ring.R0 + (ring.R1 - ring.R0) * (1 - Math.exp(-ringTime * 1.6))\n const theta = puff.th + ringTime * ring.roll\n const minorRadius = ring.r * (0.8 + 0.4 * Math.min(1, ringTime))\n const radius = majorRadius + minorRadius * Math.cos(theta)\n puff.x = ring.x + Math.cos(puff.ph) * radius + wind.x * ringTime * 0.6\n puff.z = ring.z + Math.sin(puff.ph) * radius + wind.z * ringTime * 0.6\n puff.y = centerY + minorRadius * Math.sin(theta)\n}\n\nfunction moveFree(puff: Puff, dt: number, age: number, heat: number, wind: { x: number; z: number }): void {\n const damping = Math.exp(-puff.drag * dt)\n puff.vx *= damping\n puff.vy *= damping\n puff.vz *= damping\n puff.vy += (puff.buoy * (heat > 0 ? 1.6 : 1) - puff.grav) * dt\n puff.x += (puff.vx + wind.x * puff.wind * age) * dt\n puff.y += puff.vy * dt\n puff.z += (puff.vz + wind.z * puff.wind * age) * dt\n if (puff.y < puff.s0 * FLOOR_FRACTION) {\n puff.y = puff.s0 * FLOOR_FRACTION\n if (puff.vy < 0) puff.vy *= FLOOR_BOUNCE\n }\n}\n\n/** `updatePuffs` (L205-238): ages, removes what ended, then moves the rest with the wind (or along their mushroom ring) and derives what the shader reads. */\nexport function updatePuffs(pool: OrderedPool<Puff>, dt: number, wind: { x: number; z: number }): void {\n pool.compact(ageOrKeep, dt)\n for (const puff of pool.items) {\n const age = puff.t / puff.life\n const heat = puff.h0 > 0 ? puff.h0 * Math.pow(Math.max(0, 1 - puff.t / puff.hl), 1.25) : 0\n if (puff.ring) moveOnRing(puff, puff.ring, wind)\n else moveFree(puff, dt, age, heat, wind)\n const size = puff.s0 + (puff.s1 - puff.s0) * (1 - Math.exp(-puff.t * puff.gk)) + puff.sg * Math.min(puff.t, GROWTH_CAP_SECONDS)\n puff.size = size * Math.min(1, puff.t * ARRIVAL_FRAME + ARRIVAL_BASE)\n puff.age = age\n puff.heat = heat\n }\n}\n","import { OrderedPool } from '../pool'\n\n/** Shrapnel, sparks and embers, stretched along their velocity by the shader (L239-293). `kind`: 0 hot fragment, 1 steel ball, 2 white spark, 3 glass, 4 slow ember. */\nexport interface Spark {\n x: number\n y: number\n z: number\n vx: number\n vy: number\n vz: number\n t: number\n life: number\n w: number\n st: number\n kind: number\n drag: number\n g: number\n bounce: number\n age: number\n}\n\nexport type SparkInit = Partial<Omit<Spark, 't' | 'age'>>\n\nconst GROUND_LIFT = 0.02\nconst BOUNCE_SLIDE = 0.55\n\nexport function createSparkPool(capacity: number): OrderedPool<Spark> {\n return new OrderedPool<Spark>(capacity, () => ({ x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, t: 0, life: 0.8, w: 0.04, st: 0.035, kind: 0, drag: 0.6, g: 9.8, bounce: 0.35, age: 0 }))\n}\n\nexport function spawnSpark(pool: OrderedPool<Spark>, init: SparkInit): Spark {\n const spark = pool.acquire()\n spark.x = init.x ?? 0\n spark.y = init.y ?? 0\n spark.z = init.z ?? 0\n spark.vx = init.vx ?? 0\n spark.vy = init.vy ?? 0\n spark.vz = init.vz ?? 0\n spark.t = 0\n spark.life = init.life ?? 0.8\n spark.w = init.w ?? 0.04\n spark.st = init.st ?? 0.035\n spark.kind = init.kind ?? 0\n spark.drag = init.drag ?? 0.6\n spark.g = init.g ?? 9.8\n spark.bounce = init.bounce ?? 0.35\n spark.age = 0\n return spark\n}\n\nfunction ageOrKeep(spark: Spark, dt: number): boolean {\n spark.t += dt\n return spark.t < spark.life\n}\n\nexport interface SparkStep {\n readonly dt: number\n readonly groundAt: (x: number, z: number) => number\n}\n\n/** `updateSparks` (L277-293): drag, gravity, a bounce that kills some speed on the ground. */\nexport function updateSparks(pool: OrderedPool<Spark>, step: SparkStep): void {\n pool.compact(ageOrKeep, step.dt)\n for (const spark of pool.items) {\n const damping = Math.exp(-spark.drag * step.dt)\n spark.vx *= damping\n spark.vy *= damping\n spark.vz *= damping\n spark.vy -= spark.g * step.dt\n spark.x += spark.vx * step.dt\n spark.y += spark.vy * step.dt\n spark.z += spark.vz * step.dt\n const floor = step.groundAt(spark.x, spark.z) + GROUND_LIFT\n if (spark.y < floor) {\n spark.y = floor\n if (spark.vy < 0) {\n spark.vy *= -spark.bounce\n spark.vx *= BOUNCE_SLIDE\n spark.vz *= BOUNCE_SLIDE\n }\n }\n spark.age = spark.t / spark.life\n }\n}\n","import type { VfxCore } from './core'\nimport { spawnChunk } from './pools/chunks'\nimport type { Chunk, ChunkInit } from './pools/chunks'\nimport { spawnGlow } from './pools/glows'\nimport type { Glow, GlowInit } from './pools/glows'\nimport { spawnPuff } from './pools/puffs'\nimport type { Puff, PuffInit } from './pools/puffs'\nimport { spawnSpark } from './pools/sparks'\nimport type { Spark, SparkInit } from './pools/sparks'\nimport type { RandomStream } from './random'\n\nconst HOT_GLOW_COLOR = [1, 0.45, 0.12] as const\n\n/** The spawners count what an explosion makes (the Lab's `evCount`) on top of the pools, and a hot chunk carries a halo, as in the Lab (L371-376). */\nexport function addPuff(core: VfxCore, random: RandomStream, init: PuffInit): Puff {\n core.spawned.puffs++\n const puff = spawnPuff(core.puffs, random, init)\n core.trace?.puff?.(puff)\n return puff\n}\n\nexport function addSpark(core: VfxCore, init: SparkInit): Spark {\n core.spawned.sparks++\n const spark = spawnSpark(core.sparks, init)\n core.trace?.spark?.(spark)\n return spark\n}\n\nexport function addGlow(core: VfxCore, init: GlowInit): Glow {\n const glow = spawnGlow(core.glows, init)\n core.trace?.glow?.(glow)\n return glow\n}\n\nexport function addChunk(core: VfxCore, random: RandomStream, init: ChunkInit): Chunk {\n const chunk = spawnChunk(core.chunks, random, init)\n core.spawned.chunks++\n if (chunk.hot && core.layers.shrapnel) {\n chunk.glow = addGlow(core, { follow: chunk, s: 0.35 + chunk.sx, i: 2.2, life: random.between(1, 2.2), color: HOT_GLOW_COLOR, curve: 1.5 })\n }\n core.trace?.chunk?.(chunk)\n return chunk\n}\n","import type { Rgb } from '../color'\nimport { SMOKE_COLORS } from '../constants'\nimport type { VfxCore } from '../core'\nimport { OrderedPool } from '../pool'\nimport type { RandomStream } from '../random'\nimport { addPuff, addSpark } from '../spawn'\nimport type { Follow } from '../types'\nimport { lerp } from '../vector'\n\n/** A lingering fire (L424-442): it grows, gives flames that cool into smoke, and fades out. */\nexport interface Fire {\n x: number\n y: number\n z: number\n r: number\n r0: number\n grow: number\n life: number\n t: number\n rate: number\n acc: number\n smoke: number\n smokeAcc: number\n fade: number\n follow: Follow | null\n fx: number\n fy: number\n fz: number\n color: Rgb\n rng: RandomStream\n handle: number\n}\n\nexport type FireInit = Partial<Pick<Fire, 'x' | 'y' | 'z' | 'r' | 'grow' | 'life' | 'rate' | 'smoke' | 'follow' | 'fx' | 'fy' | 'fz' | 'color'>> & { startRadius?: number }\n\nexport function createFirePool(capacity: number, placeholder: RandomStream): OrderedPool<Fire> {\n return new OrderedPool<Fire>(capacity, () => ({\n x: 0, y: 0, z: 0, r: 0.6, r0: 0.6, grow: 0, life: 6, t: 0, rate: 22, acc: 0, smoke: 0.6, smokeAcc: 0, fade: 1, follow: null, fx: 0, fy: 0, fz: 0, color: SMOKE_COLORS.black,\n rng: placeholder, handle: 0,\n }))\n}\n\nexport function spawnFire(pool: OrderedPool<Fire>, random: RandomStream, init: FireInit, handle: number): Fire {\n const fire = pool.acquire()\n fire.x = init.x ?? 0\n fire.y = init.y ?? 0\n fire.z = init.z ?? 0\n fire.r = init.r ?? 0.6\n fire.r0 = init.startRadius ?? fire.r\n fire.grow = init.grow ?? 0\n fire.life = init.life ?? 6\n fire.t = 0\n fire.rate = init.rate ?? 22\n fire.acc = 0\n fire.smoke = init.smoke ?? 0.6\n fire.smokeAcc = 0\n fire.fade = 1\n fire.follow = init.follow ?? null\n fire.fx = init.fx ?? 0\n fire.fy = init.fy ?? 0\n fire.fz = init.fz ?? 0\n fire.color = init.color ?? SMOKE_COLORS.black\n fire.rng = random\n fire.handle = handle\n return fire\n}\n\nexport function addFire(core: VfxCore, random: RandomStream, init: FireInit, handle: number): Fire {\n const fire = spawnFire(core.fires, random, init, handle)\n core.trace?.fire?.(fire)\n return fire\n}\n\nfunction positionOf(follow: Follow): { x: number; y: number; z: number } {\n return 'position' in follow ? follow.position : follow\n}\n\nfunction emitFlames(core: VfxCore, fire: Fire, radius: number, dt: number): void {\n const random = fire.rng\n fire.acc += dt * fire.rate * (0.5 + radius * 0.9) * fire.fade * core.clock.lod\n while (fire.acc >= 1) {\n fire.acc -= 1\n const angle = random.next() * Math.PI * 2\n const distance = Math.sqrt(random.next()) * radius\n addPuff(core, random, {\n x: fire.x + Math.cos(angle) * distance, y: fire.y + 0.05, z: fire.z + Math.sin(angle) * distance,\n vx: random.between(-0.2, 0.2), vy: random.between(0.6, 1.4), vz: random.between(-0.2, 0.2),\n s0: 0.1, s1: random.between(0.28, 0.46) * (0.8 + Math.min(radius, 3) * 0.22), sg: -0.08,\n life: random.between(0.45, 0.85), h0: random.between(1, 1.25), hl: random.between(0.35, 0.6),\n drag: 1.5, buoy: 3.2, color: fire.color, kind: 0, wind: 0.4,\n })\n }\n}\n\nfunction emitSmoke(core: VfxCore, fire: Fire, radius: number, dt: number): void {\n const random = fire.rng\n fire.smokeAcc += dt * fire.smoke * (1 + radius * 0.8) * fire.fade * core.clock.lod\n while (fire.smokeAcc >= 1 && core.layers.smoke) {\n fire.smokeAcc -= 1\n addPuff(core, random, {\n x: fire.x + random.between(-radius, radius) * 0.5, y: fire.y + 0.6 + radius * 0.3, z: fire.z + random.between(-radius, radius) * 0.5,\n vy: random.between(0.8, 1.5), s0: 0.25, s1: 0.6 + Math.min(radius, 2) * 0.3, sg: 0.3, life: random.between(4, 6.5),\n h0: 0.25, hl: 0.25, drag: 0.6, buoy: 0.55, color: fire.color, kind: 0,\n })\n }\n}\n\nfunction emitEmber(core: VfxCore, fire: Fire, radius: number, dt: number): void {\n const random = fire.rng\n if (!(random.next() < dt * 6 * fire.fade && core.layers.shrapnel)) return\n addSpark(core, {\n x: fire.x + random.between(-radius, radius) * 0.6, y: fire.y + 0.4, z: fire.z + random.between(-radius, radius) * 0.6,\n vx: random.between(-0.4, 0.4), vy: random.between(1.5, 3.2), vz: random.between(-0.4, 0.4),\n life: random.between(0.8, 1.6), w: 0.025, st: 0.02, kind: 4, g: -0.6, drag: 1.2,\n })\n}\n\n/** `updateFires` (L430-442), walking the list backwards like the Lab. */\nexport function updateFires(core: VfxCore, dt: number): void {\n const pool = core.fires\n for (let index = pool.items.length - 1; index >= 0; index--) {\n const fire = pool.items[index]!\n fire.t += dt\n if (fire.follow) {\n const point = positionOf(fire.follow)\n fire.x = point.x + fire.fx\n fire.y = point.y + fire.fy\n fire.z = point.z + fire.fz\n }\n const radius = fire.grow ? lerp(fire.r0, fire.r, Math.min(1, fire.t / fire.grow)) : fire.r\n fire.fade = fire.t < fire.life - 1.5 ? Math.min(1, fire.t * 3) : Math.max(0, (fire.life - fire.t) / 1.5)\n if (fire.t >= fire.life) {\n pool.removeAt(index)\n continue\n }\n if (!core.layers.fire) continue\n emitFlames(core, fire, radius, dt)\n emitSmoke(core, fire, radius, dt)\n emitEmber(core, fire, radius, dt)\n }\n}\n","import type { Expr } from './expression'\n\n/** Constructors for the computed fields of a recipe. They only build data (`{ $, a }`); `expression.ts` runs it. */\nconst call =\n (name: string) =>\n (...args: readonly unknown[]): Expr => ({ $: name, a: args })\n\nexport const rnd = call('rnd')\nexport const rand = call('rand')\nexport const add = call('add')\nexport const concat = call('concat')\nexport const sub = call('sub')\nexport const mul = call('mul')\nexport const div = call('div')\nexport const mod = call('mod')\nexport const neg = call('neg')\nexport const abs = call('abs')\nexport const sqrt = call('sqrt')\nexport const exp = call('exp')\nexport const sin = call('sin')\nexport const cos = call('cos')\nexport const atan2 = call('atan2')\nexport const min = call('min')\nexport const max = call('max')\nexport const floor = call('floor')\nexport const ceil = call('ceil')\nexport const round = call('round')\nexport const clamp = call('clamp')\nexport const lerp = call('lerp')\nexport const ease = call('ease')\nexport const lt = call('lt')\nexport const gt = call('gt')\nexport const le = call('le')\nexport const ge = call('ge')\nexport const eq = call('eq')\nexport const ne = call('ne')\nexport const not = call('not')\nexport const and = call('and')\nexport const or = call('or')\nexport const ite = call('ite')\nexport const vadd = call('vadd')\nexport const vsub = call('vsub')\nexport const vscale = call('vscale')\nexport const vnorm = call('vnorm')\nexport const vneg = call('vneg')\nexport const vlerp = call('vlerp')\nexport const vx = call('vx')\nexport const vy = call('vy')\nexport const vz = call('vz')\nexport const vdist = call('vdist')\nexport const vwith = call('with')\nexport const pick = call('at')\nexport const scaled = call('scaled')\nexport const param = (...values: readonly unknown[]): Expr => ({ $: 'param', a: [values] })\n/** Binds `name` to `value` while `body` resolves: the page's `const a = ...` inside a spawn callback. */\nexport const local = (name: string, value: unknown, body: unknown): Expr => ({ $: 'let', a: [name, value, body] })\nexport const query = (behavior: string, name: string, ...args: readonly unknown[]): Expr => ({ $: 'query', a: [behavior, name, ...args] })\nexport const variable = (name: string): Expr => ({ $: 'var', a: [name] })\nexport const cameraForward = call('cameraForward')\nexport const TIME: Expr = { $: 't' }\nexport const PARAM: Expr = { $: 'paramIndex' }\nexport const DELTA: Expr = { $: 'dt' }\nexport const INDEX: Expr = { $: 'index' }\nexport const QUALITY: Expr = { $: 'quality' }\nexport const FRESH: Expr = { $: 'fresh' }\nexport const DIRECTION: Expr = { $: 'direction' }\nexport const NORMAL: Expr = { $: 'normal' }\nexport const CAMERA_POSITION: Expr = { $: 'cameraPosition' }\nexport const UP = [0, 1, 0] as const\n","import type { Computed, EffectStep, ScreenKey } from './types'\n\ntype System = 'add' | 'normal' | 'streak'\n\n/** Constructors for the steps of a recipe (they only build data). */\nexport const burst = (system: System, count: Computed, spawn: Computed): EffectStep => ({ op: 'burst', system, count, spawn })\nexport const spawn = (system: System, options: Computed): EffectStep => ({ op: 'spawn', system, spawn: options })\nexport const emit = (key: string, system: System, rate: Computed, options: Computed): EffectStep => ({ op: 'emit', key, system, rate, spawn: options })\n/** A solid particle (`MeshSpawn`): a cone, a sphere, an icosahedron. */\nexport const mesh = (options: Computed): EffectStep => ({ op: 'mesh', spawn: options })\nexport const debris = (options: Computed): EffectStep => ({ op: 'debris', spawn: options })\nexport const decal = (...args: readonly Computed[]): EffectStep => ({ op: 'decal', args })\nexport const ring = (...args: readonly Computed[]): EffectStep => ({ op: 'ring', args })\nexport const flash = (...args: readonly Computed[]): EffectStep => ({ op: 'flash', args })\nexport const lens = (...args: readonly Computed[]): EffectStep => ({ op: 'lens', args })\nexport const screen = (change: { add?: Partial<Record<ScreenKey, Computed>>; set?: Partial<Record<ScreenKey, Computed>>; max?: Partial<Record<ScreenKey, Computed>> }): EffectStep => ({ op: 'screen', ...change })\nexport const fireLight = (slot: 0 | 1, point: Computed, intensity: Computed, time: Computed): EffectStep => ({ op: 'fire_light', slot, point, intensity, time })\nexport const fireLightOff = (slot: 0 | 1): EffectStep => ({ op: 'fire_light_off', slot })\nexport const cue = (name: string, args?: Computed, draws?: number): EffectStep => ({ op: 'cue', cue: name, ...(args !== undefined ? { args } : {}), ...(draws !== undefined ? { draws } : {}) })\nexport const call = (macro: string, ...args: readonly Computed[]): EffectStep => ({ op: 'call', macro, args })\nexport const after = (time: Computed, ...steps: readonly EffectStep[]): EffectStep => ({ op: 'at', time, steps })\nexport const when = (condition: Computed, steps: readonly EffectStep[], otherwise?: readonly EffectStep[]): EffectStep => ({ op: 'when', if: condition, steps, ...(otherwise ? { otherwise } : {}) })\nexport const repeat = (count: Computed, ...steps: readonly EffectStep[]): EffectStep => ({ op: 'repeat', count, steps })\nexport const set = (name: string, value: Computed): EffectStep => ({ op: 'set', name, value })\nexport const behavior = (id: string, action: string, args?: Computed): EffectStep => ({ op: 'behavior', id, action, ...(args !== undefined ? { args } : {}) })\nexport const append = (name: string, value: Computed): EffectStep => ({ op: 'append', name, value })\nexport const camera = (eye: Computed, target: Computed, instant?: boolean): EffectStep => ({ op: 'camera', eye, target, ...(instant ? { instant } : {}) })\nexport const post = (values: Readonly<Record<string, Computed>>): EffectStep => ({ op: 'post', values })\n","import { gt, lt, and, INDEX, ite, mod, scaled, TIME } from '../dsl'\nimport { after, burst, debris, decal, emit, repeat, ring, screen, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst CENTRE: readonly [number, number, number] = [0, 1.1, 0]\nconst UP: readonly [number, number, number] = [0, 1, 0]\n\n/** W501-520: the belly bursts after a second and leaves a green cloud near the ground for eight. */\nexport const BLOATER_CLOUD: EffectDef = {\n id: 'bloater_cloud',\n pageId: 'inchado',\n team: 'creatures',\n budget: { particles: 180 },\n loop: 9,\n view: { eye: [5.5, 3.2, 6], target: [0, 1, 0] },\n timeline: [\n after(\n 1,\n burst('normal', 40, { p: CENTRE, r: 0.4, sp: [1.5, 4], drag: 2.2, life: [4, 6], size: [1, 2.2], grow: 2, c0: 0x6f8a3c, c1: 0x3b4a26, a: 0.32, fi: 0.08, g: 0.05 }),\n burst('normal', 30, { p: CENTRE, dir: UP, cone: 1.3, sp: [2, 4], life: 1.4, size: [0.04, 0.08], c0: 0x4a5a1c, g: 9.8, fi: 0, gnd: 3, ds: [0.1, 0.2] }),\n repeat(scaled(10), debris({ p: CENTRE, dir: UP, cone: 1.2, sp: [2, 5], size: [0.07, 0.05, 0.07], k: [0.6, 1.4], c: ite(mod(INDEX, 2), 0x6b7a52, 0x4a1a12), life: 9 })),\n decal(0, 0, 0, 1.6, 0x6a7a24, { grow: 0.6, life: 60, fluid: 'bile' }),\n ring([0, 0.02, 0], 4, 4, 8, 0x3a5a20, UP, 0.35),\n screen({ add: { shake: 0.15 } }),\n ),\n ],\n tick: [\n when(and(gt(TIME, 1), lt(TIME, 8)), [\n emit('gas', 'normal', 7, { p: [0, 0.4, 0], r: 3.4, flat: true, dir: UP, cone: 1.3, sp: [0.1, 0.35], drag: 0.4, life: [3, 5], size: [1.2, 2.2], grow: 1.5, c0: 0x6f8a3c, c1: 0x3b4a26, a: 0.22, fi: 0.3 }),\n emit('mote', 'add', 10, { p: [0, 0.6, 0], r: 3.6, flat: true, dir: UP, cone: 0.6, sp: [0.05, 0.25], life: [1.5, 2.5], size: [0.04, 0.07], c0: 0x5fe08a, a: 0.7, fi: 0.4 }),\n ]),\n ],\n}\n","import { add, and, gt, lt, mul, param, PARAM, pick, TIME, vnorm } from '../dsl'\nimport { after, call, decal, emit, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst BLOOD = param(0x8a0a0a, 0x5a0c10, 0x3a1c12, 0x1d1914)\nconst BLEED = [1, 0.7, 0.35]\n\n/** W449-473: blood by infection age: a mist at the entry, a jet at the exit, a pool that grows and darkens. */\nexport const BLOOD_HIT: EffectDef = {\n id: 'blood_hit',\n pageId: 'sangue',\n team: 'creatures',\n params: { id: 'age', options: ['fresh', 'recent', 'old', 'decrepit'], default: 0 },\n budget: { particles: 70, decals: 1 },\n loop: 3.2,\n view: { eye: [2.6, 1.9, 3.4], target: [0, 1, -0.6] },\n timeline: [\n after(0.3, call('blood_hit', [0, 1.3, 0.1], vnorm([0.15, 0.1, -1]), PARAM)),\n after(1.5, call('blood_hit', [0.1, 1.05, 0.1], vnorm([-0.2, 0.05, -1]), PARAM, 0.8)),\n when(lt(PARAM, 3), [after(0.7, decal(0, 0, -0.2, add(0.5, mul(0.7, pick(BLEED, PARAM))), BLOOD, { grow: add(2, mul(2, pick(BLEED, PARAM))), dark: 60, dry: 0x3b1d14 }))]),\n ],\n tick: [\n when(and(lt(PARAM, 2), gt(TIME, 0.3)), [\n emit('drip', 'normal', param(7, 3), {\n p: [0.05, 1.15, 0.17], box: [0.08, 0.05, 0], dir: [0, 1, 0], cone: 0.2, sp: [0, 0.2], life: 1.2, size: 0.03, c0: BLOOD, g: 9.8, fi: 0, gnd: 3, ds: [0.04, 0.08],\n }),\n ]),\n ],\n}\n","import { and, gt, lt, TIME } from '../dsl'\nimport { after, decal, emit, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst UP: readonly [number, number, number] = [0, 1, 0]\n\n/** W540-553: fifteen minutes in eight seconds: the pool dries, flies appear from the minute five and the body turns to dust. */\nexport const CORPSE_DECAY: EffectDef = {\n id: 'corpse_decay',\n pageId: 'corpo',\n team: 'creatures',\n budget: { particles: 40 },\n loop: 8.5,\n view: { eye: [2.4, 1.6, 2.6], target: [0, 0.3, -0.6] },\n timeline: [after(0.8, decal(0, 0, -0.9, 1.1, 0x5a0c10, { grow: 1.5, dark: 5, dry: 0x3b1d14 }))],\n tick: [\n when(and(gt(TIME, 2.5), lt(TIME, 7)), [\n emit('fly', 'normal', 6, { p: [0, 0.35, -0.8], r: 0.5, sp: [0.3, 0.8], life: [0.6, 1.2], size: 0.02, c0: 0x050505, a: 1, fi: 0.2 }),\n ]),\n when(and(gt(TIME, 6.8), lt(TIME, 7.6)), [\n emit('dust', 'normal', 40, { p: [0, 0.15, -0.8], box: [0.25, 0.05, 0.8], dir: UP, cone: 0.6, sp: [0.2, 0.6], drag: 1, life: [1, 1.8], size: [0.12, 0.22], grow: 3, c0: 0x2b2722, a: 0.5 }),\n ]),\n ],\n}\n","import { and, INDEX, ite, lt, not, pick, query, QUALITY, rnd, variable } from '../dsl'\nimport { behavior, repeat, set, spawn, when } from '../steps'\nimport type { EffectDef } from '../types'\n\n/** Which of the ten walkers run: the ones with red eyes (W192). */\nconst RUNNERS = [false, true, false, false, false, true, false, false, false, true]\nconst RUN_SPEED = 3.8\nconst WALK_SPEED = 0.7\nconst SPEED = ite(pick(RUNNERS, INDEX), RUN_SPEED, WALK_SPEED)\nconst walkTo = behavior('walkers', 'walk', { index: INDEX, to: [-12, 0, query('walkers', 'z', INDEX)], speed: SPEED })\n\n/** W562-571 (the page's `olhos`): ten zombies walk through fog from the east; the runners leave a short red trail behind their eyes. */\nexport const EYES_IN_FOG: EffectDef = {\n id: 'eyes_in_fog',\n pageId: 'olhos',\n team: 'creatures',\n budget: { particles: 20 },\n loop: 0,\n view: { eye: [-9, 1.7, 2], target: [6, 1.3, 2] },\n setup: [\n repeat(10, set('placeX', rnd(4, 34)), set('placeZ', rnd(-1.5, 5.5)), behavior('walkers', 'place', { index: INDEX, x: variable('placeX'), z: variable('placeZ'), facing: -Math.PI / 2 }), walkTo),\n ],\n timeline: [],\n tick: [\n repeat(\n 10,\n when(lt(query('walkers', 'x', INDEX), -8), [behavior('walkers', 'move_x', { index: INDEX, x: rnd(28, 36) }), walkTo], [when(not(query('walkers', 'is_walking', INDEX)), [walkTo])]),\n when(and(pick(RUNNERS, INDEX), lt(QUALITY, 2)), [\n spawn('streak', { p: query('walkers', 'point', INDEX, [0, 1.74, 0.2]), dir: [1, 0, 0], cone: 0, sp: RUN_SPEED, v: [-RUN_SPEED, 0, 0], life: 0.25, len: 0.06, c0: 0xe5392b, c1: 0x400000, a: 0.8 }),\n ]),\n ),\n ],\n}\n","import { ite, gt, lt, and, mul, sin, sub, vnorm, TIME } from '../dsl'\nimport { after, burst, debris, emit, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst SHOULDER: readonly [number, number, number] = [0.36, 1.46, 0.05]\nconst FRESH_BLOOD = 0x8a0a0a\n\n/** W487-500: the arm leaves as a loose piece and the shoulder spurts in pulses for half a second. */\nexport const DISMEMBER: EffectDef = {\n id: 'dismember',\n pageId: 'membro',\n team: 'creatures',\n budget: { particles: 60, debris: 1, decals: 4 },\n loop: 3.6,\n view: { eye: [3, 1.8, 2.2], target: [0.4, 1.1, 0] },\n timeline: [\n after(\n 0.4,\n debris({ p: SHOULDER, v: [2.2, 2.5, -0.6], sp: 0, size: [0.12, 0.12, 0.66], c: 0x6d7a5a, spin: 9, life: 12, bounce: 0.25 }),\n burst('normal', 12, { p: SHOULDER, r: 0.05, sp: [0.3, 1.2], drag: 3, life: [0.3, 0.6], size: [0.1, 0.16], grow: 3, c0: FRESH_BLOOD, a: 0.6 }),\n ),\n ],\n tick: [\n when(and(gt(TIME, 0.4), lt(TIME, 1.1)), [\n emit('jet', 'normal', mul(90, ite(gt(sin(mul(sub(TIME, 0.4), 28)), 0), 1, 0.2)), { p: SHOULDER, dir: vnorm([1, 0.35, 0.1]), cone: 0.25, sp: [1.5, 3], life: 1.4, size: [0.03, 0.055], c0: FRESH_BLOOD, g: 9.8, fi: 0, gnd: 3, ds: [0.05, 0.12] }),\n ]),\n ],\n}\n","import { ite, mod, INDEX, scaled, vnorm } from '../dsl'\nimport { after, burst, debris, decal, flash, repeat } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst FRESH_BLOOD = 0x8a0a0a\n\n/** W474-486: the head bursts, the eyes go out in the same frame and the body falls. */\nexport const HEADSHOT: EffectDef = {\n id: 'headshot',\n pageId: 'headshot',\n team: 'creatures',\n budget: { particles: 70, debris: 8, decals: 1 },\n loop: 3.2,\n view: { eye: [2.2, 1.8, 3], target: [0, 1.2, -0.5] },\n timeline: [\n after(\n 0.45,\n burst('normal', 20, { p: [0, 1.7, 0], r: 0.1, sp: [0.4, 1.8], drag: 3, life: [0.4, 0.75], size: [0.12, 0.2], grow: 3, c0: FRESH_BLOOD, a: 0.7 }),\n burst('normal', 35, { p: [0, 1.7, 0], dir: vnorm([0, 0.45, -1]), cone: 0.8, sp: [2, 5], life: 1.5, size: [0.035, 0.065], c0: FRESH_BLOOD, g: 9.8, fi: 0, gnd: 3, ds: [0.06, 0.14] }),\n repeat(scaled(8), debris({ p: [0, 1.7, 0], dir: vnorm([0, 0.6, -1]), cone: 0.9, sp: [1.5, 4], size: [0.06, 0.05, 0.06], k: [0.5, 1.2], c: ite(mod(INDEX, 2), 0xd9cfbf, 0x6d7a5a), life: 8 })),\n flash([0, 1.7, 0], 0xff3020, 1.2, 0.08, 4),\n ),\n after(1.4, decal(0, 0, -1.5, 0.9, FRESH_BLOOD, { grow: 2.4, dark: 60, dry: 0x3b1d14 })),\n ],\n}\n","import { and, gt, lt, TIME, vnorm } from '../dsl'\nimport { cue, emit, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst UP: readonly [number, number, number] = [0, 1, 0]\n\n/** W554-561: green motes while the body twitches (two random draws a frame for the arms), the eyes light up and a breath of steam on the first scream. */\nexport const REANIMATION: EffectDef = {\n id: 'reanimation',\n pageId: 'reanima',\n team: 'creatures',\n budget: { particles: 30 },\n loop: 6.5,\n view: { eye: [2.4, 1.4, 2.4], target: [0, 0.6, -0.4] },\n timeline: [],\n tick: [\n when(lt(TIME, 3.4), [\n when(gt(TIME, 1), [cue('zombie_arm_twitch', undefined, 2)]),\n when(gt(TIME, 0.6), [\n emit('mote', 'add', 8, { p: [0, 0.2, -0.8], box: [0.25, 0.05, 0.6], dir: UP, cone: 0.4, sp: [0.2, 0.5], life: [0.6, 1], size: [0.03, 0.05], c0: 0x9fc25a, a: 0.7, fi: 0.3 }),\n ]),\n ]),\n when(and(gt(TIME, 5), lt(TIME, 5.6)), [\n emit('br', 'normal', 20, { p: [0, 1.68, 0.2], dir: vnorm([0, 0.3, 1]), cone: 0.3, sp: [0.6, 1.1], drag: 1.5, life: 1.2, size: [0.05, 0.1], grow: 5, c0: 0xd8dde2, a: 0.2 }),\n ]),\n ],\n}\n","import { add, cos, div, INDEX, local, lt, mul, rand, sin, sub, TIME, variable, vnorm } from '../dsl'\nimport { after, emit, repeat, ring, screen, set, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst WHITE_RING = 0xe8e2d0\nconst PREPARATION_END = 1.7\nconst SHRIEK_END = 2.6\n\n/** W521-539: a steam of breath while the screamer prepares, then three pressure rings, a ring of dust and a burst of chromatic aberration. */\nexport const SCREAMER_SHRIEK: EffectDef = {\n id: 'screamer_shriek',\n pageId: 'grito',\n team: 'creatures',\n budget: { particles: 150 },\n loop: 5,\n view: { eye: [6, 3, 7], target: [0, 1, -1] },\n timeline: [\n after(\n PREPARATION_END,\n repeat(3, after(add(PREPARATION_END, mul(INDEX, 0.18)), ring([0, 0.03, 0], 0.3, 9, 0.9, WHITE_RING))),\n screen({ add: { chroma: 7, shake: 0.25 } }),\n ),\n ],\n tick: [\n when(lt(TIME, PREPARATION_END), [\n emit('vap', 'normal', 18, { p: [0, 1.68, 0.2], dir: vnorm([0, 0.5, 1]), cone: 0.3, sp: [0.6, 1.2], drag: 1.5, life: [0.8, 1.3], size: [0.05, 0.1], grow: 5, c0: 0xd8dde2, a: 0.2 }),\n ], [\n when(lt(TIME, SHRIEK_END), [\n set('shriekRadius', mul(div(sub(TIME, PREPARATION_END), 0.9), 8)),\n emit(\n 'dust', 'normal', 140,\n local('angle', mul(rand(), 6.283), {\n p: [mul(cos(variable('angle')), variable('shriekRadius')), 0.05, mul(sin(variable('angle')), variable('shriekRadius'))],\n dir: vnorm([cos(variable('angle')), 0.6, sin(variable('angle'))]),\n cone: 0.3, sp: [0.6, 1.6], drag: 2, life: [0.8, 1.4], size: [0.2, 0.35], grow: 3, c0: 0x5d574d, a: 0.35,\n }),\n ),\n ]),\n ]),\n ],\n}\n","import type { Vec3Tuple } from '../workshop/types'\n\n/**\n * A point of a prop's own space in the world, for a prop placed at `translation` and turned by `rotation` (x, y, z radians, applied in the order x, y,\n * z) as three.js composes it: how the page's `carW` and `tbW` find the bumper or a table leg.\n */\nexport function localToWorld(translation: Vec3Tuple, rotation: Vec3Tuple, local: Vec3Tuple): Vec3Tuple {\n const [x, y, z] = local\n const [a, c, e] = [Math.cos(rotation[0]), Math.cos(rotation[1]), Math.cos(rotation[2])]\n const [b, d, f] = [Math.sin(rotation[0]), Math.sin(rotation[1]), Math.sin(rotation[2])]\n const [ae, af, be, bf] = [a * e, a * f, b * e, b * f]\n return [\n c * e * x + -c * f * y + d * z + translation[0],\n (af + be * d) * x + (ae - bf * d) * y + -b * c * z + translation[1],\n (bf - ae * d) * x + (be + af * d) * y + a * c * z + translation[2],\n ]\n}\n","import { add, eq, FRESH, INDEX, ite, PARAM, pick, rnd, variable, vadd } from '../dsl'\nimport { localToWorld } from '../props'\nimport { after, behavior, call, camera, decal, repeat, set, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst MATERIALS = ['concrete', 'metal', 'wood', 'glass', 'flesh', 'water']\nconst CAR_ORIGIN = [5.6, 0, 0.8] as const\nconst CAR_ROTATION = [0, -0.4, 0] as const\nconst FACING_WALL = [0, 0, 1] as const\nconst UP = [0, 1, 0] as const\nconst GLASS = 3\nconst FLESH = 4\nconst WATER = 5\nconst SHOT_TIMES = [0, 0.55, 1.1]\nconst LAST_SHOT = 2\nconst BUMPER = localToWorld(CAR_ORIGIN, CAR_ROTATION, [0.96, 0.7, 0.4])\nconst CAR_SIDE = [Math.cos(CAR_ROTATION[1]), 0, -Math.sin(CAR_ROTATION[1])] as const\n/** Where each material is hit and the way the surface faces (W752-756). */\nconst POINTS = [[-2, 1.6, -4.45], BUMPER, [2, 1.3, -4.36], [-5, 1.9, -4.45], [0, 1.25, 0.16], [-1.5, 0.012, 2.2]]\nconst NORMALS = [FACING_WALL, CAR_SIDE, FACING_WALL, FACING_WALL, FACING_WALL, UP]\n/** The eye the page glides to for each material on the first run (W773-774); the car's is found from the car. */\nconst EYES = [[-2, 1.6, -1.8], localToWorld(CAR_ORIGIN, CAR_ROTATION, [3, 1.4, 1.5]), [2, 1.3, -1.6], [-5, 1.9, -1.8], [0, 1.2, 2.6], [-1.5, 0.8, 4.6]]\n/** The shop window as the glass texture sees it (`crackAt` takes pixels): 2.6 x 1.8 m on 256 x 180. */\nconst WINDOW = { centreX: -5, centreY: 1.9, width: 2.6, height: 1.8, pixelsX: 256, pixelsY: 180 }\n\nconst scatter = ite(eq(PARAM, WATER), [rnd(-0.6, 0.6), 0, rnd(-0.3, 0.3)], ite(eq(PARAM, FLESH), [rnd(-0.1, 0.1), rnd(-0.15, 0.15), 0], [rnd(-0.4, 0.4), rnd(-0.3, 0.3), 0]))\nconst hitX = { $: 'vx', a: [variable('hit')] }\nconst hitY = { $: 'vy', a: [variable('hit')] }\nconst pixelX = { $: 'mul', a: [{ $: 'add', a: [{ $: 'div', a: [{ $: 'sub', a: [hitX, WINDOW.centreX] }, WINDOW.width] }, 0.5] }, WINDOW.pixelsX] }\nconst pixelY = { $: 'mul', a: [{ $: 'sub', a: [1, { $: 'add', a: [{ $: 'div', a: [{ $: 'sub', a: [hitY, WINDOW.centreY] }, WINDOW.height] }, 0.5] }] }, WINDOW.pixelsY] }\n\n/** W762-775: three hits, 0.55 s apart, on the material the parameter names; the window keeps its cracks and the puddle takes a dark patch. */\nexport const IMPACT_BY_MATERIAL: EffectDef = {\n id: 'impact',\n pageId: 'impacto',\n team: 'weapons',\n params: { id: 'material', options: MATERIALS, default: 0 },\n budget: { particles: 40, decals: 1 },\n loop: 2.6,\n view: { eye: [0, 1.8, 4.5], target: [0, 1.2, -2] },\n props: ['cracks'],\n timeline: [\n when({ $: 'eq', a: [PARAM, WATER] }, [decal(-1.5, 0, 2.2, 3.2, 0x0d1218, { grow: 0, life: 60, ar: 0.55 })]),\n repeat(\n SHOT_TIMES.length,\n after(\n add(0.2, pick(SHOT_TIMES, INDEX)),\n set('hit', vadd(pick(POINTS, PARAM), scatter)),\n call('impact', pick(MATERIALS, PARAM), variable('hit'), pick(NORMALS, PARAM), 1, 0),\n when(eq(PARAM, GLASS), [behavior('cracks', 'crack', { x: pixelX, y: pixelY, big: eq(INDEX, LAST_SHOT) })]),\n ),\n ),\n when(FRESH, [camera(pick(EYES, PARAM), pick(POINTS, PARAM))]),\n ],\n}\n","import { and, DELTA, gt, INDEX, le, lt, mod, mul, add, pick, rnd, sub, TIME, variable } from '../dsl'\nimport { after, append, call, cue, decal, repeat, set, spawn, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst BLOOD = [0x8a0a0a, 0x5a0c10, 0x3a1c12, 0x1d1914]\n/** Where the five bodies stand along the line of the shot (W786). */\nconst BODY_Z = [2.2, 1, -0.2, -1.4, -2.6]\nconst MUZZLE_Z = 8.5\nconst SPEED = 12\nconst WALL_Z = -4.4\nconst FORWARD = [0, 0, -1] as const\nconst bulletAt = [0, 1.45, variable('bulletZ')]\n\n/** W776-785: a tracer of the Barrett crosses five bodies, each bleeding the colour of its age, then hits the wall. */\nexport const PENETRATION: EffectDef = {\n id: 'penetration',\n pageId: 'penetra',\n team: 'weapons',\n budget: { particles: 250 },\n loop: 4.2,\n view: { eye: [4.2, 2.2, 6.5], target: [0, 1.2, -0.5] },\n setup: [\n cue('zombie_visible', { index: -1, visible: false }),\n repeat(\n BODY_Z.length,\n set('bodyX', rnd(-0.05, 0.05)),\n set('bodyTurn', rnd(-0.3, 0.3)),\n append('bodiesX', variable('bodyX')),\n cue('zombie_place', { index: INDEX, x: variable('bodyX'), z: pick(BODY_Z, INDEX), turn: variable('bodyTurn') }),\n ),\n ],\n timeline: [set('bulletZ', MUZZLE_Z), set('bulletOn', true), call('muzzle', 'barrett', [0, 1.45, MUZZLE_Z], FORWARD)],\n tick: [\n when(variable('bulletOn'), [\n set('previousZ', variable('bulletZ')),\n set('bulletZ', sub(variable('bulletZ'), mul(SPEED, DELTA))),\n spawn('streak', { p: bulletAt, dir: FORWARD, cone: 0, sp: 12, life: 0.04, len: 0.07, c0: 0xffd090 }),\n spawn('add', { p: bulletAt, size: 0.09, life: 0.05, c0: 0xffc070, fi: 0 }),\n spawn('normal', { p: bulletAt, r: 0.02, sp: [0, 0.05], life: [1.2, 2], size: 0.05, grow: 3, c0: 0xc8ccd2, a: 0.12, fi: 0.1 }),\n repeat(\n BODY_Z.length,\n when(and(gt(variable('previousZ'), pick(BODY_Z, INDEX)), le(variable('bulletZ'), pick(BODY_Z, INDEX))), [\n call('blood_hit', [0, 1.45, sub(pick(BODY_Z, INDEX), 0.1)], [0, 0.05, -1], mod(INDEX, 4), 0.8),\n cue('zombie_fall', { index: INDEX, delay: 0.05, duration: 0.6 }),\n after(\n add(TIME, 0.6),\n decal(pick(variable('bodiesX'), INDEX), 0, sub(pick(BODY_Z, INDEX), 1.2), 0.6, pick(BLOOD, mod(INDEX, 4)), { grow: 2, dark: 60, dry: 0x3b1d14 }),\n ),\n ]),\n ),\n when(lt(variable('bulletZ'), WALL_Z), [set('bulletOn', false), call('impact', 'concrete', [0, 1.45, -4.45], [0, 0, 1], 2.2)]),\n ]),\n ],\n}\n","import { add, gt, INDEX, lt, and, mul, TIME } from '../dsl'\nimport { after, call, cue, debris, emit, repeat, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst TIP = [-0.8, 1.38, 3.4 - 0.78] as const\nconst PORT = [-0.76, 1.4, 3.32] as const\nconst FORWARD = [0, 0, -1] as const\nconst UP = [0, 1, 0] as const\nconst BURST_SHOTS = 12\n\n/** W787-797: twelve rounds of an AK with their brass, the empty magazine falling at 1.8 s and a thread of smoke from the hot barrel. */\nexport const CASINGS_AND_MAGAZINE: EffectDef = {\n id: 'casings_and_magazine',\n pageId: 'capsulas',\n team: 'weapons',\n budget: { particles: 200, debris: 13 },\n loop: 4.6,\n view: { eye: [0.9, 1.3, 4.6], target: [-0.6, 0.6, 3.1] },\n setup: [cue('gun_visible', { visible: true }), cue('zombie_visible', { index: -1, visible: false })],\n timeline: [\n repeat(\n BURST_SHOTS,\n after(add(0.2, mul(INDEX, 0.1)), call('muzzle', 'ak', TIP, FORWARD), call('casing', PORT, ''), cue('gun_recoil', { kick: 0.05 })),\n ),\n after(1.8, debris({ p: [-0.8, 1.15, 3.3], v: [0.1, -0.5, 0.1], sp: 0, size: [0.05, 0.2, 0.09], c: 0x2a2a2a, spin: 3, life: 20, bounce: 0.2 })),\n ],\n tick: [\n when(and(gt(TIME, 1.4), lt(TIME, 3.6)), [\n emit('hot', 'normal', 10, { p: TIP, dir: UP, cone: 0.25, sp: [0.2, 0.5], life: [1.4, 2.2], size: [0.03, 0.06], grow: 5, c0: 0x9a9a96, a: 0.18, g: -0.2, fi: 0.2 }),\n ]),\n ],\n}\n","import { add, and, div, DELTA, FRESH, ge, gt, ite, lerp, lt, max, min, mul, not, scaled, sin, sub, TIME, variable, vadd, vlerp, vwith } from '../dsl'\nimport { call, cue, debris, burst, decal, emit, fireLight, fireLightOff, flash, repeat, set, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst FROM = [-4, 1.6, 5] as const\nconst TARGET = [0, 0, -0.5] as const\nconst UP = [0, 1, 0] as const\nconst FLIGHT = 1\nconst ARC_HEIGHT = 2.2\nconst GLASS_GREEN = 0x3a6a3a\nconst OBJECT_ID_DRAWS = 12\nconst at = (offset: readonly [number, number, number]) => vadd(TARGET, offset)\n\n/** W798-818 (`lab: 1`): a bottle in an arc with a wick burning, glass and a splash of fire on landing, and a pool of fire that spreads for 6 s. */\nexport const MOLOTOV: EffectDef = {\n id: 'molotov',\n pageId: 'molotov',\n team: 'weapons',\n lab: 'molotov',\n budget: { particles: 300, lights: 1 },\n loop: 9.5,\n view: { eye: [5, 3, 6.5], target: [0, 0.6, -0.5] },\n // three.js draws 12 random numbers (the object ids of the bottle's geometry, material and mesh) the first time the page builds the bottle\n setup: [cue('zombie_visible', { index: -1, visible: false }), when(FRESH, [cue('prop_created', { prop: 'bottle' }, OBJECT_ID_DRAWS)])],\n timeline: [set('landed', false), cue('bottle_visible', { visible: true })],\n tick: [\n when(lt(TIME, FLIGHT), [\n set('bottle', vwith(vlerp(FROM, TARGET, TIME), 1, add(lerp(FROM[1], TARGET[1], TIME), mul(sin(mul(TIME, Math.PI)), ARC_HEIGHT)))),\n cue('bottle_move', { position: variable('bottle'), spin: mul(10, DELTA) }),\n emit('wick', 'add', 40, { p: vadd(variable('bottle'), [0, 0.12, 0]), r: 0.03, sp: [0.1, 0.3], life: [0.15, 0.3], size: [0.08, 0.14], grow: 0.5, c0: 0xffd27a, c1: 0xff5010, fi: 0, g: -1 }),\n emit('wsm', 'normal', 14, { p: variable('bottle'), size: [0.06, 0.1], grow: 4, life: [0.8, 1.2], c0: 0x3a3530, a: 0.3, sp: [0, 0.2] }),\n ], [\n when(not(variable('landed')), [\n set('landed', true),\n cue('bottle_visible', { visible: false }),\n repeat(scaled(12), debris({ p: at([0, 0.1, 0]), dir: UP, cone: 1.3, sp: [1, 3], size: [0.04, 0.01, 0.04], c: GLASS_GREEN, life: 12 })),\n burst('add', 45, { p: at([0, 0.15, 0]), dir: UP, cone: 1.35, sp: [2, 4], drag: 2, life: [0.3, 0.6], size: [0.3, 0.5], c0: 0xffd27a, c1: 0xa02008, fi: 0.05 }),\n flash(at([0, 0.6, 0]), 0xff8a30, 6, 0.3, 12, true),\n decal(TARGET[0], 0, TARGET[2], 4.2, 0x0d0b09, { grow: 0.8, life: 60 }),\n ]),\n ]),\n when(and(gt(TIME, FLIGHT), lt(TIME, 8.2)), [\n set('poolRadius', min(2.2, add(0.5, mul(sub(TIME, 1), 2.1)))),\n set('poolFade', ite(gt(TIME, 7), max(0.1, sub(1, div(sub(TIME, 7), 1.2))), 1)),\n call('fire_emit', 'mol', at([0, 0.05, 0]), variable('poolRadius'), mul(1.6, variable('poolFade')), { smoke: 16 }),\n fireLight(0, at([0, 1, 0]), mul(3.2, variable('poolFade')), TIME),\n ], [\n when(ge(TIME, 8.2), [\n fireLightOff(0),\n emit('emb', 'add', 6, { p: TARGET, r: 2, flat: true, size: [0.04, 0.07], life: [0.6, 1.2], c0: 0xff6a20, a: 0.8, fi: 0.3 }),\n ]),\n ]),\n ],\n}\n","import { add, atan2, DELTA, FRESH, INDEX, lt, mul, neg, not, pick, sub, TIME, variable } from '../dsl'\nimport { call, cue, repeat, set, when } from '../steps'\nimport type { EffectDef } from '../types'\n\n/** The five bodies the blast reaches (W826), where they stand and the way each faces (`atan2(-x, -z)`, toward the origin). */\nconst BODIES = [[1.6, 0.8], [-1.8, 1.2], [0.6, -2], [-1, -1.8], [2.4, -1.2]] as const\nconst BODY_X = BODIES.map(([x]) => x)\nconst BODY_Z = BODIES.map(([, z]) => z)\nconst FACING = BODIES.map(([x, z]) => Math.atan2(-x, -z))\nconst FUSE = 1.8\nconst GRAVITY = 9.8\nconst FLOOR = 0.07\nconst BOUNCE = -0.4\nconst SIDE_LOSS = 0.5\n\n/** W819-830 (`lab: 1`): a grenade thrown in an arc, bouncing on the asphalt, then the Workshop's blast and the five bodies turning away to fall. */\nexport const GRENADE: EffectDef = {\n id: 'grenade',\n pageId: 'granada',\n team: 'weapons',\n lab: 'm67',\n budget: { particles: 400, debris: 16, decals: 1 },\n loop: 6.5,\n view: { eye: [7, 3.4, 7.5], target: [0, 0.8, -0.2] },\n setup: [\n cue('zombie_visible', { index: -1, visible: false }),\n // three.js draws 12 random numbers (the object ids of the grenade's geometry, material and mesh) the first time the page builds it\n when(FRESH, [cue('prop_created', { prop: 'grenade' }, 12)]),\n repeat(BODIES.length, cue('zombie_place', { index: INDEX, x: pick(BODY_X, INDEX), z: pick(BODY_Z, INDEX), turn: pick(FACING, INDEX) })),\n ],\n timeline: [\n set('px', -4), set('py', 1.4), set('pz', 4.5),\n set('vx', 3.2), set('vy', 2.4), set('vz', -3.6),\n set('boom', false),\n cue('grenade_visible', { visible: true }),\n ],\n tick: [\n when(lt(TIME, FUSE), [\n set('vy', sub(variable('vy'), mul(GRAVITY, DELTA))),\n set('px', add(variable('px'), mul(variable('vx'), DELTA))),\n set('py', add(variable('py'), mul(variable('vy'), DELTA))),\n set('pz', add(variable('pz'), mul(variable('vz'), DELTA))),\n when(lt(variable('py'), FLOOR), [\n set('py', FLOOR),\n set('vy', mul(variable('vy'), BOUNCE)),\n set('vx', mul(variable('vx'), SIDE_LOSS)),\n set('vz', mul(variable('vz'), SIDE_LOSS)),\n ]),\n cue('grenade_move', { position: [variable('px'), variable('py'), variable('pz')] }),\n ], [\n when(not(variable('boom')), [\n set('boom', true),\n cue('grenade_visible', { visible: false }),\n call('explode', [variable('px'), 0, variable('pz')], 1),\n repeat(\n BODIES.length,\n cue('zombie_turn_and_fall', {\n index: INDEX,\n turn: atan2(neg(sub(pick(BODY_X, INDEX), variable('px'))), neg(sub(pick(BODY_Z, INDEX), variable('pz')))),\n delay: 0.02,\n duration: 0.45,\n }),\n ),\n ]),\n ]),\n ],\n}\n","import { add, and, cos, gt, ge, INDEX, ite, lt, mod, mul, rnd, scaled, sin, sub, TIME, variable, vadd, concat } from '../dsl'\nimport { after, append, burst, call, cue, debris, emit, fireLight, fireLightOff, repeat, set, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst BARREL = [-4.4, 0, 1.4] as const\nconst UP = [0, 1, 0] as const\nconst RED = 0x9b1c18\nconst YELLOW = 0xd8b02a\nconst POOLS = 3\nconst above = (height: number) => vadd(BARREL, [0, height, 0])\n\n/** W831-845 (`lab: 1`): the barrel goes up in a mushroom, its lid and red scraps fly, three pools of petrol burn for 5 s and a black column marks it from afar. */\nexport const EXPLOSIVE_BARREL: EffectDef = {\n id: 'explosive_barrel',\n pageId: 'barril',\n team: 'weapons',\n lab: 'barrel',\n budget: { particles: 450, lights: 1 },\n loop: 9,\n view: { eye: [2.4, 3, 7.5], target: [-4.4, 1.6, 1.4] },\n setup: [cue('zombie_visible', { index: -1, visible: false })],\n timeline: [\n set('pools', []),\n after(\n 0.4,\n cue('barrel_visible', { visible: false }),\n call('explode', BARREL, 1.3, RED),\n burst('add', 30, { p: above(0.8), r: 0.4, dir: UP, cone: 0.25, sp: [3, 5], drag: 1.2, life: [1, 1.6], size: [0.9, 1.4], grow: 1.4, c0: 0xffb050, c1: 0x3a0d05, g: -1, fi: 0.05 }),\n debris({ p: above(1), v: [0.6, 9, 0.3], sp: 0, size: [0.7, 0.05, 0.7], c: RED, spin: 8, life: 12 }),\n repeat(scaled(8), debris({ p: BARREL, dir: UP, cone: 1, sp: [3, 7], size: [0.25, 0.04, 0.18], c: ite(mod(INDEX, 2), RED, YELLOW), life: 12 })),\n repeat(\n POOLS,\n set('poolAngle', add(mul(INDEX, 2.1), rnd(0, 0.6))),\n append('pools', vadd(BARREL, [mul(cos(variable('poolAngle')), 1.4), 0.05, mul(sin(variable('poolAngle')), 1.4)])),\n ),\n ),\n ],\n tick: [\n when(and(variable('pools'), gt(TIME, 0.5), lt(TIME, 6)), [\n set('poolFade', ite(gt(TIME, 5), sub(1, sub(TIME, 5)), 1)),\n repeat(POOLS, call('fire_emit', concat('bp', INDEX), { $: 'at', a: [variable('pools'), INDEX] }, 0.45, mul(0.6, variable('poolFade')), { smoke: 6 })),\n fireLight(0, above(1), mul(2.6, variable('poolFade')), TIME),\n ]),\n when(and(gt(TIME, 0.6), lt(TIME, 8)), [\n emit('col', 'normal', 6, { p: above(1.5), r: 0.6, dir: UP, cone: 0.2, sp: [1.5, 2.5], v: [0.4, 0, 0], drag: 0.05, life: [4, 6], size: [1.2, 1.8], grow: 3, c0: 0x1a1817, c1: 0x0c0d10, a: 0.5, fi: 0.2 }),\n ]),\n when(ge(TIME, 6), [fireLightOff(0)]),\n ],\n}\n","import { add, and, eq, INDEX, ite, mod, mul, ne, param, PARAM, pick, rnd, round, variable } from '../dsl'\nimport { after, call, repeat, set, spawn, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst WEAPONS = ['glock', 'ak', 'r870', 'barrett']\nconst TIP = [-0.8, 1.38, 3.4 - 0.78] as const\nconst PORT = [-0.76, 1.4, 3.32] as const\nconst FORWARD = [0, 0, -1] as const\nconst NORMAL = [0, 0, 1] as const\nconst WALL_Z = -4.45\nconst SHOTS = [[0, 0.45, 0.9], [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0], [0]]\nconst SHOT_TIME = pick(pick(SHOTS, PARAM), INDEX)\nconst AK = 1\nconst SHOTGUN = 2\nconst BARRETT = 3\n\n/** W745-761: core, petals and smoke scaled by weapon (the Barrett also vents sideways and lifts dust), with the shot's casing and hit on the wall. */\nexport const MUZZLE_FLASH: EffectDef = {\n id: 'muzzle_flash',\n pageId: 'muzzle',\n team: 'weapons',\n params: { id: 'weapon', options: ['glock', 'ak', 'r870', 'barrett'], default: 1 },\n budget: { particles: 60, lights: 1 },\n loop: 2.4,\n view: { eye: [1.8, 1.8, 5.2], target: [-0.8, 1.3, 1.4] },\n timeline: [\n repeat(\n param(3, 7, 1, 1),\n after(\n add(0.2, SHOT_TIME),\n call('muzzle', pick(WEAPONS, PARAM), TIP, FORWARD),\n when(ne(PARAM, SHOTGUN), [call('casing', PORT, ite(eq(PARAM, BARRETT), '50', ''))]),\n set('target', [add(-0.8, rnd(-0.4, 0.4)), add(1.4, rnd(-0.3, 0.3)), WALL_Z]),\n when(\n eq(PARAM, SHOTGUN),\n [repeat(8, call('impact', 'concrete', [add(-0.8, rnd(-0.5, 0.5)), add(1.4, rnd(-0.4, 0.4)), WALL_Z], NORMAL, 0.35))],\n [call('impact', 'concrete', variable('target'), NORMAL, ite(eq(PARAM, BARRETT), 2, 1))],\n ),\n when(and(eq(PARAM, AK), eq(mod(round(mul(SHOT_TIME, 10)), 3), 0)), [spawn('streak', { p: TIP, dir: FORWARD, cone: 0, sp: 90, life: 0.09, len: 0.025, c0: 0xffa040, a: 1 })]),\n ),\n ),\n when(eq(PARAM, SHOTGUN), [after(0.75, call('casing', PORT, 'shell'))]),\n ],\n}\n","import { rnd, scaled } from '../dsl'\nimport { after, behavior, burst, call, cue, debris, repeat, screen } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst FACING = [0, 0, 1] as const\n\n/** W846-855 (`vidro`): two cracks open in the shop window, then the whole pane comes down as 60 to 80 shards that stay on the pavement. */\nexport const SHOPFRONT_GLASS: EffectDef = {\n id: 'shopfront_glass',\n pageId: 'vidro',\n team: 'destruction',\n budget: { debris: 80, decals: 1 },\n loop: 5,\n view: { eye: [-2.4, 1.8, 1.2], target: [-5, 1.4, -4] },\n timeline: [\n after(0.3, call('impact', 'glass', [-5.4, 2.2, -4.45], FACING), behavior('cracks', 'crack', { x: 97, y: 59, big: false })),\n after(1, call('impact', 'glass', [-4.5, 1.6, -4.45], FACING), behavior('cracks', 'crack', { x: 177, y: 108, big: true })),\n after(\n 1.8,\n cue('glass_pane_gone'),\n screen({ add: { shake: 0.08 } }),\n repeat(\n scaled(75),\n debris({ p: [-5, 1.9, -4.42], box: [1.25, 0.85, 0.02], v: [rnd(-0.4, 0.4), rnd(-0.5, 0.8), rnd(0.6, 2)], sp: 0, size: [rnd(0.05, 0.18), 0.006, rnd(0.05, 0.18)], c: 0xa8c8d8, spin: 14, life: 30, bounce: 0.15 }),\n ),\n burst('add', 30, { p: [-5, 1.9, -4.3], box: [1.2, 0.8, 0.05], size: [0.02, 0.05], life: [0.2, 0.6], c0: 0xffffff, a: 0.9, fi: 0.3 }),\n burst('normal', 20, { p: [-5, 1.3, -4.2], box: [1.2, 0.5, 0.1], sp: [0.2, 0.6], drag: 2, life: [0.8, 1.4], size: [0.12, 0.2], grow: 3, c0: 0xbfd6e2, a: 0.2 }),\n ),\n ],\n}\n","import type { Vec3Tuple } from '../workshop/types'\n\n/** Vector helpers written as three.js r128 does them, so a recipe's numbers match the page's. */\nexport function normalized(vector: Vec3Tuple): Vec3Tuple {\n const length = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]) || 1\n const inverse = 1 / length\n return [vector[0] * inverse, vector[1] * inverse, vector[2] * inverse]\n}\n\nexport function plus(vector: Vec3Tuple, x: number, y: number, z: number): Vec3Tuple {\n return [vector[0] + x, vector[1] + y, vector[2] + z]\n}\n\nexport function scaledBy(vector: Vec3Tuple, factor: number): Vec3Tuple {\n return [vector[0] * factor, vector[1] * factor, vector[2] * factor]\n}\n\nexport function plusScaled(vector: Vec3Tuple, direction: Vec3Tuple, factor: number): Vec3Tuple {\n return [vector[0] + direction[0] * factor, vector[1] + direction[1] * factor, vector[2] + direction[2] * factor]\n}\n\nexport function negated(vector: Vec3Tuple): Vec3Tuple {\n return [-vector[0], -vector[1], -vector[2]]\n}\n","import { add, and, div, DELTA, ge, INDEX, min, mul, not, rnd, scaled, sub, variable } from '../dsl'\nimport { after, behavior, burst, cue, debris, repeat, screen, set, when } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef, EffectStep } from '../types'\n\nconst FRAME_DUST = normalized([0, -0.3, 1])\nconst SPLINTER_DIRECTION = normalized([0, 0.3, 1])\nconst FALL_SECONDS = 0.4\n\n/** One blow on the door (`hit`, W779): the frame sheds dust and splinters; a heavy blow sheds twice as much. */\nconst blow = (heavy: boolean): EffectStep[] => [\n cue('door_shake', { strength: 0.25 }),\n burst('normal', heavy ? 24 : 12, { p: [2, 2.3, -4.38], box: [0.6, 0.05, 0.02], dir: FRAME_DUST, cone: 0.6, sp: [0.2, 0.8], drag: 1.5, life: [0.8, 1.4], size: [0.08, 0.16], grow: 3, c0: 0x7a7366, a: 0.45, g: 0.4 }),\n repeat(scaled(heavy ? 5 : 2), debris({ p: [add(2, rnd(-0.5, 0.5)), rnd(0.4, 2), -4.36], dir: SPLINTER_DIRECTION, cone: 0.6, sp: [1, 2.5], size: [0.015, 0.015, 0.1], c: 0x7a5530, life: 12 })),\n]\n\n/** W856-871 (`porta`): blows loosen dust from the frame, a plank cracks at 66 %, falls at 33 % and the door drops at the third stage. */\nexport const DOOR_STAGES: EffectDef = {\n id: 'door_stages',\n pageId: 'porta',\n team: 'destruction',\n budget: { particles: 120, debris: 6 },\n loop: 7.5,\n view: { eye: [4.2, 1.7, 1.4], target: [2, 1.1, -4] },\n setup: [behavior('walkers', 'place', { index: 0, x: 2, z: -5.4, facing: 0 })],\n timeline: [\n after(0.5, ...blow(false)),\n after(1.2, ...blow(false)),\n after(1.9, ...blow(true), cue('door_plank_crack', { stage: 1 })),\n after(2.7, ...blow(false)),\n after(\n 3.3,\n ...blow(true),\n cue('door_plank_fall', { stage: 2 }),\n repeat(3, debris({ p: [add(2, mul(sub(INDEX, 1), 0.4)), 1.1, -4.3], v: [rnd(-0.3, 0.3), 0.5, 1.2], sp: 0, size: [0.38, 0.68, 0.07], c: 0x5a3c22, spin: 4, life: 20, bounce: 0.15 })),\n ),\n after(4.4, set('fall', 0), set('falling', true), cue('door_fall', { stage: 3 })),\n after(4.6, behavior('walkers', 'walk', { index: 0, to: [2, 0, -2.2], speed: 0.9 })),\n ],\n tick: [\n when(variable('falling'), [\n set('fall', add(variable('fall'), DELTA)),\n set('progress', min(1, div(variable('fall'), FALL_SECONDS))),\n when(and(ge(variable('progress'), 1), not(variable('landed'))), [\n set('landed', true),\n screen({ add: { shake: 0.2 } }),\n burst('normal', 45, { p: [2, 0.15, -3.4], box: [0.7, 0.05, 1], dir: [0, 1, 0], cone: 1.4, sp: [0.6, 2], drag: 1.8, life: [1.5, 2.8], size: [0.25, 0.5], grow: 3, c0: 0x6a645a, a: 0.45 }),\n ]),\n ]),\n ],\n}\n","import { abs, add, and, cos, DELTA, gt, local, lt, mul, rand, rnd, scaled, sin, TIME, variable, vnorm } from '../dsl'\nimport { after, cue, debris, emit, repeat, screen, spawn, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst CENTRE = [-1.4, 3.35, -3.75] as const\n\n/** W872-886 (`desaba`): dust trickles from the awning for a second as a warning, then the slab drops in blocks and a cloud rolls along the street. */\nexport const BUILDING_COLLAPSE: EffectDef = {\n id: 'building_collapse',\n pageId: 'desaba',\n team: 'destruction',\n budget: { particles: 350, debris: 26 },\n loop: 8,\n view: { eye: [4, 2.4, 5], target: [-1.4, 1.4, -2.8] },\n setup: [cue('zombie_visible', { index: -1, visible: false })],\n timeline: [\n after(\n 1.5,\n cue('slab_gone'),\n screen({ add: { shake: 0.3 } }),\n repeat(6, debris({ p: CENTRE, box: [2, 0.05, 0.6], v: [rnd(-0.5, 0.5), rnd(-0.5, 0), rnd(0, 0.8)], sp: 0, size: [rnd(0.5, 1.1), 0.25, rnd(0.4, 0.8)], c: 0x6e6a63, spin: 3, life: 30, bounce: 0.1 })),\n repeat(scaled(20), debris({ p: CENTRE, box: [2.1, 0.1, 0.7], v: [rnd(-1, 1), rnd(-1, 1), rnd(0, 1.5)], sp: 0, size: [0.12, 0.1, 0.12], k: [0.5, 1.5], c: 0x5e5a54, life: 30 })),\n ),\n after(\n 2.15,\n screen({ add: { shake: 0.45 } }),\n repeat(\n scaled(70),\n spawn('normal', local('angle', rnd(-0.4, 3.5), {\n p: [add(-1.4, rnd(-2, 2)), 0.2, add(-3.4, rnd(-0.5, 0.5))],\n dir: vnorm([mul(cos(variable('angle')), 0.6), 0.25, abs(sin(variable('angle')))]),\n cone: 0.5, sp: [1, 3.2], drag: 1.1, life: [3, 6], size: [0.7, 1.2], grow: 3, c0: 0x7a7268, c1: 0x4a4640, a: 0.5, fi: 0.12,\n })),\n ),\n ),\n ],\n tick: [\n when(and(gt(TIME, 0.3), lt(TIME, 1.5)), [\n emit('trk', 'normal', 30, { p: [add(-1.4, rnd(-2, 2)), 3.2, -3.05], dir: [0, -1, 0], cone: 0.1, sp: [0.2, 0.5], life: [1, 1.6], size: [0.04, 0.08], grow: 2.5, c0: 0x8a8478, a: 0.5, g: 1 }),\n when(lt(rand(), mul(DELTA, 6)), [debris({ p: [add(-1.4, rnd(-2, 2)), 3.2, -3.05], sp: 0.2, size: [0.04, 0.04, 0.04], c: 0x6e6a63, life: 10 })]),\n ]),\n ],\n}\n","import { add, cos, INDEX, local, mul, rnd, scaled, sin, variable, vnorm } from '../dsl'\nimport { after, behavior, burst, call, cue, debris, repeat, screen, spawn } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef } from '../types'\n\nconst FIRING_FROM = [0.6, 1.3, 7] as const\nconst FIRING_DIRECTION = normalized([-2.8, -0.4, -4.4])\nconst WALL_CENTRE = [-2.2, 1, 2.6] as const\n\n/** W1039-1053 (`parede`): a burst of AK fire leaves holes that are stuck to the blocks, then the wall gives way and the holes fall with them. */\nexport const BLOCK_WALL: EffectDef = {\n id: 'block_wall',\n pageId: 'parede',\n team: 'destruction',\n budget: { particles: 300, debris: 16 },\n loop: 8,\n view: { eye: [0.4, 1.7, 6.6], target: [-2.2, 0.9, 2.4] },\n props: ['blocks'],\n setup: [cue('zombie_visible', { index: -1, visible: false }), cue('work_light', { intensity: 2.2 })],\n timeline: [\n repeat(\n 14,\n after(\n add(0.3, mul(INDEX, 0.11)),\n call('muzzle', 'ak', FIRING_FROM, FIRING_DIRECTION),\n behavior('blocks', 'hit', { x: rnd(-1.3, 1.3), y: rnd(0.4, 1.9), scale: 1 }),\n ),\n ),\n after(2.2, behavior('blocks', 'hit', { x: -0.2, y: 1.1, scale: 2.2 })),\n after(\n 3,\n screen({ add: { shake: 0.4 } }),\n behavior('blocks', 'collapse', { x: -0.2, y: 1.1 }),\n burst('normal', 30, { p: WALL_CENTRE, box: [1.4, 0.8, 0.2], sp: [0.3, 1.2], drag: 1.5, life: [1.5, 2.6], size: [0.4, 0.7], grow: 3, c0: 0x8a857a, c1: 0x4a4640, a: 0.45 }),\n repeat(scaled(16), debris({ p: WALL_CENTRE, box: [1.3, 0.8, 0.1], sp: [1, 3], size: [0.08, 0.07, 0.08], k: [0.5, 1.5], c: 0x6e6a63, life: 20 })),\n ),\n after(\n 3.6,\n screen({ add: { shake: 0.25 } }),\n repeat(\n scaled(60),\n spawn('normal', local('angle', rnd(0, 6.283), {\n p: [add(-2.2, rnd(-1.4, 1.4)), 0.2, add(2.4, rnd(-0.6, 0.4))],\n dir: vnorm([cos(variable('angle')), 0.3, sin(variable('angle'))]),\n cone: 0.4, sp: [0.8, 2.4], drag: 1.3, life: [2.5, 4.5], size: [0.6, 1], grow: 3, c0: 0x7a7268, c1: 0x4a4640, a: 0.45, fi: 0.12,\n })),\n ),\n ),\n ],\n}\n","import type { Vec3Tuple } from '../../workshop/types'\n\n/** The free spot of the Workshop's set where the wall, door, railing, fence, table and cylinders are put one at a time (W937, `SPOT`). */\nexport const SPOT: Vec3Tuple = [-2.2, 0, 2.6]\nexport const UP: Vec3Tuple = [0, 1, 0]\n","import { add, and, DELTA, div, floor, ge, INDEX, ite, min, mod, mul, not, pick, scaled, variable } from '../dsl'\nimport { after, behavior, burst, call, cue, debris, repeat, screen, set, when } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef } from '../types'\nimport { SPOT, UP } from './spot'\n\nconst HITS = [[0.4, 1.4], [0.75, 1.0], [0.3, 0.7], [0.6, 1.7], [0.5, 1.2]]\nconst HIT_X = HITS.map(([x]) => x)\nconst HIT_Y = HITS.map(([, y]) => y)\nconst SPARK_DIRECTION = normalized([0, 0.3, 1])\nconst CHIP_DIRECTION = normalized([0, 0.2, 1])\nconst HINGE_DIRECTION = normalized([1, 0.3, 1])\nconst SCRAPE_DIRECTION = normalized([0, 0.4, 1])\nconst FALL_SECONDS = 0.45\nconst SCRAPES = 5\n\n/** W960-985 (`portametal`): each blow dents the plate where it lands and throws sparks, paint chips and rust; the top hinge bursts and the door scrapes down. */\nexport const METAL_DOOR_EFFECT: EffectDef = {\n id: 'metal_door',\n pageId: 'portametal',\n team: 'destruction',\n budget: { particles: 160, debris: 20 },\n loop: 7.5,\n view: { eye: [0.6, 1.6, 6.2], target: [-2.2, 1.1, 2.6] },\n props: ['plate'],\n setup: [\n cue('zombie_visible', { index: -1, visible: false }),\n cue('work_light', { intensity: 2.2 }),\n behavior('walkers', 'place', { index: 3, x: SPOT[0] + 0.1, z: SPOT[2] - 0.7, facing: 0 }),\n ],\n timeline: [\n repeat(\n HITS.length,\n after(\n add(0.5, mul(INDEX, 0.75)),\n cue('zombie_swing', { index: 3, side: mod(INDEX, 2) }),\n after(\n add(0.5, mul(INDEX, 0.75), 0.15),\n behavior('plate', 'dent', { x: pick(HIT_X, INDEX), y: pick(HIT_Y, INDEX), radius: 0.42, depth: add(0.09, mul(INDEX, 0.025)) }),\n set('spot', [add(SPOT[0] - 0.56, pick(HIT_X, INDEX)), pick(HIT_Y, INDEX), SPOT[2] + 0.06]),\n call('spark_at', variable('spot'), SPARK_DIRECTION, 8),\n repeat(scaled(5), debris({ p: variable('spot'), dir: CHIP_DIRECTION, cone: 0.9, sp: [0.8, 2.2], size: [0.035, 0.004, 0.03], k: [0.6, 1.4], c: ite(mod(INDEX, 2), 0x51625a, 0x8a8f86), life: 20, spin: 16 })),\n call('rust_at', variable('spot'), 6),\n cue('door_dent', { level: min(3, add(1, floor(div(INDEX, 1.7)))) }),\n ),\n ),\n ),\n after(4.4, call('spark_at', [SPOT[0] - 0.6, 2.05, SPOT[2] + 0.05], HINGE_DIRECTION, 30, 1.2), screen({ add: { shake: 0.15 } }), set('fall', 0), set('falling', true), cue('door_hinge_burst')),\n after(5.2, behavior('walkers', 'walk', { index: 3, to: [SPOT[0], 0, SPOT[2] + 2.4], speed: 0.8 })),\n ],\n tick: [\n when(variable('falling'), [\n set('fall', add(variable('fall'), DELTA)),\n set('progress', min(1, div(variable('fall'), FALL_SECONDS))),\n when(and(ge(variable('progress'), 1), not(variable('landed'))), [\n set('landed', true),\n screen({ add: { shake: 0.3 } }),\n repeat(SCRAPES, call('spark_at', [add(SPOT[0] - 0.5, mul(INDEX, 0.27)), 0.03, SPOT[2] + 2.1], SCRAPE_DIRECTION, 8, 1)),\n burst('normal', 40, { p: [SPOT[0], 0.15, SPOT[2] + 1.2], box: [0.6, 0.05, 1], dir: UP, cone: 1.4, sp: [0.6, 2], drag: 1.8, life: [1.4, 2.4], size: [0.25, 0.5], grow: 3, c0: 0x6a645a, a: 0.45 }),\n ]),\n ]),\n ],\n}\n","import { add, and, DELTA, div, ge, gt, INDEX, lt, min, mul, not, pick, rnd, sub, TIME, variable } from '../dsl'\nimport { after, behavior, burst, call, cue, emit, repeat, screen, set, when } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef } from '../types'\nimport { SPOT, UP } from './spot'\n\n/** Where the four pushing bodies stand, relative to the railing (W987). */\nconst PUSHERS = [[-0.6, 0.45], [0.1, 0.5], [0.6, 0.42], [-0.1, 1]]\nconst BEND_LIMIT = 0.42\nconst BEND_STEP = 0.004\nconst FALL_SECONDS = 0.55\nconst WELD = normalized([0, 0.3, 1])\n\n/** W986-1003 (`grade`): the horde pushes, the bars bend and rust falls in a thread; the weld bursts in sparks twice and the railing topples. */\nexport const IRON_RAILING: EffectDef = {\n id: 'iron_railing',\n pageId: 'grade',\n team: 'destruction',\n budget: { particles: 200 },\n loop: 7.5,\n view: { eye: [1.4, 1.8, 6.4], target: [-2.2, 0.7, 2.4] },\n setup: [\n cue('zombie_visible', { index: -1, visible: false }),\n cue('work_light', { intensity: 2.2 }),\n set('bend', 0),\n ...PUSHERS.map(([x, dz], index) => behavior('walkers', 'place', { index, x: SPOT[0] + x!, z: SPOT[2] - dz!, facing: 0 })),\n ],\n timeline: [\n after(1.6, call('spark_at', [SPOT[0], 0.75, SPOT[2] + 0.05], WELD, 20, 1), screen({ add: { shake: 0.08 } }), cue('weld_burst')),\n after(3.2, call('spark_at', [SPOT[0] + 0.02, 1.2, SPOT[2] + 0.1], WELD, 20, 1), screen({ add: { shake: 0.1 } })),\n after(\n 4.3,\n set('fall', 0),\n set('falling', true),\n call('spark_at', [SPOT[0] - 1.5, 0.25, SPOT[2]], normalized([-0.3, 0.5, 1]), 14, 1),\n call('spark_at', [SPOT[0] + 1.5, 0.25, SPOT[2]], normalized([0.3, 0.5, 1]), 14, 1),\n cue('railing_gave_way'),\n ),\n after(\n 4.9,\n repeat(PUSHERS.length, behavior('walkers', 'walk', { index: INDEX, to: [add(SPOT[0], pick(PUSHERS.map(([x]) => x), INDEX)), 0, add(SPOT[2], 2.6, mul(INDEX, 0.3))], speed: 0.7 })),\n ),\n ],\n tick: [\n when(lt(TIME, 4.3), [\n set('target', min(BEND_LIMIT, mul(TIME, 0.1))),\n when(gt(sub(variable('target'), variable('bend')), BEND_STEP), [set('bend', variable('target')), cue('railing_bend', { amount: variable('bend') })]),\n emit('rust', 'normal', 10, { p: [add(SPOT[0], rnd(-0.6, 0.6)), rnd(0.3, 1.2), add(SPOT[2], mul(variable('bend'), 0.8))], sp: [0, 0.2], life: [0.8, 1.2], size: [0.015, 0.03], c0: 0x7a4a2a, a: 0.9, g: 6, fi: 0, gnd: 1 }),\n ]),\n when(variable('falling'), [\n set('fall', add(variable('fall'), DELTA)),\n set('progress', min(1, div(variable('fall'), FALL_SECONDS))),\n when(and(ge(variable('progress'), 1), not(variable('landed'))), [\n set('landed', true),\n screen({ add: { shake: 0.25 } }),\n repeat(4, call('spark_at', [add(SPOT[0] - 1.2, mul(INDEX, 0.8)), 0.04, SPOT[2] + 1.2], normalized([0, 0.5, 1]), 6, 1)),\n burst('normal', 30, { p: [SPOT[0], 0.12, SPOT[2] + 0.8], box: [1.4, 0.05, 0.5], dir: UP, cone: 1.4, sp: [0.5, 1.6], drag: 1.8, life: [1.2, 2.2], size: [0.2, 0.4], grow: 3, c0: 0x6a645a, a: 0.4 }),\n ]),\n ]),\n ],\n}\n","import { add, and, DELTA, div, ge, INDEX, min, mul, not, pick, rnd, scaled, sub, variable, vadd, mod } from '../dsl'\nimport { after, behavior, burst, cue, debris, repeat, screen, set, when } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef } from '../types'\nimport { SPOT, UP } from './spot'\n\n/** The order the planks break in and where each stands along the fence (W1005-1006). */\nconst ORDER = [3, 4, 8, 5, 9, 2, 7]\nconst PLANK_X = Array.from({ length: 12 }, (_, index) => -1.43 + index * 0.26)\nconst BODIES = [[-0.8, 0.6], [0.5, 0.7], [0, 1.2]]\nconst SPLINTER_DIRECTION = normalized([0, 0.4, 1])\nconst FALL_SECONDS = 0.5\nconst PLANK_HEIGHT = 1.3\n\n/** W1004-1020 (`cerca`): the planks snap one by one at a random height, the stump stays on the rail and the rest flies; the section finally tips over. */\nexport const WOODEN_FENCE: EffectDef = {\n id: 'wooden_fence',\n pageId: 'cerca',\n team: 'destruction',\n budget: { particles: 150, debris: 12 },\n loop: 7.5,\n view: { eye: [1, 1.6, 6], target: [-2.2, 0.7, 2.4] },\n setup: [\n cue('zombie_visible', { index: -1, visible: false }),\n cue('work_light', { intensity: 2.2 }),\n ...BODIES.map(([x, dz], index) => behavior('walkers', 'place', { index, x: SPOT[0] + x!, z: SPOT[2] - dz!, facing: 0 })),\n ],\n timeline: [\n repeat(\n ORDER.length,\n after(\n add(0.4, mul(INDEX, 0.5)),\n set('cut', rnd(0.32, 0.6)),\n set('top', sub(PLANK_HEIGHT, variable('cut'))),\n set('break', [add(SPOT[0], pick(PLANK_X, pick(ORDER, INDEX))), variable('cut'), SPOT[2]]),\n cue('zombie_swing', { index: mod(INDEX, 3), side: mod(INDEX, 2) }),\n cue('plank_snap', { plank: pick(ORDER, INDEX), cut: variable('cut') }),\n debris({ p: vadd(variable('break'), [0, div(variable('top'), 2), 0]), v: [rnd(-0.8, 0.8), rnd(1.5, 3), rnd(1.5, 3)], sp: 0, size: [0.17, variable('top'), 0.035], c: 0x8a6a44, spin: 7, life: 20, bounce: 0.2 }),\n repeat(scaled(6), debris({ p: variable('break'), dir: SPLINTER_DIRECTION, cone: 0.9, sp: [1, 3], size: [0.012, 0.012, 0.1], k: [0.6, 1.4], c: 0xb08a5a, life: 20 })),\n burst('normal', 8, { p: variable('break'), dir: [0, 0, 1], cone: 0.8, sp: [0.4, 1.4], drag: 2, life: [0.8, 1.4], size: [0.1, 0.18], grow: 3, c0: 0x7a6048, a: 0.4 }),\n ),\n ),\n after(\n 4.2,\n set('fall', 0),\n set('falling', true),\n repeat(scaled(10), debris({ p: [add(SPOT[0], rnd(-1.4, 1.4)), 1, SPOT[2]], dir: SPLINTER_DIRECTION, cone: 0.9, sp: [1, 3], size: [0.012, 0.012, 0.1], c: 0xb08a5a, life: 20 })),\n cue('fence_rail_cracked'),\n ),\n after(4.8, repeat(BODIES.length, behavior('walkers', 'walk', { index: INDEX, to: [add(SPOT[0], pick(BODIES.map(([x]) => x), INDEX)), 0, SPOT[2] + 2.5], speed: 0.75 }))),\n ],\n tick: [\n when(variable('falling'), [\n set('fall', add(variable('fall'), DELTA)),\n set('progress', min(1, div(variable('fall'), FALL_SECONDS))),\n when(and(ge(variable('progress'), 1), not(variable('landed'))), [\n set('landed', true),\n screen({ add: { shake: 0.2 } }),\n burst('normal', 35, { p: [SPOT[0], 0.12, SPOT[2] + 0.8], box: [1.5, 0.05, 0.5], dir: UP, cone: 1.4, sp: [0.5, 1.6], drag: 1.8, life: [1.2, 2.2], size: [0.25, 0.45], grow: 3, c0: 0x6a5a48, a: 0.4 }),\n ]),\n ]),\n ],\n}\n","import { add, INDEX, mul, rnd, variable, vadd } from '../dsl'\nimport { after, call, cue, debris, burst, repeat, screen, set } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef } from '../types'\nimport { SPOT, UP } from './spot'\n\nconst at = (x: number, y: number, z: number): readonly [number, number, number] => [SPOT[0] + x, SPOT[1] + y, SPOT[2] + z]\n/**\n * Where each part of the table is in the world. The page asks its scene graph for it (`tbW`), and the capture does not render, so the table's tilt never\n * reaches the world matrix it reads: the parts are always where the table stands level (W1009, recorded at 2.2 s, 3.8 s and 5.3 s).\n */\nconst FRONT_LEG = at(-0.52, 0.2, 0.32)\nconst TOP_CORNER = at(0.3, 0.78, 0.2)\nconst TOP_PIECE = at(-0.3, 0.78, -0.2)\nconst FAR_CORNER = at(0.3, 0.78, -0.2)\nconst HOLE_OBJECT_ID_DRAWS = 12\nconst HOLES = 9\nconst GLOCK_FROM = [-0.4, 1, 6] as const\nconst SHOTGUN_FROM = [0.2, 1.1, 4.6] as const\nconst toward = (target: readonly [number, number, number], from: readonly [number, number, number]) => normalized([target[0] - from[0], target[1] - from[1], target[2] - from[2]])\n\n/** W1008-1020 (`pontos`): a 9 mm in the leg, a machete on a corner, a bat on the top and a 12-gauge up close: each breaks only the part it hit. */\nexport const HIT_POINT_BREAKAGE: EffectDef = {\n id: 'hit_point_breakage',\n pageId: 'pontos',\n team: 'destruction',\n budget: { particles: 120, debris: 20 },\n loop: 7.5,\n view: { eye: [0.2, 1.6, 5.4], target: [-2.2, 0.6, 2.6] },\n setup: [cue('zombie_visible', { index: -1, visible: false }), cue('work_light', { intensity: 2.2 })],\n timeline: [\n after(\n 0.6,\n call('muzzle', 'glock', GLOCK_FROM, toward(FRONT_LEG, GLOCK_FROM)),\n call('chips', FRONT_LEG, normalized([0, 0.2, 1]), 6),\n cue('table_part_detached', { part: 'front_leg' }),\n debris({ p: FRONT_LEG, v: [0.4, 0.6, 1.4], sp: 0, size: [0.07, 0.4, 0.07], c: 0x7a5530, spin: 6, life: 20, bounce: 0.25 }),\n cue('table_tilt'),\n ),\n after(\n 2.2,\n repeat(8, call('chips', vadd(TOP_CORNER, [add(-0.3, mul(INDEX, 0.08)), 0.03, 0]), normalized([0, 0.6, 1]), 1, 0.4)),\n cue('table_part_detached', { part: 'top_corner' }),\n debris({ p: TOP_CORNER, v: [1.2, 1.5, 1], sp: 0, size: [0.6, 0.06, 0.4], c: 0x7a5530, spin: 6, life: 20, bounce: 0.25 }),\n ),\n after(\n 3.8,\n screen({ add: { shake: 0.12 } }),\n call('chips', TOP_PIECE, UP, 10, 1.3),\n cue('table_part_gone', { part: 'top_piece' }),\n repeat(4, debris({ p: [add(TOP_PIECE[0], rnd(-0.2, 0.2)), TOP_PIECE[1], add(TOP_PIECE[2], rnd(-0.1, 0.1))], dir: UP, cone: 1.2, sp: [1, 2.5], size: [rnd(0.15, 0.3), 0.06, rnd(0.12, 0.25)], c: 0x6b4a2a, spin: 8, life: 20, bounce: 0.2 })),\n burst('normal', 20, { p: TOP_PIECE, r: 0.2, sp: [0.3, 1], drag: 2, life: [1, 1.8], size: [0.2, 0.35], grow: 3, c0: 0x7a6048, a: 0.4 }),\n ),\n after(\n 5.3,\n call('muzzle', 'r870', SHOTGUN_FROM, toward(FAR_CORNER, SHOTGUN_FROM)),\n repeat(HOLES, set('holeX', rnd(-0.15, 0.15)), set('holeZ', rnd(-0.1, 0.1)), cue('table_hole', { x: variable('holeX'), z: variable('holeZ') }, HOLE_OBJECT_ID_DRAWS)),\n call('chips', FAR_CORNER, UP, 10, 1),\n ),\n ],\n}\n","import { abs, clamp, DELTA, div, eq, gt, lt, min, mul, not, PARAM, query, sub, TIME, variable, vx, vz } from '../dsl'\nimport { after, behavior, call, cue, emit, set, screen, when } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef } from '../types'\nimport { SPOT, UP } from './spot'\n\nconst OXYGEN = 1\nconst PUNCTURE_AT = 0.6\nconst JET_SECONDS = 3.4\nconst CLOUD_UNTIL = 7\nconst SHOT_FROM = [0.4, 1.2, 6.4] as const\nconst SHOT_DIRECTION = normalized([-0.5, -0.15, -1])\nconst SCRAPE_DIRECTION = normalized([1, 0.4, 0])\nconst jetStrength = variable('jet')\nconst isOxygen = eq(PARAM, OXYGEN)\n\n/** W1021-1038 (`pressao`): a shot into an extinguisher (a white cloud and a spin) or into an oxygen bottle (the valve breaks and it flies off as a rocket). */\nexport const PRESSURE_CYLINDERS: EffectDef = {\n id: 'pressure_cylinders',\n pageId: 'pressao',\n team: 'destruction',\n params: { id: 'cylinder', options: ['extinguisher', 'oxygen'], default: 0 },\n budget: { particles: 250 },\n loop: 7,\n view: { eye: [1.6, 1.5, 6.4], target: [-1.6, 0.4, 2.4] },\n props: ['cylinder'],\n setup: [cue('zombie_visible', { index: -1, visible: false }), cue('work_light', { intensity: 2.2 }), behavior('cylinder', 'choose', { oxygen: isOxygen }), cue('cylinder_visible', { oxygen: isOxygen })],\n timeline: [\n after(\n PUNCTURE_AT,\n call('muzzle', 'glock', SHOT_FROM, SHOT_DIRECTION),\n screen({ add: { shake: 0.1 } }),\n behavior('cylinder', 'puncture'),\n when(isOxygen, [call('spark_at', [SPOT[0], 1.3, SPOT[2]], UP, 20, 1.2)]),\n ),\n ],\n tick: [\n when(query('cylinder', 'live'), [\n set('jet', clamp(sub(1, div(sub(TIME, PUNCTURE_AT), JET_SECONDS)), 0, 1)),\n behavior('cylinder', 'step', { dt: DELTA, t: TIME }),\n when(not(isOxygen), [\n when(gt(jetStrength, 0), [\n emit('jet', 'normal', 60, { p: query('cylinder', 'tip'), dir: query('cylinder', 'direction'), cone: 0.3, sp: [3, 6], drag: 2.5, life: [2, 3.5], size: [0.15, 0.3], grow: 6, c0: 0xeeeeea, c1: 0xbfc3c8, a: 0.45, g: -0.05, fi: 0.05 }),\n ]),\n when(lt(TIME, CLOUD_UNTIL), [\n emit('cloud', 'normal', 4, { p: [SPOT[0], 0.6, SPOT[2]], r: 2.5, flat: true, sp: [0.05, 0.2], life: [3, 5], size: [1.5, 2.5], grow: 1.3, c0: 0xe2e2de, a: mul(0.18, min(1, sub(TIME, PUNCTURE_AT))), fi: 0.3 }),\n ]),\n ], [\n when(gt(jetStrength, 0), [\n emit('jet', 'normal', 50, { p: query('cylinder', 'back'), dir: [1, 0, 0], cone: 0.25, sp: [2, 4], drag: 3, life: [0.5, 0.9], size: [0.1, 0.2], grow: 4, c0: 0xdde6ee, a: 0.2 }),\n ]),\n when(gt(abs(query('cylinder', 'speed_x')), 0.5), [\n emit('scr', 'streak', 120, { p: [vx(query('cylinder', 'position')), 0.03, vz(query('cylinder', 'position'))], dir: SCRAPE_DIRECTION, cone: 0.6, sp: [2, 6], v: [mul(query('cylinder', 'speed_x'), 0.5), 0, 0], life: [0.2, 0.45], len: 0.02, c0: 0xfff0c0, c1: 0xff5010, g: 9.8, gnd: 2 }),\n ]),\n ]),\n ]),\n ],\n}\n","import { add, concat, eq, INDEX, pick, TIME, vadd } from '../dsl'\nimport { call, cue, fireLight, repeat, when } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef } from '../types'\n\n/** The two burning windows of the shop front (W73). */\nconst WINDOWS = [[4.6, 5, -4.48], [-1.6, 5, -4.48]]\n\n/** W1054-1068 (`incendio`): fire out of the top of each window, licking up the facade, embers and smoke carried by the wind, and a light that paints the street. */\nexport const BUILDING_FIRE: EffectDef = {\n id: 'building_fire',\n pageId: 'incendio',\n team: 'world',\n budget: { particles: 600, lights: 2 },\n loop: 0,\n view: { eye: [3, 2.4, 6.5], target: [1.5, 4, -4] },\n setup: [cue('zombie_visible', { index: -1, visible: false })],\n timeline: [],\n tick: [\n repeat(\n WINDOWS.length,\n call('fire_emit', concat('w', INDEX), vadd(pick(WINDOWS, INDEX), [0, 0.55, 0.12]), 0.55, 1.1, { dir: normalized([0, 1, 0.35]), wind: [0.9, 0, 0], smoke: 22, smokeAlpha: 0.5 }),\n when(eq(INDEX, 0), [fireLight(0, vadd(pick(WINDOWS, INDEX), [0, 0, 1.4]), 3, add(TIME, INDEX))], [fireLight(1, vadd(pick(WINDOWS, INDEX), [0, 0, 1.4]), 3, add(TIME, INDEX))]),\n ),\n ],\n}\n","import { INDEX, pick, rnd, variable } from '../dsl'\nimport { behavior, cue, emit, repeat, set } from '../steps'\nimport type { EffectDef } from '../types'\n\n/** Where the three zombies stand in the fog (W1084). */\nconst SPOTS_X = [-3, 1, 5]\nconst SPOTS_Z = [-1, -2.4, 0.4]\n\n/**\n * W1073-1080 (the page's `neblina`): a low layer of mist that drifts slowly, fine ash falling in a spiral and the lamp's cone of light, with three\n * zombies standing in the fog (each turned at random). The fog density (0.075) and the cone are cues for whoever owns the scene.\n */\nexport const FOG_AND_ASH: EffectDef = {\n id: 'fog_and_ash',\n pageId: 'neblina',\n team: 'world',\n budget: { particles: 120, lights: 1 },\n loop: 0,\n view: { eye: [3, 1.8, 7.5], target: [-3, 1.2, -1] },\n setup: [\n cue('fog', { density: 0.075 }),\n cue('lamp_cone', { visible: true }),\n cue('zombie_visible', { index: -1, visible: false }),\n repeat(\n SPOTS_X.length,\n set('turn', rnd(-0.6, 0.6)),\n behavior('walkers', 'place', { index: INDEX, x: pick(SPOTS_X, INDEX), z: pick(SPOTS_Z, INDEX), facing: variable('turn') }),\n ),\n ],\n timeline: [],\n tick: [\n emit('fog', 'normal', 5, { p: [rnd(-14, 10), rnd(0.2, 1.1), rnd(-3, 8)], v: [rnd(0.15, 0.35), 0, rnd(-0.05, 0.05)], sp: 0, life: [9, 13], size: [3, 5], grow: 1.4, c0: 0x7d8aa0, a: 0.13, fi: 0.35 }),\n emit('ash', 'normal', 14, { p: [rnd(-10, 8), rnd(4, 6), rnd(-3, 7)], v: [rnd(0.1, 0.4), -0.35, rnd(-0.1, 0.1)], sp: [0.05, 0.15], life: [8, 12], size: [0.025, 0.04], c0: 0xa09a90, a: 0.75, fi: 0.1, gnd: 1 }),\n ],\n}\n","import type { WorkshopEngine } from './engine'\nimport type { Vec3Tuple } from './types'\n\n/** The most drops the field keeps (`RAINMAX`, W312). */\nexport const RAIN_CAPACITY = 4000\n/** Metres per second a drop falls (W322: `15 * dt`). */\nconst FALL_SPEED = 15\n/** How far the wind slants a drop per metre it falls (W322: `.07`), and the slant of the streak itself (W321: `.04`). */\nconst WIND_PER_FALL = 0.07\nconst STREAK_SLANT = 0.04\nconst STREAK_LENGTH = 0.6\n/** Where a drop starts, in a box of the field's own: x -15..15, z -6..16, height 0..16 at first and 14..17 when it comes back (W321, W328). */\nconst BOX_X: readonly [number, number] = [-15, 15]\nconst BOX_Z: readonly [number, number] = [-6, 16]\nconst START_HEIGHT: readonly [number, number] = [0, 16]\nconst REENTRY_HEIGHT: readonly [number, number] = [14, 17]\n/** A drop that lands makes a splash 35% of the time, within `12 / (1 + quality)` metres of the camera (W324). */\nconst SPLASH_CHANCE = 0.35\nconst SPLASH_RANGE = 12\nconst SPRITE_CHANCE = 0.5\nconst UP: Vec3Tuple = [0, 1, 0]\n\n/** An ellipse of the ground a puddle covers: centre x, centre z and the two half-axes (W332 `PUD`). */\nexport type Puddle = readonly [x: number, z: number, radiusX: number, radiusZ: number]\n\n/** The Workshop's four puddles (W332): the rings of the rain show only in them. */\nexport const WORKSHOP_PUDDLES: readonly Puddle[] = [\n [-1.5, 2.2, 1.6, 0.9],\n [3.0, 3.6, 1.1, 0.7],\n [-6, 3.2, 1.3, 0.8],\n [1.4, -2.2, 0.9, 0.5],\n]\n\nexport const inPuddle = (puddles: readonly Puddle[], x: number, z: number): boolean => puddles.some((p) => ((x - p[0]) / p[2]) ** 2 + ((z - p[1]) / p[3]) ** 2 < 1)\n\n/**\n * The Workshop's rain (W313-331): up to 4000 streaks (two points each, in a Float32Array as the page keeps them) that fall in a box, slanted by the\n * wind. A streak that reaches the ground may leave a splash (a sprite of the `normal` pool, and a ring where there is a puddle) when it is near the\n * camera, and comes back at the top. It draws its random numbers in the page's order, so the weather and the Workshop see the same rain.\n */\nexport class RainField {\n /** Two points per streak: x, y, z of the top and x, y, z of the bottom. */\n readonly positions = new Float32Array(RAIN_CAPACITY * 6)\n /** Streaks alive. */\n count = 0\n /** The slant (the wind along x in the Workshop's units). */\n wind = 1\n on = false\n /** Where the box is centred (the Workshop leaves it at the origin; a game follows the camera). */\n centerX = 0\n centerZ = 0\n puddles: readonly Puddle[] = WORKSHOP_PUDDLES\n /** Replaces the puddle test (a game knows its own wet spots). */\n puddleAt: ((x: number, z: number) => boolean) | undefined\n\n constructor(private readonly engine: WorkshopEngine) {}\n\n /** `initRain` (W321): `count` streaks (thinned by the quality), each drawn at a random place in the box. */\n init(count: number, wind: number): void {\n const { random } = this.engine\n this.count = Math.min(RAIN_CAPACITY, Math.round(count * this.engine.context.quality.mult))\n this.wind = wind\n this.on = true\n for (let index = 0; index < this.count; index++) {\n const offset = index * 6\n const x = random.between(BOX_X[0], BOX_X[1])\n const y = random.between(START_HEIGHT[0], START_HEIGHT[1])\n const z = random.between(BOX_Z[0], BOX_Z[1])\n this.set(offset, x, y, z)\n }\n }\n\n /**\n * What a game does with the field every frame: the box follows the camera (`centerX`, `centerZ`), the streaks thin or thicken to `count` (a new streak\n * starts at a random place of the box, as `init` draws it) and the wind slants them. The golden never calls it.\n */\n drive(count: number, wind: number, centerX: number, centerZ: number): void {\n const { random } = this.engine\n const target = Math.max(0, Math.min(RAIN_CAPACITY, Math.round(count * this.engine.context.quality.mult)))\n this.wind = wind\n this.centerX = centerX\n this.centerZ = centerZ\n this.on = target > 0\n for (let index = this.count; index < target; index++) {\n this.set(index * 6, random.between(BOX_X[0], BOX_X[1]) + centerX, random.between(START_HEIGHT[0], START_HEIGHT[1]), random.between(BOX_Z[0], BOX_Z[1]) + centerZ)\n }\n this.count = target\n }\n\n /** `updateRain` (W322-329). `camera` is where the eye is; `splash` lets the drops that land leave a splash. */\n update(dt: number, splash: boolean, camera: Vec3Tuple): void {\n if (!this.on) return\n const { random, context } = this.engine\n const positions = this.positions\n const fall = FALL_SPEED * dt\n const slant = this.wind * fall * WIND_PER_FALL\n const reach = SPLASH_RANGE / (1 + context.quality.level)\n for (let index = 0; index < this.count; index++) {\n const o = index * 6\n positions[o]! += slant\n positions[o + 3]! += slant\n positions[o + 1]! -= fall\n positions[o + 4]! -= fall\n const ground = context.groundAt(positions[o]!, positions[o + 2]!)\n if (positions[o + 4]! < ground) {\n const x = positions[o]!\n const z = positions[o + 2]!\n if (splash && random.next() < SPLASH_CHANCE) {\n const distance = Math.hypot(x - camera[0], z - camera[2])\n if (distance < reach) {\n if (random.next() < SPRITE_CHANCE) {\n this.engine.spawn('normal', { p: [x, ground + 0.01, z], dir: UP, cone: 0.6, sp: [0.6, 1.3], life: [0.18, 0.3], size: 0.035, c0: 0xb8c8dc, a: 0.6, g: 9.8, fi: 0 })\n }\n if (this.puddleAt ? this.puddleAt(x, z) : inPuddle(this.puddles, x, z)) {\n this.engine.ring([[x, ground + 0.012, z], 0.01, random.between(0.12, 0.25), random.between(0.4, 0.6), 0x6a7a90])\n }\n }\n }\n const nx = random.between(BOX_X[0], BOX_X[1]) + this.centerX\n const ny = random.between(REENTRY_HEIGHT[0], REENTRY_HEIGHT[1])\n const nz = random.between(BOX_Z[0], BOX_Z[1]) + this.centerZ\n this.set(o, nx, ny, nz)\n }\n }\n }\n\n /** `rain.visible = false; RN.on = false` (W440): the field stops; the streaks stay where they were. */\n clear(): void {\n this.on = false\n }\n\n private set(offset: number, x: number, y: number, z: number): void {\n const positions = this.positions\n positions[offset] = x\n positions[offset + 1] = y\n positions[offset + 2] = z\n positions[offset + 3] = x + this.wind * STREAK_SLANT\n positions[offset + 4] = y - STREAK_LENGTH\n positions[offset + 5] = z\n }\n}\n","import { add, DELTA, eq, ge, gt, ite, lt, mul, PARAM, rnd, TIME, variable } from '../dsl'\nimport { behavior, cue, decal, emit, screen, set, when } from '../steps'\nimport type { EffectDef } from '../types'\nimport { WORKSHOP_PUDDLES } from '../../workshop/rain'\n\n/** How many streaks, and how much the wind slants them, for the garoa, the chuva and the temporal (W1070). */\nconst DROPS = [900, 2200, 4000]\nconst SLANT = [0.3, 1.2, 4.5]\nconst STORM = 2\n/** The double flash (W1072): at full for 0.08 s, 0.2 for 0.08 s, 0.8 for 0.1 s, then dark; it ends at 0.4 s. */\nconst FLASH_STEPS = ite(lt(variable('flash'), 0.08), 1, ite(lt(variable('flash'), 0.16), 0.2, ite(lt(variable('flash'), 0.26), 0.8, 0)))\n\n/**\n * W1063-1072 (the page's `chuva`): up to 4000 streaks in a box by the camera, slanted by the wind; splashes and rings only within 12 m and only on the\n * four dark puddles; in the storm, sheets of water pass and a double flash lights the scene for 0.2 s every 3.5 to 6 s. The flash is a cue (`lightning`) for\n * whoever owns the lights; the screen goes white by itself.\n */\nexport const RAIN_AND_PUDDLES: EffectDef = {\n id: 'rain_and_puddles',\n pageId: 'chuva',\n team: 'world',\n params: { id: 'intensity', options: ['drizzle', 'rain', 'storm'], default: 1 },\n budget: { particles: 4000, decals: 4 },\n loop: 0,\n view: { eye: [2, 2, 7], target: [-1, 0.6, 1] },\n setup: [\n cue('lamp_cone', { visible: true }),\n ...WORKSHOP_PUDDLES.map(([x, z, radiusX, radiusZ]) => decal(x, 0, z, radiusX * 2, 0x0c1016, { grow: 0, life: 1e6, ar: radiusZ / radiusX })),\n ],\n timeline: [\n behavior('rain', 'init', { count: ite(eq(PARAM, 0), DROPS[0], ite(eq(PARAM, 1), DROPS[1], DROPS[2])), wind: ite(eq(PARAM, 0), SLANT[0], ite(eq(PARAM, 1), SLANT[1], SLANT[2])) }),\n set('nextStrike', rnd(2, 4)),\n set('flash', -1),\n ],\n tick: [\n behavior('rain', 'update', { dt: DELTA, splash: gt(PARAM, 0) }),\n when(eq(PARAM, STORM), [\n when(gt(TIME, variable('nextStrike')), [set('nextStrike', add(TIME, rnd(3.5, 6))), set('flash', 0)]),\n when(ge(variable('flash'), 0), [\n set('flash', add(variable('flash'), DELTA)),\n set('light', FLASH_STEPS),\n cue('lightning', { intensity: variable('light') }),\n screen({ max: { white: mul(variable('light'), 0.12) } }),\n when(gt(variable('flash'), 0.4), [set('flash', -1), cue('lightning', { intensity: 0 })]),\n ]),\n emit('sheet', 'normal', 2.5, { p: [-12, 2.5, rnd(-1, 8)], box: [0, 2, 0], v: [7, -0.5, 0], sp: 0, life: [3, 4], size: [3, 5], grow: 1.3, c0: 0x8a9ab0, a: 0.06, fi: 0.3 }),\n ]),\n ],\n}\n","import { localToWorld } from '../props'\nimport type { Vec3Tuple } from '../../workshop/types'\nimport { normalized } from '../vector-math'\n\n/** The car of the Workshop's set as the page leaves it between effects (`CAR0`, W104): at (5.6, 0, 0.8), turned -0.4 rad. */\nexport const CAR_ORIGIN: Vec3Tuple = [5.6, 0, 0.8]\nexport const CAR_ROTATION: Vec3Tuple = [0, -0.4, 0]\n/** A point of the car's own space in the world (`carW`). */\nexport const carPoint = (x: number, y: number, z: number): Vec3Tuple => localToWorld(CAR_ORIGIN, CAR_ROTATION, [x, y, z])\nexport const carDirection = (from: Vec3Tuple, to: Vec3Tuple): Vec3Tuple => normalized([to[0] - from[0], to[1] - from[1], to[2] - from[2]])\n","import { add, cos, DELTA, INDEX, local, mul, not, pick, query, rand, rnd, sin, sqrt, variable, vadd, vscale, vwith, vy } from '../dsl'\nimport { behavior, cue, emit, repeat, set, when } from '../steps'\nimport type { EffectDef } from '../types'\nimport { carDirection, carPoint } from './car'\n\n/** Where the beam starts (between the two lamps, W1176) and which way it points: the car's own +z. */\nconst BEAM_FROM = carPoint(0, 0.74, 2.15)\nconst BEAM_DIRECTION = carDirection(BEAM_FROM, carPoint(0, 0.74, 3.15))\nconst BEAM_SIDE = [BEAM_DIRECTION[2], 0, -BEAM_DIRECTION[0]] as const\n/** The six zombies that come into the light walk toward a point 2.6 m in front of the car (W1168). */\nconst WALKERS = 6\nconst WALK_SPEED = 0.7\nconst DROPS = 1800\nconst SLANT = 0.8\n\nconst startWalking = [\n set('spot', query('car', 'point', rnd(-2, 2), 0, rnd(8, 18))),\n set('goal', query('car', 'point', rnd(-0.6, 0.6), 0, 2.6)),\n behavior('walkers', 'place', { index: INDEX, x: pick(variable('spot'), 0), z: pick(variable('spot'), 2), facing: 0 }),\n behavior('walkers', 'walk', { index: INDEX, to: variable('goal'), speed: WALK_SPEED }),\n]\n\n/**\n * W1161-1170 (the page's `farol`): two beams in the air, rain that glows only inside the cone (up to 600 lit drops a second, thrown into the beam and\n * falling), and zombies that walk into the light. The lamps, the cones and the fog (0.05) are cues for whoever owns the scene.\n */\nexport const HEADLIGHTS_IN_RAIN: EffectDef = {\n id: 'headlights_in_rain',\n pageId: 'farol',\n team: 'vehicles',\n budget: { particles: 600, lights: 2 },\n loop: 0,\n view: { eye: [-2.5, 2, 5.5], target: [4, 1, 6] },\n setup: [\n cue('fog', { density: 0.05 }),\n cue('headlights', { on: true, intensity: 2.4, color: 0xfff1d0 }),\n behavior('rain', 'init', { count: DROPS, wind: SLANT }),\n repeat(WALKERS, ...startWalking),\n ],\n timeline: [],\n tick: [\n behavior('rain', 'update', { dt: DELTA, splash: true }),\n emit(\n 'lit',\n 'streak',\n 600,\n local(\n 'depth',\n rnd(1, 13),\n local(\n 'spread',\n mul(variable('depth'), 0.33, sqrt(rand())),\n local(\n 'turn',\n rnd(0, 6.283),\n local('base', vadd(BEAM_FROM, vscale(BEAM_DIRECTION, variable('depth')), vscale(BEAM_SIDE, mul(cos(variable('turn')), variable('spread')))), {\n p: vwith(variable('base'), 1, add(vy(variable('base')), mul(sin(variable('turn')), variable('spread'), 0.6))),\n v: [0.8, -14, 0],\n sp: 0,\n life: 0.06,\n len: 0.035,\n c0: 0xcfdcf0,\n a: 0.9,\n }),\n ),\n ),\n ),\n ),\n repeat(WALKERS, when(not(query('walkers', 'is_walking', INDEX)), startWalking)),\n ],\n}\n","import { add, DELTA, lt, mul, rand, rnd, sub, TIME, variable, vx, vz } from '../dsl'\nimport { cue, decal, emit, fireLight, set, when } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef } from '../types'\n\nconst YAW = -Math.PI / 2\nconst COS = Math.cos(YAW)\nconst SIN = Math.sin(YAW)\nconst START_X = 10\nconst SPEED = 2.4\n/** `carW(0.6, 0.06, 2.15)` for a car turned by `YAW`, as the matrix multiplies it (the y component is replaced by 0.04). */\nconst contactX = (carX: number | object) => add(COS * 0.6, 0 * 0.06, SIN * 2.15, carX)\nconst contactZ = add(-SIN * 0.6, 0 * 0.06, COS * 2.15, 1.5)\nconst BACKWARD = normalized([1, 0.35, 0])\n\n/** W1090-1100 (`faisca`): a bumper dragging on the asphalt throws a river of sparks that bounce and go from white to orange, with a short light and dark scratches. */\nexport const SCRAPING_SPARKS: EffectDef = {\n id: 'scraping_sparks',\n pageId: 'faisca',\n team: 'vehicles',\n budget: { particles: 200, lights: 1 },\n loop: 4.4,\n view: { eye: [2, 1.6, 5.5], target: [3, 0.3, 1.5] },\n setup: [cue('zombie_visible', { index: -1, visible: false }), cue('car_pose', { x: START_X, z: 1.5, yaw: YAW })],\n timeline: [],\n tick: [\n set('carX', sub(START_X, mul(TIME, SPEED))),\n set('contact', [contactX(variable('carX')), 0.04, contactZ]),\n cue('car_pose', { x: variable('carX'), z: 1.5, yaw: YAW }),\n emit('sp', 'streak', 200, { p: variable('contact'), dir: BACKWARD, cone: 0.55, sp: [3, 8], v: [-2.4, 0, 0], life: [0.2, 0.5], len: 0.02, c0: 0xfff0c0, c1: 0xff5010, g: 9.8, gnd: 2 }),\n emit('gl', 'add', 40, { p: variable('contact'), r: 0.05, size: [0.08, 0.16], life: 0.05, c0: 0xffd8a0, fi: 0 }),\n when(lt(rand(), mul(DELTA, 14)), [decal(vx(variable('contact')), 0, add(vz(variable('contact')), rnd(-0.05, 0.05)), 0.12, 0x0a0a0a, { grow: 0, life: 60, ar: 0.25 })]),\n fireLight(0, [vx(variable('contact')), 0.34, vz(variable('contact'))], 1.6, mul(TIME, 3)),\n ],\n}\n","import { DELTA, eq, gt, ite, lt, mul, PARAM, rand, TIME } from '../dsl'\nimport { burst, call, emit, fireLight, fireLightOff, when } from '../steps'\nimport type { EffectDef } from '../types'\nimport { carDirection, carPoint } from './car'\n\nconst HOOD = carPoint(0, 1.02, 1.4)\nconst EXHAUST = carPoint(0.5, 0.3, -2.2)\nconst EXHAUST_DIRECTION = carDirection(EXHAUST, carPoint(0.5, 0.3, -3))\nconst SPARK_FROM = carPoint(0, 0.7, 2.1)\nconst SPARK_DIRECTION = carDirection(SPARK_FROM, carPoint(0, 0.9, 3))\nconst WORN = 1\nconst BAD = 2\nconst CRITICAL = 3\n\n/** W1101-1112 (`motor`): exhaust only at 100, a thread of white steam at 60, steady grey smoke and the odd spark at 30, thick black smoke with fire at 10. */\nexport const ENGINE_SMOKE: EffectDef = {\n id: 'engine_smoke',\n pageId: 'motor',\n team: 'vehicles',\n params: { id: 'condition', options: ['100', '60', '30', '10'], default: 2 },\n budget: { particles: 120, lights: 1 },\n loop: 0,\n view: { eye: [9.5, 2.6, 5.2], target: [5.2, 0.9, 1.5] },\n timeline: [],\n tick: [\n emit('ex', 'normal', ite(eq(PARAM, CRITICAL), 6, 3), { p: EXHAUST, dir: EXHAUST_DIRECTION, cone: 0.3, sp: [0.6, 1.2], drag: 1.2, life: [1, 1.8], size: [0.06, 0.12], grow: 5, c0: ite(gt(PARAM, 1), 0x55524e, 0x9a9a96), a: 0.18, g: -0.2 }),\n when(eq(PARAM, WORN), [\n emit('st', 'normal', 14, { p: HOOD, box: [0.4, 0, 0.3], dir: [0, 1, 0], cone: 0.35, sp: [0.5, 1], drag: 0.8, life: [1.4, 2.4], size: [0.15, 0.25], grow: 4, c0: 0xc8ccd0, a: 0.22, g: -0.3 }),\n ]),\n when(eq(PARAM, BAD), [\n emit('st', 'normal', 22, { p: HOOD, box: [0.4, 0, 0.3], dir: [0, 1, 0], cone: 0.35, sp: [0.6, 1.2], drag: 0.6, life: [2, 3], size: [0.2, 0.35], grow: 4, c0: 0x6a6a68, c1: 0x3a3a3c, a: 0.4, g: -0.35 }),\n when(lt(rand(), mul(DELTA, 0.8)), [burst('streak', 8, { p: SPARK_FROM, dir: SPARK_DIRECTION, cone: 0.6, sp: [2, 4], life: 0.3, len: 0.02, c0: 0xffd080, c1: 0xff4000, g: 9.8 })]),\n ]),\n when(eq(PARAM, CRITICAL), [\n emit('st', 'normal', 34, { p: HOOD, box: [0.5, 0, 0.4], dir: [0, 1, 0], cone: 0.3, sp: [0.8, 1.6], drag: 0.4, life: [2.5, 3.8], size: [0.3, 0.5], grow: 4.5, c0: 0x1c1b1b, c1: 0x0e0e10, a: 0.6, g: -0.4 }),\n call('fire_emit', 'hood', carPoint(0, 0.8, 2.0), 0.3, 0.45, { noSmoke: true }),\n fireLight(0, carPoint(0, 1.2, 2.6), 2.2, TIME),\n ], [fireLightOff(0)]),\n ],\n}\n","import { rnd, scaled, query, vadd } from '../dsl'\nimport { behavior, call, cue, debris, repeat } from '../steps'\nimport type { EffectStep } from '../types'\nimport { carPoint } from './car'\n\nconst UP = [0, 1, 0] as const\n\n/** The velocity the page gives a door: the car's own space pushed outward by `s * 4`, minus the centre, with the height set to 3 (W1124). */\nconst doorVelocity = (side: number): readonly [number, number, number] => {\n const out = carPoint(side * 4, 3, 0.3)\n const centre = carPoint(0, 0, 0.3)\n return [out[0] - centre[0], 3, out[2] - centre[2]]\n}\n\n/** `vehicleBoom` (W1118-1128): the blast at the car, the car jumping, glass in the air, then the bonnet, both doors and a wheel flying off. */\nexport const VEHICLE_BOOM: readonly EffectStep[] = [\n call('explode', vadd(query('car', 'position'), [0, 0.6, 0]), 1.8, 0x2a1a14),\n cue('car_burnt'),\n behavior('car', 'launch', { rise: 4.5, spin: 0.9 }),\n repeat(scaled(30), debris({ p: vadd(query('car', 'position'), [0, 1.3, 0]), dir: UP, cone: 1.3, sp: [2, 6], size: [0.06, 0.006, 0.06], c: 0xa8c8d8, life: 20 })),\n debris({ p: carPoint(0, 1, 1.4), v: [rnd(-1, 1), 9, rnd(-1, 1)], sp: 0, size: [1.6, 0.06, 1.2], c: 0x3a1d18, spin: 6, life: 20, bounce: 0.2 }),\n ...[-1, 1].map((side) => debris({ p: carPoint(side * 0.95, 0.7, 0.3), v: doorVelocity(side), sp: 0, size: [0.06, 0.7, 1.1], c: 0x3a1d18, spin: 5, life: 20, bounce: 0.2 })),\n debris({ p: carPoint(0.95, 0.38, 1.35), v: [3, 4, 2], sp: 0, size: [0.3, 0.76, 0.76], c: 0x111111, spin: 8, life: 20, bounce: 0.4 }),\n]\n","import { add, and, clamp, DELTA, div, ge, gt, lt, mul, query, sub, TIME, variable, vadd, vlerp } from '../dsl'\nimport { after, behavior, burst, call, cue, decal, emit, fireLight, set, when } from '../steps'\nimport type { EffectDef } from '../types'\nimport { VEHICLE_BOOM } from './boom'\nimport { carDirection, carPoint } from './car'\n\nconst TANK = carPoint(-0.6, 0.25, -1.3)\nconst TANK_FLOOR = [TANK[0], 0, TANK[2]] as const\nconst POOL = [TANK_FLOOR[0] - 1.2, 0, TANK_FLOOR[2] - 0.6] as const\nconst BOOM_AT = 7.2\nconst HOLE = carPoint(-0.96, 0.6, -1.2)\n\n/** W1113-1132 (`vazamento`): petrol drips into a pool, a shot into the metal throws a spark, the fire runs along the pool to the tank and the car blows up. */\nexport const FUEL_LEAK: EffectDef = {\n id: 'fuel_leak',\n pageId: 'vazamento',\n team: 'vehicles',\n budget: { particles: 400, lights: 1 },\n loop: 11,\n view: { eye: [10, 2.4, 4.2], target: [5.6, 0.4, 0] },\n setup: [cue('zombie_visible', { index: -1, visible: false })],\n timeline: [\n decal(POOL[0], 0, POOL[2], 2.2, 0x1f2230, { grow: 3, life: 60, ar: 0.6, fluid: 'oil' }),\n after(\n 3.4,\n call('impact', 'metal', HOLE, carDirection(HOLE, carPoint(-2, 0.6, -1.2)), 1.5),\n burst('streak', 14, { p: [POOL[0], 0.05, POOL[2]], cone: 1.4, sp: [1, 3], life: 0.3, len: 0.02, c0: 0xffd080, c1: 0xff4000, g: 9.8 }),\n ),\n after(BOOM_AT, ...VEHICLE_BOOM),\n ],\n tick: [\n when(lt(TIME, BOOM_AT), [\n emit('drip', 'normal', 12, { p: TANK, box: [0.05, 0, 0.05], sp: 0, life: 1, size: 0.03, c0: 0x5a5a7a, a: 0.9, g: 9.8, fi: 0, gnd: 1 }),\n ]),\n when(and(gt(TIME, 3.5), lt(TIME, BOOM_AT)), [\n set('spread', clamp(div(sub(TIME, 3.5), 2.5), 0, 1)),\n set('front', vlerp(POOL, TANK_FLOOR, mul(variable('spread'), 0.8))),\n call('fire_emit', 'vz', variable('front'), add(0.6, mul(variable('spread'), 0.4)), 1.1, { smoke: 14 }),\n call('fire_emit', 'vz2', POOL, 0.7, 0.8, { smoke: 6 }),\n fireLight(0, vadd(variable('front'), [0, 1, 0]), 3, TIME),\n ]),\n when(ge(TIME, BOOM_AT), [\n call('fire_emit', 'wr', vadd(query('car', 'position'), [0, 0.9, 0]), 0.8, 1.4, { smoke: 18, smokeAlpha: 0.55 }),\n fireLight(0, vadd(query('car', 'position'), [0, 1.6, 0]), 3.4, TIME),\n behavior('car', 'step', { dt: DELTA }),\n ]),\n ],\n}\n\n/**\n * The same leak without the fire and the explosion: the pool of petrol spreading under the car and the drips from the hole, for as long as it plays. The steps\n * are the page's own (`decal` of the pool, `emit` of the drips); the spark of the shot and everything from `BOOM_AT` on (the fire running to the tank, the\n * blast) are left out. A normal bullet only does this; when the leak catches fire the vehicle asks for the full `fuel_leak` sequence (`vehicle_leak_ignited`).\n */\nexport const FUEL_LEAK_ONLY: EffectDef = {\n id: 'fuel_leak_only',\n pageId: 'vazamento',\n team: 'vehicles',\n budget: { particles: 200, lights: 0 },\n loop: 11,\n view: FUEL_LEAK.view,\n setup: FUEL_LEAK.setup,\n timeline: [decal(POOL[0], 0, POOL[2], 2.2, 0x1f2230, { grow: 3, life: 60, ar: 0.6, fluid: 'oil' })],\n tick: [emit('drip', 'normal', 12, { p: TANK, box: [0.05, 0, 0.05], sp: 0, life: 1, size: 0.03, c0: 0x5a5a7a, a: 0.9, g: 9.8, fi: 0, gnd: 1 })],\n}\n","import { DELTA, gt, query, TIME, vadd } from '../dsl'\nimport { after, behavior, call, cue, fireLight, when } from '../steps'\nimport type { EffectDef } from '../types'\nimport { VEHICLE_BOOM } from './boom'\n\n/** W1133-1139 (`explosao`, `lab: 1`): a big blast (scale 1.8 of the grenade), the car jumps and lands crooked, bonnet, doors and a wheel fly, and the wreck burns. */\nexport const CAR_EXPLOSION: EffectDef = {\n id: 'car_explosion',\n pageId: 'explosao',\n team: 'vehicles',\n lab: 'car_fuel_tank',\n budget: { particles: 600, debris: 34, lights: 1 },\n loop: 10,\n view: { eye: [12, 4.2, 8], target: [5.6, 1, 0.8] },\n props: ['car'],\n setup: [cue('zombie_visible', { index: -1, visible: false })],\n timeline: [after(0.4, ...VEHICLE_BOOM)],\n tick: [\n when(gt(TIME, 0.5), [\n call('fire_emit', 'wr', vadd(query('car', 'position'), [0, 0.9, 0]), 0.8, 1.4, { smoke: 18, smokeAlpha: 0.55 }),\n fireLight(0, vadd(query('car', 'position'), [0, 1.6, 0]), 3.4, TIME),\n ]),\n behavior('car', 'step', { dt: DELTA }),\n ],\n}\n","import { add, eq, gt, INDEX, mod, mul, or, pick } from '../dsl'\nimport { after, behavior, burst, camera, cue, screen, repeat, when } from '../steps'\nimport type { EffectDef } from '../types'\nimport { carPoint } from './car'\n\nconst HITS = [[90, 40], [170, 60], [120, 75], [60, 50]]\nconst HIT_X = HITS.map(([x]) => x)\nconst HIT_Y = HITS.map(([, y]) => y)\nconst ZOMBIE = carPoint(0, 0, 3.1)\nconst CAR_YAW = -0.4\n\n/** W1140-1152 (`parabrisa`): from inside the car, blows shake the camera and add a star crack; at the second and fourth a smear of blood with a handprint runs down. */\nexport const WINDSHIELD: EffectDef = {\n id: 'windshield',\n pageId: 'parabrisa',\n team: 'vehicles',\n budget: { particles: 6, decals: 1 },\n loop: 5.8,\n setup: [\n cue('zombie_visible', { index: -1, visible: false }),\n behavior('walkers', 'place', { index: 0, x: ZOMBIE[0], z: ZOMBIE[2], facing: CAR_YAW + Math.PI }),\n camera(carPoint(-0.3, 1.35, -0.2), carPoint(0, 1.2, 2.2), true),\n ],\n timeline: [\n repeat(\n HITS.length,\n after(\n add(0.6, mul(INDEX, 0.85)),\n cue('zombie_swing', { index: 0, side: mod(INDEX, 2) }),\n after(\n add(0.6, mul(INDEX, 0.85), 0.15),\n behavior('cracks', 'crack', { x: pick(HIT_X, INDEX), y: pick(HIT_Y, INDEX), big: gt(INDEX, 1) }),\n when(or(eq(INDEX, 1), eq(INDEX, 3)), [behavior('cracks', 'blood_streaks', { x: pick(HIT_X, INDEX), y: pick(HIT_Y, INDEX) })]),\n screen({ add: { shake: 0.18 } }),\n burst('add', 6, { p: carPoint(-0.1, 1.25, 0.75), box: [0.4, 0.1, 0.05], size: [0.01, 0.03], life: [0.2, 0.5], c0: 0xffffff, a: 0.8, fi: 0.2 }),\n cue('windshield_stage', { stage: add(INDEX, 1) }),\n ),\n ),\n ),\n ],\n}\n","import { add, and, DELTA, gt, lt, mod, mul, rnd, TIME } from '../dsl'\nimport { after, burst, cue, lens, repeat, screen, when } from '../steps'\nimport type { EffectDef, EffectStep } from '../types'\n\nconst FRESH_BLOOD = 0x8a0a0a\nconst BEAT_RATE = 1.9\n\n/** A blow from one side (`swing`, W1178): a red edge on that side, a shake and splashes on the lens, 0.18 s after the swing starts. */\nconst blow = (side: 0 | 1): EffectStep[] => [\n cue('zombie_swing', { index: -1, side }),\n after(\n add(TIME, 0.18),\n screen({ set: side === 0 ? { dirL: 1 } : { dirR: 1 }, add: { shake: 0.3 } }),\n repeat(4, lens(side === 0 ? rnd(0.05, 0.3) : rnd(0.7, 0.95), rnd(0.3, 0.8), rnd(0.02, 0.05), '110,8,8', 0.8)),\n burst('normal', 12, { p: [0, 1.4, 0.9], dir: [0, 0.2, 1], cone: 0.6, sp: [1, 2], life: 1, size: [0.03, 0.05], c0: FRESH_BLOOD, g: 9.8, fi: 0, gnd: 3, ds: [0.05, 0.1] }),\n ),\n]\n\n/** W1169-1185 (`dano`): two blows, from the left and from the right, then the bite: a red flash, the colour drains, the heartbeat pulses and big blood runs down the lens. */\nexport const DAMAGE_AND_BITE: EffectDef = {\n id: 'damage_and_bite',\n pageId: 'dano',\n team: 'screen',\n budget: {},\n loop: 5.6,\n view: { eye: [0, 1.6, 1.7], target: [0, 1.5, 0] },\n timeline: [\n after(0.3, ...blow(0)),\n after(1.6, ...blow(1)),\n after(3, cue('zombie_lunge', { z: 0.45 })),\n after(\n 3.15,\n screen({ set: { red: 1, desat: 0.9 }, add: { shake: 0.6 } }),\n cue('bite', { infection: 1 }),\n repeat(7, lens(rnd(0.2, 0.8), rnd(0.2, 0.7), rnd(0.04, 0.1), '100,6,6', 1)),\n ),\n ],\n tick: [\n when(and(gt(TIME, 3.2), lt(TIME, 5.4)), [when(lt(mod(mul(TIME, BEAT_RATE), 1), mul(DELTA, BEAT_RATE)), [screen({ set: { beat: 1 } })])]),\n ],\n}\n","import { add, CAMERA_POSITION, cameraForward, DELTA, eq, gt, INDEX, lt, max, mod, mul, PARAM, rand, rnd, sin, TIME, variable, vadd } from '../dsl'\nimport { burst, cue, post, repeat, screen, set, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst MALAISE = 0\nconst FEVER = 1\nconst DELIRIUM = 2\nconst LAST_HOURS = 3\nconst GHOSTS = [[-3, -1.8], [3.5, -2.4], [-6, 1.5]]\n\n/** W1186-1205 (`infeccao`): malaise (washed colour, a breathing vignette), fever (cold edge, waves of blur, a cough that shakes the camera), delirium, the last hours. */\nexport const INFECTION_PHASES: EffectDef = {\n id: 'infection_phases',\n pageId: 'infeccao',\n team: 'screen',\n params: { id: 'phase', options: ['malaise', 'fever', 'delirium', 'last_hours'], default: 1 },\n budget: { particles: 10 },\n loop: 0,\n view: { eye: [2, 1.7, 7], target: [0, 1.2, -1] },\n setup: [\n cue('zombie_visible', { index: -1, visible: false }),\n ...GHOSTS.map(([x, z], index) => cue('ghost_place', { index, x, z, turn: rnd(-0.4, 0.4) })),\n ],\n timeline: [set('nextCough', 2)],\n tick: [\n when(eq(PARAM, MALAISE), [post({ desat: 0.25, tunnel: add(0.15, mul(0.12, sin(mul(TIME, 1.2)))), blur: mul(0.3, max(0, sin(mul(TIME, 0.7)))) })]),\n when(eq(PARAM, FEVER), [\n post({ cold: add(0.8, mul(0.15, sin(mul(TIME, 2)))), blur: add(0.6, mul(0.6, sin(mul(TIME, 1.6)))), desat: 0.3 }),\n when(gt(TIME, variable('nextCough')), [\n set('nextCough', add(TIME, rnd(3, 5))),\n screen({ add: { shake: 0.25 } }),\n burst('normal', 10, { p: vadd(CAMERA_POSITION, cameraForward([0, -0.15, -0.6])), dir: cameraForward([0, 0, -1]), cone: 0.4, sp: [0.6, 1.2], drag: 1.5, life: 1.2, size: [0.05, 0.1], grow: 5, c0: 0xdfe6ee, a: 0.2 }),\n ]),\n ]),\n when(eq(PARAM, DELIRIUM), [\n post({ chroma: add(2, mul(3, add(0.5, mul(0.5, sin(mul(TIME, 1.7)))))), hue: mul(22, sin(mul(TIME, 0.6))), tunnel: 0.3, desat: 0.15 }),\n repeat(GHOSTS.length, cue('ghost_visible', { index: INDEX, visible: gt(sin(add(mul(TIME, add(1.3, mul(INDEX, 0.7))), mul(INDEX, 2))), 0.6) })),\n when(lt(rand(), mul(DELTA, 0.3)), [screen({ set: { white: 0.12 } })]),\n ]),\n when(eq(PARAM, LAST_HOURS), [\n post({ desat: 0.85, tintAlpha: 0.22, tintColor: '70,110,40', tunnel: 0.55, blur: 1.2 }),\n when(lt(mod(mul(TIME, 2.2), 1), mul(DELTA, 2.2)), [screen({ set: { beat: 1 }, add: { shake: 0.05 } })]),\n ]),\n ],\n}\n","import { add, CAMERA_POSITION, clamp, DELTA, div, eq, INDEX, ite, lt, min, mod, mul, query, rnd, sub, TIME, variable, vdist } from '../dsl'\nimport { behavior, cue, post, repeat, screen, set, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst WALKERS = 6\nconst NEAR = 9\nconst RANGE = 7\nconst RESTING_BPM = 70\nconst BPM_RISE = 80\nconst walkerPoint = [query('walkers', 'x', INDEX), 0, query('walkers', 'z', INDEX)]\n\n/** W1206-1217 (`panico`): the aim trembles, the tunnel closes as the nearest zombie arrives and the heartbeat quickens from 70 to 150 bpm. */\nexport const PANIC: EffectDef = {\n id: 'panic',\n pageId: 'panico',\n team: 'screen',\n budget: {},\n loop: 8,\n view: { eye: [0, 1.65, 6], target: [0, 1.3, -2] },\n setup: [\n cue('zombie_visible', { index: -1, visible: false }),\n repeat(\n WALKERS,\n set('startX', rnd(-4, 4)),\n set('startZ', sub(rnd(-4, -2), mul(INDEX, 0.6))),\n behavior('walkers', 'place', { index: INDEX, x: variable('startX'), z: variable('startZ'), facing: 0 }),\n behavior('walkers', 'walk', { index: INDEX, to: [rnd(-1.2, 1.2), 0, 5], speed: ite(eq(mod(INDEX, 3), 1), 1.6, 0.7) }),\n ),\n ],\n timeline: [],\n tick: [\n set('nearest', 99),\n repeat(WALKERS, set('nearest', min(variable('nearest'), vdist(walkerPoint, CAMERA_POSITION)))),\n set('fear', clamp(div(sub(NEAR, variable('nearest')), RANGE), 0, 1)),\n set('bpm', add(RESTING_BPM, mul(BPM_RISE, variable('fear')))),\n when(lt(mod(div(mul(TIME, variable('bpm')), 60), 1), div(mul(DELTA, variable('bpm')), 60)), [screen({ set: { beat: add(0.4, mul(0.6, variable('fear'))) } })]),\n post({ tunnel: add(0.1, mul(0.65, variable('fear'))), desat: mul(0.45, variable('fear')) }),\n set('tremor', add(0.012, mul(0.035, variable('fear')))),\n ],\n}\n","import { rnd } from '../dsl'\nimport { after, burst, cue, lens, repeat, screen } from '../steps'\nimport type { EffectDef } from '../types'\n\n/** W1218-1226 (`cegueira`): a green cloud bursts between the bloater and the camera, the screen goes green and blurred and clears in 5 s; two coughs shake the camera. */\nexport const BLOATER_BLINDNESS: EffectDef = {\n id: 'bloater_blindness',\n pageId: 'cegueira',\n team: 'screen',\n budget: { particles: 30 },\n loop: 7.5,\n view: { eye: [0, 1.6, 2.6], target: [0, 1.3, 0] },\n setup: [cue('bloater_pose', { eyes: 0x5fe08a, skin: 0x6b7a52, belly: [1.4, 1.25, 1.6] })],\n timeline: [\n after(\n 0.7,\n cue('zombie_visible', { index: -1, visible: false }),\n screen({ set: { gas: 1 }, add: { shake: 0.3 } }),\n burst('normal', 30, { p: [0, 1.3, 0.8], r: 0.6, sp: [0.5, 2], drag: 2, life: [3, 5], size: [0.8, 1.6], grow: 1.6, c0: 0x6f8a3c, c1: 0x3b4a26, a: 0.3, fi: 0.05 }),\n repeat(8, lens(rnd(0.15, 0.85), rnd(0.15, 0.7), rnd(0.03, 0.08), '110,150,50', 0.9)),\n ),\n after(2, screen({ add: { shake: 0.2 } })),\n after(3.4, screen({ add: { shake: 0.2 } })),\n ],\n}\n","import { TIME } from '../dsl'\nimport { call, cue, fireLight } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst FIRE = [-1.4, 0.1, 3.0] as const\n\n/** W1227-1233 (`fogueira`): a low flame, embers that spiral up, thin smoke and a warm light that trembles and paints the survivor. */\nexport const CAMPFIRE: EffectDef = {\n id: 'campfire',\n pageId: 'fogueira',\n team: 'screen',\n budget: { particles: 120, lights: 1 },\n loop: 0,\n view: { eye: [1.4, 1.6, 6.4], target: [-1.4, 0.6, 3] },\n setup: [cue('zombie_visible', { index: -1, visible: false }), cue('survivor_pose', { x: -2.6, z: 3.6, turn: 2.2 })],\n timeline: [],\n tick: [\n call('fire_emit', 'camp', FIRE, 0.25, 0.55, { smoke: 8, smokeColor: 0x4a4642, smokeAlpha: 0.25 }),\n fireLight(0, [FIRE[0], FIRE[1] + 0.8, FIRE[2]], 2.4, TIME),\n ],\n}\n","import { add, gt, TIME, variable } from '../dsl'\nimport { after, burst, cue, set, when } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef, EffectStep } from '../types'\n\nconst FACING = 0.5\n/** The survivor's mouth and the way it breathes out, with the survivor at (-1.6, 0, 1.8) turned by `FACING` (W1236-1238). */\nconst MOUTH = [Math.sin(FACING) * 0.2 + -1.6, 1.68, Math.cos(FACING) * 0.2 + 1.8] as const\nconst BREATH = normalized([Math.sin(FACING), -0.15, Math.cos(FACING)])\nconst BREATH_EVERY = 2.4\nconst COUGH_EVERY = 6.5\n\n/** A puff of breath (`puff`, W1239): `strength` scales how many and how fast. */\nconst puff = (strength: number): EffectStep =>\n burst('normal', 8 * strength, { p: MOUTH, dir: BREATH, cone: 0.35, sp: [0.5, 1.1 * strength], drag: 1.6, life: [1.2, 1.8], size: [0.05, 0.09], grow: 6, c0: 0xdfe6ee, a: 0.22, g: -0.1 })\n\n/** W1234-1243 (`tosse`): a short breath every 2.4 s; in a cough, three quick bigger ones and a jolt of the body; the torch shows its beam in the fog. */\nexport const COUGH_AND_BREATH: EffectDef = {\n id: 'cough_and_breath',\n pageId: 'tosse',\n team: 'screen',\n budget: { particles: 40, lights: 1 },\n loop: 0,\n view: { eye: [2.4, 1.8, 6.5], target: [-1.4, 1.3, 1.6] },\n setup: [cue('zombie_visible', { index: -1, visible: false }), cue('survivor_pose', { x: -1.6, z: 1.8, turn: FACING }), cue('torch', { on: true })],\n timeline: [set('nextBreath', 0.5), set('nextCough', 3)],\n tick: [\n when(gt(TIME, variable('nextBreath')), [set('nextBreath', add(TIME, BREATH_EVERY)), puff(1)]),\n when(gt(TIME, variable('nextCough')), [\n set('nextCough', add(TIME, COUGH_EVERY)),\n ...[0, 0.25, 0.5].map((delay) => after(add(TIME, delay), puff(1.6), cue('survivor_jolt', { tilt: 0.25 }))),\n ]),\n ],\n}\n","import { cos, local, mul, rnd, scaled, sin, variable, vnorm } from './dsl'\nimport { burst, call, debris, repeat, screen, spawn } from './steps'\nimport type { EffectDef } from './types'\n\nconst FACING = [0, 0, 1] as const\nconst UP = [0, 1, 0] as const\nconst ORIGIN = [0, 0, 0] as const\nconst FORWARD = [0, 0, -1] as const\nconst IMPACT_MATERIALS = ['concrete', 'metal', 'wood', 'glass', 'flesh', 'water'] as const\n\n/** What a hit leaves on one material, the way the Workshop's `impact` helper does (W374-391), played once at the hit. */\nconst impactEffect = (material: (typeof IMPACT_MATERIALS)[number]): EffectDef => ({\n id: `impact_${material}`,\n pageId: `event:impact_${material}`,\n team: 'weapons',\n budget: { particles: 40, decals: 1 },\n loop: 2,\n timeline: [call('impact', material, ORIGIN, FACING, 1, 0)],\n})\n\n/** The flash of one weapon at its muzzle (W417 `muzzle`), played once: the recipe's own per-weapon sizes, petals, light and smoke. */\nconst muzzleEffect = (weapon: 'glock' | 'ak' | 'r870' | 'barrett'): EffectDef => ({\n id: `muzzle_${weapon}`,\n pageId: `event:muzzle_${weapon}`,\n team: 'weapons',\n budget: { particles: 60, lights: 1 },\n loop: 2.4,\n timeline: [call('muzzle', weapon, ORIGIN, FORWARD)],\n})\n\n/** A case thrown out of the port (W430 `casing`): brass, a red shell or a .50 case. */\nconst casingEffect = (id: string, type: '' | 'shell' | '50'): EffectDef => ({\n id,\n pageId: `event:${id}`,\n team: 'weapons',\n budget: { debris: 1 },\n loop: 2,\n timeline: [call('casing', ORIGIN, type)],\n})\n\n/**\n * The effects the destruction events play, as slices of the Workshop's recipes: the same bursts, with the recipe's own numbers, centred on the point the event\n * happens (`data/event-effects.json` says which event plays which). They are not Workshop pages, so they have no golden of their own; each cites the lines it was cut from.\n */\nexport const EVENT_EFFECTS: readonly EffectDef[] = [\n ...IMPACT_MATERIALS.map(impactEffect),\n ...(['glock', 'ak', 'r870', 'barrett'] as const).map(muzzleEffect),\n casingEffect('casing_brass', ''),\n casingEffect('casing_shell', 'shell'),\n casingEffect('casing_50', '50'),\n {\n // door_stages (W856-871): the dust a blow shakes loose from the frame and the splinters.\n id: 'stage_crack',\n pageId: 'event:stage_crack',\n team: 'destruction',\n budget: { particles: 12, debris: 5 },\n loop: 2,\n timeline: [\n burst('normal', 12, { p: ORIGIN, box: [0.6, 0.05, 0.02], dir: [0, -0.287, 0.958], cone: 0.6, sp: [0.2, 0.8], drag: 1.5, life: [0.8, 1.4], size: [0.08, 0.16], grow: 3, c0: 0x7a7366, a: 0.45, g: 0.4 }),\n repeat(scaled(2), debris({ p: [rnd(-0.5, 0.5), rnd(0, 1.6), 0], dir: [0, 0.287, 0.958], cone: 0.6, sp: [1, 2.5], size: [0.015, 0.015, 0.1], c: 0x7a5530, life: 12 })),\n ],\n },\n {\n // block_wall (W872-886, 3.6 s): the ring of dust that rolls out when the wall gives way.\n id: 'breach_dust',\n pageId: 'event:breach_dust',\n team: 'destruction',\n budget: { particles: 90, debris: 16 },\n loop: 5,\n timeline: [\n screen({ add: { shake: 0.25 } }),\n burst('normal', 30, { p: ORIGIN, box: [1.4, 0.8, 0.2], sp: [0.3, 1.2], drag: 1.5, life: [1.5, 2.6], size: [0.4, 0.7], grow: 3, c0: 0x8a857a, c1: 0x4a4640, a: 0.45 }),\n repeat(scaled(16), debris({ p: ORIGIN, box: [1.3, 0.8, 0.1], sp: [1, 3], size: [0.08, 0.07, 0.08], k: [0.5, 1.5], c: 0x6e6a63, life: 20 })),\n repeat(\n scaled(60),\n spawn('normal', local('angle', rnd(0, 6.283), {\n p: [rnd(-1.4, 1.4), 0.2, rnd(-0.6, 0.4)],\n dir: vnorm([cos(variable('angle')), 0.3, sin(variable('angle'))]),\n cone: 0.4, sp: [0.8, 2.4], drag: 1.3, life: [2.5, 4.5], size: [0.6, 1], grow: 3, c0: 0x7a7268, c1: 0x4a4640, a: 0.45, fi: 0.12,\n })),\n ),\n ],\n },\n {\n // building_collapse (W872-886): the slab dropping in blocks and the cloud that rolls along the street.\n id: 'collapse_dust',\n pageId: 'event:collapse_dust',\n team: 'destruction',\n budget: { particles: 350, debris: 26 },\n loop: 8,\n timeline: [\n screen({ add: { shake: 0.45 } }),\n repeat(6, debris({ p: ORIGIN, box: [2, 0.05, 0.6], v: [rnd(-0.5, 0.5), rnd(-0.5, 0), rnd(0, 0.8)], sp: 0, size: [rnd(0.5, 1.1), 0.25, rnd(0.4, 0.8)], c: 0x6e6a63, spin: 3, life: 30, bounce: 0.1 })),\n repeat(scaled(20), debris({ p: ORIGIN, box: [2.1, 0.1, 0.7], v: [rnd(-1, 1), rnd(-1, 1), rnd(0, 1.5)], sp: 0, size: [0.12, 0.1, 0.12], k: [0.5, 1.5], c: 0x5e5a54, life: 30 })),\n repeat(\n scaled(70),\n spawn('normal', local('angle', rnd(-0.4, 3.5), {\n p: [rnd(-2, 2), 0.2, rnd(-0.5, 0.5)],\n dir: vnorm([mul(cos(variable('angle')), 0.6), 0.25, sin(variable('angle'))]),\n cone: 0.5, sp: [1, 3.2], drag: 1.1, life: [3, 6], size: [0.7, 1.2], grow: 3, c0: 0x7a7268, c1: 0x4a4640, a: 0.5, fi: 0.12,\n })),\n ),\n ],\n },\n {\n // shopfront_glass (W846-855): the pane coming down as shards, the flash of the glints and the pale dust.\n id: 'glass_burst',\n pageId: 'event:glass_burst',\n team: 'destruction',\n budget: { debris: 80, particles: 50 },\n loop: 5,\n timeline: [\n screen({ add: { shake: 0.08 } }),\n repeat(\n scaled(75),\n debris({ p: ORIGIN, box: [1.25, 0.85, 0.02], v: [rnd(-0.4, 0.4), rnd(-0.5, 0.8), rnd(0.6, 2)], sp: 0, size: [rnd(0.05, 0.18), 0.006, rnd(0.05, 0.18)], c: 0xa8c8d8, spin: 14, life: 30, bounce: 0.15 }),\n ),\n burst('add', 30, { p: ORIGIN, box: [1.2, 0.8, 0.05], size: [0.02, 0.05], life: [0.2, 0.6], c0: 0xffffff, a: 0.9, fi: 0.3 }),\n burst('normal', 20, { p: [0, -0.6, 0.1], box: [1.2, 0.5, 0.1], sp: [0.2, 0.6], drag: 2, life: [0.8, 1.4], size: [0.12, 0.2], grow: 3, c0: 0xbfd6e2, a: 0.2 }),\n ],\n },\n {\n // The chips macro the page uses on wood (W849) plus rust (`rustAt`), for rebar laid bare in concrete.\n id: 'rebar_expose',\n pageId: 'event:rebar_expose',\n team: 'destruction',\n budget: { particles: 20, debris: 8 },\n loop: 2,\n timeline: [call('chips', ORIGIN, UP, 8, 0.9), call('rust_at', ORIGIN, 6)],\n },\n]\n","import { add, cos, DIRECTION, div, eq, floor, gt, INDEX, ite, lt, min, mul, ne, neg, pick, rand, sin, sub, variable, vx, vz } from '../dsl'\nimport { append, flash, mesh, repeat, set, when } from '../steps'\nimport type { EffectDef, EffectStep } from '../types'\n\n/**\n * The weapon in the player's hands (the Arsenal gallery's first-person effects: the muzzle flash and smoke of `createEffects`, the backblast of a launched rocket, the rocket's\n * trail and the flame jet of a flamethrower) as recipes of the Workshop engine, drawn as the gallery draws them: as solids (cones, spheres, icosahedra) with the gallery's sizes,\n * segment counts, materials, timing and growth, not as sprites. They are written in the weapon's own axes, the gallery's: x along the barrel, y up, z to the right. A player\n * says how big (`values`), where the weapon points (`direction`, the barrel in the world) and how it is turned (`orientation`, the quaternion, pitch and roll included), and\n * seeds the numbers the effect draws, which it draws in the order the gallery drew them.\n */\nconst PI = Math.PI\n/** The gallery's `smoke`: a sphere of 10 by 8 segments, a grey that rises, slows and grows by 2.1 a second while it fades. */\nconst SMOKE = { color: 0x8a8a86, width: 10, height: 8, gravity: -0.08, drag: 2.2, growth: 2.1 }\n/** Default muzzle smoke numbers (the `smokeless` profile of `@adatechnology/game-weapons`). */\nconst DEFAULT_SMOKE = { puffs: 2, radius: 0.01, opacity: 0.14, life: 0.7, drift: 0.2, rise: 0.06, growth: 2.1, flash: 1 } as const\n/** Default random spreads on top of drift, rise and sideways speed; the `gallery` profile widens them to reproduce the Arsenal gallery's smoke. */\nconst DEFAULT_SPREAD = { drift: 0.1, rise: 0.04, side: 0.06 } as const\n/** The flash lasts 55 ms and its group grows from 0.6 to 1.5 times its size (`0.6 + min(1, age / 0.055) * 0.9`). */\nconst FLASH_SECONDS = 0.055\nconst FLASH_RAMP = [0.6, 0.9] as const\nconst FLASH_OPACITY = 0.95\n/** The gallery's palette for flame bits: orange, yellow and dark red, and a pale additive core. */\nconst FLAME_COLORS = [0xff7a22, 0xffc040, 0xc8361a, 0xfff0b0]\nconst FLAME_ADDITIVE = 3\nconst ORIGIN = [0, 0, 0] as const\n\nconst SIZE = variable('size')\nconst FLASH_SCALE = variable('flash')\nconst fallback = (name: string, defaultValue: number): unknown => ite(ne(variable(name), undefined), variable(name), defaultValue)\nconst wide = (width: number): unknown => mul(sub(rand(), 0.5), width)\n\n/** The gallery's `smoke(position, velocity, size, life, opacity)`; the keys are in the order the gallery drew its numbers. `world` is a velocity that is not turned with the weapon. */\nconst puff = (point: unknown, velocity: unknown, world: unknown, radius: unknown, life: unknown, opacity: unknown, growth: unknown = SMOKE.growth): EffectStep =>\n mesh({\n shape: 'sphere',\n p: point,\n v: velocity,\n wv: world,\n r: radius,\n seg: SMOKE.width,\n seg2: SMOKE.height,\n life,\n c0: SMOKE.color,\n a: opacity,\n fade: true,\n lit: true,\n g: SMOKE.gravity,\n drag: SMOKE.drag,\n grow: growth,\n })\n\n/** The gallery's `flameBit`: an icosahedron of one of four colours, tumbling, thrown, rising, growing and slowing. The numbers are drawn in the gallery's order, the colour after the life. */\nconst flameBit = (point: unknown, velocity: unknown, world: unknown, radius: unknown, life: unknown): EffectStep[] => [\n set('bitPoint', point),\n set('bitVelocity', velocity),\n set('bitWorld', world),\n set('bitRadius', radius),\n set('bitLife', life),\n set('bitColour', floor(mul(rand(), FLAME_COLORS.length))),\n mesh({\n shape: 'icosahedron',\n p: variable('bitPoint'),\n v: variable('bitVelocity'),\n wv: variable('bitWorld'),\n r: variable('bitRadius'),\n seg: 1,\n rot: [mul(rand(), 3), mul(rand(), 3), 0],\n spin: [3, 4, 2],\n life: variable('bitLife'),\n c0: pick(FLAME_COLORS, variable('bitColour')),\n add: eq(variable('bitColour'), FLAME_ADDITIVE),\n a: 0.9,\n fade: true,\n g: -1.4,\n drag: 1.4,\n grow: 2.2,\n }),\n]\n\n/** One piece of the flash: additive, 55 ms, in the group that grows and is rolled by `roll`. */\nconst flashPiece = (shape: 'cone' | 'cone_open' | 'sphere', radius: unknown, height: unknown, width: number, at: unknown, rot: unknown, color: number): EffectStep =>\n mesh({ shape, r: radius, ...(height === undefined ? {} : { h: height }), seg: width, seg2: shape === 'sphere' ? 8 : 1, p: ORIGIN, at, rot, roll: variable('roll'), ramp: FLASH_RAMP, life: FLASH_SECONDS, c0: color, a: FLASH_OPACITY, add: true })\n\n/**\n * `muzzleFlash` of the gallery (size `k`): an open cone, four small cones and a sphere, additive for 55 ms and growing, a short orange light that holds its strength, and six\n * puffs of smoke. It draws the four angles of the small cones, the roll of the group, then the velocity and life of each puff.\n */\nconst MUZZLE_FLASH: EffectDef = {\n id: 'viewmodel_muzzle_flash',\n pageId: 'event:viewmodel_muzzle_flash',\n team: 'weapons',\n budget: { particles: 14, lights: 1 },\n loop: 0.1,\n setup: [\n set('size', fallback('size', 1)),\n set('puffs', fallback('puffs', DEFAULT_SMOKE.puffs)),\n set('radius', fallback('radius', DEFAULT_SMOKE.radius)),\n set('opacity', fallback('opacity', DEFAULT_SMOKE.opacity)),\n set('life', fallback('life', DEFAULT_SMOKE.life)),\n set('drift', fallback('drift', DEFAULT_SMOKE.drift)),\n set('rise', fallback('rise', DEFAULT_SMOKE.rise)),\n set('growth', fallback('growth', DEFAULT_SMOKE.growth)),\n set('flash', fallback('flash', DEFAULT_SMOKE.flash)),\n set('driftSpread', fallback('driftSpread', DEFAULT_SPREAD.drift)),\n set('riseSpread', fallback('riseSpread', DEFAULT_SPREAD.rise)),\n set('sideSpread', fallback('sideSpread', DEFAULT_SPREAD.side)),\n set('lifeSpread', ite(ne(variable('lifeSpread'), undefined), variable('lifeSpread'), mul(variable('life'), 0.3))),\n ],\n timeline: [\n set('angles', []),\n repeat(4, append('angles', add(div(mul(INDEX, PI), 2), mul(rand(), 0.6)))),\n set('roll', mul(rand(), PI)),\n when(gt(FLASH_SCALE, 0), [\n flash(ORIGIN, 0xffb060, mul(4, SIZE, FLASH_SCALE), FLASH_SECONDS, mul(2, SIZE, FLASH_SCALE), true, true),\n flashPiece('cone_open', mul(0.013, SIZE, FLASH_SCALE), mul(0.085, SIZE, FLASH_SCALE), 10, [mul(0.042, SIZE, FLASH_SCALE), 0, 0], [0, 0, -PI / 2], 0xffb347),\n repeat(\n 4,\n set('angle', pick(variable('angles'), INDEX)),\n flashPiece(\n 'cone',\n mul(0.005, SIZE, FLASH_SCALE),\n mul(0.04, SIZE, FLASH_SCALE),\n 6,\n [mul(0.012, SIZE, FLASH_SCALE), mul(cos(variable('angle')), 0.016, SIZE, FLASH_SCALE), mul(sin(variable('angle')), 0.016, SIZE, FLASH_SCALE)],\n [variable('angle'), 0, -PI / 2],\n 0xffb347,\n ),\n ),\n flashPiece('sphere', mul(0.016, SIZE, FLASH_SCALE), undefined, 12, [mul(0.008, SIZE, FLASH_SCALE), 0, 0], [0, 0, 0], 0xfff1c0),\n ]),\n repeat(\n variable('puffs'),\n puff(\n ORIGIN,\n [add(variable('drift'), mul(rand(), variable('driftSpread'))), add(variable('rise'), mul(rand(), variable('riseSpread'))), mul(sub(rand(), 0.5), variable('sideSpread'))],\n [0, 0, 0],\n mul(variable('radius'), SIZE),\n add(variable('life'), mul(rand(), variable('lifeSpread'))),\n variable('opacity'),\n variable('growth'),\n ),\n ),\n ],\n}\n\n/**\n * One puff of smoke the gallery's `smoke(position, velocity, size, life, opacity)` throws: a hot barrel's, a rocket's trail, the smoke of a flame on the ground. The numbers come as\n * `values` (`vx`, `vy`, `vz` in the world, `radius`, `life`, `opacity`), so the weapon keeps its own random draws.\n */\nconst SMOKE_PUFF: EffectDef = {\n id: 'weapon_smoke_puff',\n pageId: 'event:weapon_smoke_puff',\n team: 'weapons',\n budget: { particles: 1 },\n loop: 0.1,\n timeline: [puff(ORIGIN, undefined, [variable('vx'), variable('vy'), variable('vz')], variable('radius'), variable('life'), variable('opacity'))],\n}\n\n/** A rocket leaving the tube, from the rear of it: 26 puffs thrown back (-x) and 14 bits of fire. */\nconst BACKBLAST: EffectDef = {\n id: 'launcher_backblast',\n pageId: 'event:launcher_backblast',\n team: 'weapons',\n budget: { particles: 40 },\n loop: 0.1,\n timeline: [\n repeat(26, puff(ORIGIN, [neg(add(1.5, mul(rand(), 3))), 0, 0], [wide(1.4), wide(0.8), wide(1.4)], add(0.03, mul(rand(), 0.03)), add(1.8, mul(rand(), 1.4)), 0.5)),\n repeat(14, ...flameBit(ORIGIN, [neg(add(2, mul(rand(), 2))), 0, 0], [wide(1), wide(0.6), wide(1)], 0.02, 0.25)),\n ],\n}\n\n/**\n * A frame of a flamethrower's jet: `floor(420 dt scale)` bits of fire thrown along the barrel and up (`dt` and `scale` are the values), and a puff of smoke downwind half the time.\n * It is played every frame the nozzle burns. It needs the `direction` of the barrel for the wind of the smoke.\n */\nconst FLAME_JET: EffectDef = {\n id: 'flame_jet',\n pageId: 'event:flame_jet',\n team: 'weapons',\n budget: { particles: 30 },\n loop: 0.1,\n timeline: [\n repeat(\n floor(mul(420, variable('dt'), variable('scale'))),\n set('speed', mul(add(2.2, mul(rand(), 1.1)), variable('scale'))),\n ...flameBit(\n [mul(rand(), variable('speed'), min(variable('dt'), 0.3)), 0, 0],\n [variable('speed'), 0, 0],\n [wide(0.35), mul(sub(rand(), 0.3), 0.3), wide(0.35)],\n add(0.02, mul(rand(), 0.022)),\n add(0.4, mul(rand(), 0.35)),\n ),\n ),\n when(lt(rand(), 0.5), [\n puff([add(mul(1.1, variable('scale')), mul(rand(), 0.4)), 0, 0], undefined, [mul(vx(DIRECTION), 0.6), 0.35, mul(vz(DIRECTION), 0.6)], 0.03, 1.6, 0.4),\n ]),\n ],\n}\n\nexport const VIEWMODEL_EFFECTS: readonly EffectDef[] = [MUZZLE_FLASH, SMOKE_PUFF, BACKBLAST, FLAME_JET]\n","import { CAR_ORIGIN } from './car'\nimport { ENGINE_SMOKE } from './engine-smoke'\nimport { FUEL_LEAK, FUEL_LEAK_ONLY } from './fuel-leak'\nimport type { EffectDef } from '../types'\n\n/**\n * The Garagem's recipes written around a car, to be played at a vehicle: the Workshop's `engine_smoke` and `fuel_leak` keep the car where the page stood\n * it (`CAR_ORIGIN`), so each alias names that spot as its `anchor` and the effect lands on the point it is played at. The values are the recipes' own;\n * only the id, the condition they start in and (for the exhaust) how long one play lasts change. Ids a vehicle module asks for: `vehicle_fire`, `vehicle_leak`, `vehicle_leak_ignited`, `vehicle_exhaust`.\n * The other vehicle requests use point-based entries that already exist: the explosion catalogue's `car_fuel_tank`, `glass_burst` and `impact_metal`.\n */\nconst aliasOf = (base: EffectDef, id: string, changes: Partial<EffectDef>): EffectDef => ({ ...base, id, pageId: `vehicle:${id}`, anchor: CAR_ORIGIN, ...changes })\n\nexport const VEHICLE_EFFECTS: readonly EffectDef[] = [\n /** The engine on fire: `engine_smoke` at its worst condition (black smoke, flames in the hood, a light). */\n aliasOf(ENGINE_SMOKE, 'vehicle_fire', { params: { id: 'condition', options: ['100', '60', '30', '10'], default: 3 }, loop: 12 }),\n /** The exhaust of a car under way: `engine_smoke` in the best condition, for half a second so a moving car asks again. */\n aliasOf(ENGINE_SMOKE, 'vehicle_exhaust', { params: { id: 'condition', options: ['100', '60', '30', '10'], default: 0 }, loop: 0.5 }),\n /** A hole in the tank: the page's petrol leak without the fire and the blast (the pool and the drips). A normal bullet does only this. */\n aliasOf(FUEL_LEAK_ONLY, 'vehicle_leak', {}),\n /** The leak caught fire (an incendiary or tracer round, a spark, a fire near): the page's whole sequence, the fire running along the pool to the tank and the blast. */\n aliasOf(FUEL_LEAK, 'vehicle_leak_ignited', {}),\n]\n","import { BLOATER_CLOUD } from './creatures/bloater-cloud'\nimport { BLOOD_HIT } from './creatures/blood-hit'\nimport { CORPSE_DECAY } from './creatures/corpse-decay'\nimport { EYES_IN_FOG } from './creatures/eyes-in-fog'\nimport { DISMEMBER } from './creatures/dismember'\nimport { HEADSHOT } from './creatures/headshot'\nimport { REANIMATION } from './creatures/reanimation'\nimport { SCREAMER_SHRIEK } from './creatures/screamer-shriek'\nimport { IMPACT_BY_MATERIAL } from './weapons/impact-by-material'\nimport { PENETRATION } from './weapons/penetration'\nimport { CASINGS_AND_MAGAZINE } from './weapons/casings-and-magazine'\nimport { MOLOTOV } from './weapons/molotov'\nimport { GRENADE } from './weapons/grenade'\nimport { EXPLOSIVE_BARREL } from './weapons/explosive-barrel'\nimport { MUZZLE_FLASH } from './weapons/muzzle-flash'\nimport { SHOPFRONT_GLASS } from './destruction/shopfront-glass'\nimport { DOOR_STAGES } from './destruction/door-stages'\nimport { BUILDING_COLLAPSE } from './destruction/building-collapse'\nimport { BLOCK_WALL } from './destruction/block-wall'\nimport { METAL_DOOR_EFFECT } from './destruction/metal-door'\nimport { IRON_RAILING } from './destruction/iron-railing'\nimport { WOODEN_FENCE } from './destruction/wooden-fence'\nimport { HIT_POINT_BREAKAGE } from './destruction/hit-point-breakage'\nimport { PRESSURE_CYLINDERS } from './destruction/pressure-cylinders'\nimport { BUILDING_FIRE } from './world/building-fire'\nimport { FOG_AND_ASH } from './world/fog-and-ash'\nimport { RAIN_AND_PUDDLES } from './world/rain'\nimport { HEADLIGHTS_IN_RAIN } from './vehicles/headlights'\nimport { SCRAPING_SPARKS } from './vehicles/scraping-sparks'\nimport { ENGINE_SMOKE } from './vehicles/engine-smoke'\nimport { FUEL_LEAK } from './vehicles/fuel-leak'\nimport { CAR_EXPLOSION } from './vehicles/car-explosion'\nimport { WINDSHIELD } from './vehicles/windshield'\nimport { DAMAGE_AND_BITE } from './screen/damage-and-bite'\nimport { INFECTION_PHASES } from './screen/infection-phases'\nimport { PANIC } from './screen/panic'\nimport { BLOATER_BLINDNESS } from './screen/bloater-blindness'\nimport { CAMPFIRE } from './screen/campfire'\nimport { COUGH_AND_BREATH } from './screen/cough-and-breath'\nimport { EVENT_EFFECTS } from './events'\nimport { VIEWMODEL_EFFECTS } from './weapons/viewmodel'\nimport { VEHICLE_EFFECTS } from './vehicles/game-effects'\nimport type { EffectDef } from './types'\n\nconst ALL_EFFECTS: readonly EffectDef[] = [BLOOD_HIT, HEADSHOT, DISMEMBER, BLOATER_CLOUD, SCREAMER_SHRIEK, CORPSE_DECAY, REANIMATION, EYES_IN_FOG, MUZZLE_FLASH, IMPACT_BY_MATERIAL, PENETRATION, CASINGS_AND_MAGAZINE, MOLOTOV, GRENADE, EXPLOSIVE_BARREL, SHOPFRONT_GLASS, DOOR_STAGES, BUILDING_COLLAPSE, BLOCK_WALL, METAL_DOOR_EFFECT, IRON_RAILING, WOODEN_FENCE, HIT_POINT_BREAKAGE, PRESSURE_CYLINDERS, BUILDING_FIRE, RAIN_AND_PUDDLES, FOG_AND_ASH, SCRAPING_SPARKS, ENGINE_SMOKE, FUEL_LEAK, CAR_EXPLOSION, WINDSHIELD, HEADLIGHTS_IN_RAIN, DAMAGE_AND_BITE, INFECTION_PHASES, PANIC, BLOATER_BLINDNESS, CAMPFIRE, COUGH_AND_BREATH]\n\n/** Every effect of the Workshop by id. A game adds, changes or removes entries with `patchEffects`. */\nexport const WORKSHOP_EFFECTS: Readonly<Record<string, EffectDef>> = Object.fromEntries(ALL_EFFECTS.map((effect) => [effect.id, effect]))\n\n/** The Workshop's recipes plus the one-shot effects the destruction events play (`event-effects.json`) and the ones of the weapon in the player's hands and the vehicle ones (`vehicle_fire`, `vehicle_exhaust`, `vehicle_leak`, written around a car): what a world plays by default. */\nexport const GAME_EFFECTS: Readonly<Record<string, EffectDef>> = { ...WORKSHOP_EFFECTS, ...Object.fromEntries([...EVENT_EFFECTS, ...VIEWMODEL_EFFECTS, ...VEHICLE_EFFECTS].map((effect) => [effect.id, effect])) }\n\n/** A copy of the recipes with some changed (a partial replaces the fields it names), added or removed (`null`); a game tunes the Workshop's effects without touching the core. */\nexport function patchEffects(base: Readonly<Record<string, EffectDef>>, changes: Readonly<Record<string, Partial<EffectDef> | EffectDef | null>>): Record<string, EffectDef> {\n const result: Record<string, EffectDef> = { ...base }\n for (const [id, change] of Object.entries(changes)) {\n if (change === null) delete result[id]\n else result[id] = { ...(base[id] ?? {}), ...change, id } as EffectDef\n }\n return result\n}\n","const LEHMER_MULTIPLIER = 16807\nconst LEHMER_MODULUS = 2147483647\nconst LCG_MULTIPLIER = 1664525\nconst LCG_INCREMENT = 1013904223\nconst LCG_RANGE = 4294967296\nconst HASH_MIX = 0x9e3779b1\n\nexport interface MutableVec3 {\n x: number\n y: number\n z: number\n}\n\n/** A seeded stream of numbers in [0, 1). The Lab's generator and the Fluid Standard's are both here, behind the same three calls. */\nexport interface RandomStream {\n /** The next number (the Lab's `rnd`). */\n next(): number\n /** The Lab's `rr(min, max)`: one draw, scaled. */\n between(min: number, max: number): number\n /** The Lab's `rdir`: a random unit vector, two draws (`u`, then the angle), written into `out`. */\n direction(out: MutableVec3): MutableVec3\n readonly state: number\n}\n\nfunction normalizeLehmerSeed(seed: number): number {\n const whole = Math.floor(Math.abs(seed)) % (LEHMER_MODULUS - 1)\n return whole === 0 ? 1 : whole\n}\n\n/** Park-Miller 16807 (the Lab's L16-18). A seed in 1..2147483646 is used as is, so a stream replays the page's. */\nexport function lehmer(seed: number): RandomStream {\n let state = normalizeLehmerSeed(seed)\n const next = (): number => {\n state = (state * LEHMER_MULTIPLIER) % LEHMER_MODULUS\n return (state - 1) / (LEHMER_MODULUS - 1)\n }\n return {\n next,\n between: (min, max) => min + (max - min) * next(),\n direction(out) {\n const up = next() * 2 - 1\n const angle = next() * Math.PI * 2\n const ring = Math.sqrt(1 - up * up)\n out.x = ring * Math.cos(angle)\n out.y = up\n out.z = ring * Math.sin(angle)\n return out\n },\n get state() {\n return state\n },\n }\n}\n\n/** The Fluid Standard's generator (F17-18): a 32-bit linear congruential one. */\nexport function lcg(seed: number): RandomStream {\n let state = seed >>> 0\n const next = (): number => {\n state = (Math.imul(state, LCG_MULTIPLIER) + LCG_INCREMENT) >>> 0\n return state / LCG_RANGE\n }\n return {\n next,\n between: (min, max) => min + (max - min) * next(),\n direction(out) {\n const up = next() * 2 - 1\n const angle = next() * Math.PI * 2\n const ring = Math.sqrt(1 - up * up)\n out.x = ring * Math.cos(angle)\n out.y = up\n out.z = ring * Math.sin(angle)\n return out\n },\n get state() {\n return state\n },\n }\n}\n\nfunction mixEventSeed(eventSeed: number): number {\n let mixed = Math.imul((eventSeed | 0) ^ HASH_MIX, 0x85ebca6b)\n mixed ^= mixed >>> 13\n mixed = Math.imul(mixed, 0xc2b2ae35)\n mixed ^= mixed >>> 16\n return (mixed >>> 0) % (LEHMER_MODULUS - 1) + 1\n}\n\n/** The stream of one event: the same event seed draws the same effect on every machine, whatever else is playing (`randomMode: 'per_event'`). */\nexport function streamFor(eventSeed: number): RandomStream {\n return lehmer(mixEventSeed(eventSeed))\n}\n","import type { ImpactMaterial } from './blood'\n\n/** What each material raises when hit: dust colour, chip colour and the number of sparks (W374-381). Zero means none. */\nconst TABLE: Readonly<Record<ImpactMaterial, { readonly dust: number; readonly chip: number; readonly spark: number }>> = {\n concrete: { dust: 0x8a857a, chip: 0x6e6a63, spark: 2 },\n metal: { dust: 0, chip: 0, spark: 18 },\n wood: { dust: 0x6a5038, chip: 0x7a5530, spark: 0 },\n glass: { dust: 0xbfd6e2, chip: 0xa8c8d8, spark: 0 },\n flesh: { dust: 0, chip: 0, spark: 0 },\n water: { dust: 0, chip: 0, spark: 0 },\n}\n\nexport function sparkSpec(material: ImpactMaterial): { readonly dust: number; readonly chip: number; readonly spark: number } {\n return TABLE[material]\n}\n","import type { WorkshopEngine } from '../../workshop/engine'\nimport type { Vec3Tuple } from '../../workshop/types'\nimport { negated, plus, plusScaled } from '../vector-math'\nimport { sparkSpec } from './impact-table'\n\n/** The blood colour of each infection age (W116) and how much each bleeds (W392). */\nexport const BLOOD_COLORS = [0x8a0a0a, 0x5a0c10, 0x3a1c12, 0x1d1914] as const\nconst BLEED_BY_AGE = [1, 0.7, 0.35, 0.1] as const\n\n/** The dust and dry flakes a decrepit body gives instead of blood. */\nfunction drySplash(engine: WorkshopEngine, point: Vec3Tuple, exitDirection: Vec3Tuple, scale: number): void {\n engine.burst('normal', 16 * scale, { p: point, dir: exitDirection, cone: 1, sp: [0.5, 1.6], drag: 2, life: [0.9, 1.6], size: [0.1, 0.2], grow: 4, c0: 0x2e2822, c1: 0x15130f, a: 0.6, g: -0.15 })\n for (let index = 0; index < engine.scaled(5); index++) engine.debrisPiece({ p: point, dir: exitDirection, cone: 1, sp: [0.5, 2], size: [0.03, 0.008, 0.03], c: 0x1c1916, life: 6 })\n engine.decal([point[0], 0, point[2] + exitDirection[2] * 0.6, 0.25, 0x1a1714, { grow: 0.4, ar: 0.7 }])\n}\n\n/** `bloodHit` (W392-398): a mist at the entry, a jet out of the exit and a cloud, by infection age. */\nexport function bloodHit(engine: WorkshopEngine, point: Vec3Tuple, exitDirection: Vec3Tuple, age: number, scale = 1): void {\n if (age === 3) return drySplash(engine, point, exitDirection, scale)\n const amount = BLEED_BY_AGE[age]!\n const color = BLOOD_COLORS[age]!\n engine.burst('normal', 10 * amount * scale, { p: point, dir: negated(exitDirection), cone: 0.9, sp: [0.2, 0.9], drag: 3, life: [0.25, 0.5], size: [0.08, 0.14], grow: 3, c0: color, a: 0.65 })\n engine.burst('normal', 45 * amount * scale, { p: point, dir: exitDirection, cone: 0.45, sp: [2.5, 6], life: [1, 1.6], size: [0.035, 0.07], c0: color, a: 0.95, g: 9.8, fi: 0, gnd: 3, ds: [0.06, 0.16] })\n engine.burst('normal', 14 * amount * scale, { p: point, dir: exitDirection, cone: 0.5, sp: [1, 2.5], drag: 2.5, life: [0.4, 0.8], size: [0.1, 0.18], grow: 3, c0: color, a: 0.45 })\n}\n\nconst MATERIAL_NAMES = ['concrete', 'metal', 'wood', 'glass', 'flesh', 'water'] as const\nexport type ImpactMaterial = (typeof MATERIAL_NAMES)[number]\n\nfunction waterImpact(engine: WorkshopEngine, point: Vec3Tuple, scale: number): void {\n engine.burst('normal', 22 * scale, { p: point, dir: [0, 1, 0], cone: 0.5, sp: [1.5, 3.5], life: [0.4, 0.7], size: [0.03, 0.06], c0: 0xc8d6e6, a: 0.7, g: 9.8, fi: 0, gnd: 1 })\n engine.ring([plus(point, 0, 0.012, 0), 0.02, 0.6, 0.7, 0x8090a8])\n engine.ring([plus(point, 0, 0.013, 0), 0.02, 0.35, 0.5, 0x8090a8])\n}\n\n/** `impact` (W374-391): what a bullet raises on each material. */\nexport function impact(engine: WorkshopEngine, material: ImpactMaterial, point: Vec3Tuple, normal: Vec3Tuple, scale = 1, age = 0): void {\n if (material === 'flesh') return bloodHit(engine, point, negated(normal), age, scale)\n if (material === 'water') return waterImpact(engine, point, scale)\n const spec = sparkSpec(material)\n if (spec.dust) engine.burst('normal', 14 * scale, { p: point, dir: normal, cone: 0.6, sp: [0.6, 2.4], drag: 2.5, life: [0.6, 1.2], size: [0.08, 0.16], grow: 4, c0: spec.dust, c1: 0x3a3936, a: 0.55, g: -0.1 })\n if (spec.chip) {\n const size = material === 'wood' ? ([0.015, 0.015, 0.09] as const) : material === 'glass' ? ([0.03, 0.004, 0.03] as const) : ([0.03, 0.03, 0.03] as const)\n for (let index = 0; index < engine.scaled(material === 'wood' ? 8 : 6) * scale; index++) {\n engine.debrisPiece({ p: point, dir: normal, cone: 0.8, sp: [1.5, 4], size, k: [0.6, 1.4], c: spec.chip, life: 6 })\n }\n }\n if (spec.spark && engine.quality < 2) {\n engine.burst('streak', spec.spark * scale, { p: point, dir: normal, cone: material === 'metal' ? 0.9 : 0.5, sp: [3, 9], life: [0.15, 0.45], len: 0.025, c0: 0xfff0c0, c1: 0xff5a10, g: 9.8, gnd: 2 })\n engine.burst('add', material === 'metal' ? 3 : 1, { p: point, size: [0.12, 0.2], life: 0.06, c0: 0xffe0a0, fi: 0 })\n if (material === 'metal') engine.flash([plusScaled(point, normal, 0.2), 0xffb060, 2, 0.06, 5])\n }\n if (material === 'glass') engine.burst('add', 5, { p: point, r: 0.15, size: [0.02, 0.05], life: [0.2, 0.5], c0: 0xffffff, a: 0.9, fi: 0.3 })\n if (engine.quality < 2) {\n const size = material === 'glass' ? 0.07 : material === 'metal' ? 0.05 : 0.06\n engine.decal([point[0], point[1], point[2], size, material === 'metal' ? 0x2a2522 : 0x0e0d0c, { n: normal, grow: 0, life: 60 }])\n }\n}\n\nexport function impactMaterialAt(index: number): ImpactMaterial {\n return MATERIAL_NAMES[index]!\n}\n","import { impact } from '../builders/blood'\nimport type { Behavior } from '../behavior'\n\nconst COLUMNS = 6\nconst ROWS = 5\nconst ORIGIN = [-2.2, 0, 2.6] as const\nconst BLOCK_SIZE = [0.5, 0.44, 0.25] as const\nconst OBJECT_ID_DRAWS = 4\nconst FLOOR = 0.13\nconst QUARTER_TURN = 1.5708\n\ninterface Motion {\n vx: number\n vy: number\n vz: number\n wx: number\n wy: number\n wz: number\n rest: boolean\n}\n\nexport interface WallBlock {\n readonly home: readonly [number, number]\n x: number\n y: number\n rotation: [number, number, number]\n /** Bullet holes stuck to this block (local to the block). */\n holes: { x: number; y: number; scale: number; chipTurn: number; chipScale: number }[]\n motion: Motion | undefined\n}\n\nconst clamp = (value: number, low: number, high: number): number => (value < low ? low : value > high ? high : value)\n\nfunction createBlocks(): WallBlock[] {\n const blocks: WallBlock[] = []\n for (let row = 0; row < ROWS; row++) {\n for (let column = 0; column < COLUMNS; column++) {\n const home = [(column - 2.5) * BLOCK_SIZE[0] + (row % 2 ? 0.12 : 0), 0.22 + row * BLOCK_SIZE[1]] as const\n blocks.push({ home, x: home[0], y: home[1], rotation: [0, 0, 0], holes: [], motion: undefined })\n }\n }\n return blocks\n}\n\n/**\n * The free-standing wall of 30 blocks of the page (W926-935, W1039-1046): a bullet leaves a hole and a chip stuck to the nearest block (`bwHit`, which\n * also costs the page the ids three.js gives each new mesh), and when it gives way the blocks fall with the velocities the page draws for them and\n * rest on their sides. The holes travel with the blocks, which is the point of the effect.\n */\nexport function createBlocksBehavior(): Behavior {\n let blocks = createBlocks()\n return {\n reset() {\n blocks = createBlocks()\n },\n act(host, action, args) {\n const random = host.engine.random\n if (action === 'hit') {\n const { x, y, scale } = args as { readonly x: number; readonly y: number; readonly scale: number }\n let best: WallBlock | undefined\n let bestDistance = 9\n for (const block of blocks) {\n const distance = Math.hypot(block.x - x, block.y - y)\n if (distance < bestDistance) {\n bestDistance = distance\n best = block\n }\n }\n if (!best) return\n const offsetX = clamp(x - best.x, -0.2, 0.2)\n const offsetY = clamp(y - best.y, -0.17, 0.17)\n for (let draw = 0; draw < OBJECT_ID_DRAWS; draw++) random.next()\n const chipTurn = random.between(0, 6)\n const chipScale = random.between(0.8, 1.3) * scale\n for (let draw = 0; draw < OBJECT_ID_DRAWS; draw++) random.next()\n best.holes.push({ x: offsetX, y: offsetY, scale, chipTurn, chipScale })\n impact(host.engine, 'concrete', [ORIGIN[0] + x, y, ORIGIN[2] + 0.13], [0, 0, 1], 0.8)\n } else if (action === 'collapse') {\n const { x, y } = args as { readonly x: number; readonly y: number }\n for (const block of blocks) {\n const dx = block.x - x\n const dy = block.y - y\n block.motion = {\n vx: dx * random.between(0.8, 2),\n vy: random.between(0.5, 2.5) + dy * 0.5,\n vz: random.between(-2.2, -0.4),\n wx: random.between(-4, 4),\n wy: random.between(-3, 3),\n wz: random.between(-4, 4),\n rest: false,\n }\n }\n }\n },\n query(_host, name) {\n if (name === 'blocks') return blocks\n throw new Error(`blocks: unknown query ${name}`)\n },\n update(_host, dt) {\n for (const block of blocks) {\n const motion = block.motion\n if (!motion || motion.rest) continue\n motion.vy -= 9.8 * dt\n block.x += motion.vx * dt\n block.y += motion.vy * dt\n block.rotation[0] += motion.wx * dt\n block.rotation[1] += motion.wy * dt\n block.rotation[2] += motion.wz * dt\n if (block.y < FLOOR) {\n block.y = FLOOR\n motion.vy *= -0.25\n motion.vx *= 0.5\n motion.vz *= 0.5\n motion.wx *= 0.5\n motion.wy *= 0.5\n motion.wz *= 0.5\n if (Math.abs(motion.vy) < 0.4 && Math.hypot(motion.vx, motion.vz) < 0.3) {\n motion.rest = true\n block.rotation[0] = Math.round(block.rotation[0] / QUARTER_TURN) * QUARTER_TURN\n block.rotation[2] = Math.round(block.rotation[2] / QUARTER_TURN) * QUARTER_TURN\n }\n }\n }\n },\n }\n}\n","import type { Behavior } from '../behavior'\nimport { carPoint } from '../vehicles/car'\nimport type { Vec3Tuple } from '../../workshop/types'\n\nconst ORIGIN: Vec3Tuple = [5.6, 0, 0.8]\nconst MAX_TILT = 0.25\n\n/**\n * The car of the page after it blew up (`carTick`, W1131): it leaps, spins and lands crooked. Its height is what the burning wreck's fire is emitted\n * from, so the model keeps it; the body is drawn by whoever owns the car.\n */\nexport function createCarBehavior(): Behavior {\n let y = 0\n let rise: number | undefined\n let spin = 0\n let roll = 0\n return {\n reset() {\n y = 0\n rise = undefined\n spin = 0\n roll = 0\n },\n act(_host, action, args) {\n if (action === 'launch') {\n const launch = args as { readonly rise: number; readonly spin: number }\n rise = launch.rise\n spin = launch.spin\n } else if (action === 'step') {\n if (rise === undefined) return\n const dt = (args as { readonly dt: number }).dt\n rise -= 9.8 * dt\n y = Math.max(0, y + rise * dt)\n roll += spin * dt\n spin *= Math.exp(-dt * 2)\n if (y === 0 && rise < 0) {\n rise = undefined\n roll = Math.min(MAX_TILT, Math.max(-MAX_TILT, roll))\n }\n }\n },\n query(_host, name, args) {\n if (name === 'position') return [ORIGIN[0], y, ORIGIN[2]]\n // `carW(x, y, z)` (W104): a point of the car's own space in the world, with the car where it is now\n if (name === 'point') {\n const [x, py, z] = args as readonly [number, number, number]\n const point = carPoint(x, py, z)\n return [point[0], point[1] + y, point[2]]\n }\n if (name === 'roll') return roll\n throw new Error(`car: unknown query ${name}`)\n },\n update() {},\n }\n}\n","import type { Behavior } from '../behavior'\n\n/** A smear of blood that runs down the windshield from a hit (`wsMarks`, W1156). */\nexport interface BloodStreak {\n readonly x: number\n readonly y: number\n readonly dx: number\n readonly dy: number\n readonly width: number\n}\n\nexport interface Crack {\n readonly x: number\n readonly y: number\n /** Each ray is a list of `[dx, dy]` segments that start at the impact. */\n readonly rays: readonly (readonly (readonly [number, number])[])[]\n readonly rings: readonly number[]\n}\n\nconst FULL_TURN_ROUNDED = 6.28\nconst SHORT_RAYS = 7\nconst LONG_RAYS = 12\nconst STREAKS_PER_HIT = 5\n\n/** `crackAt` (W62-63): the star of cracks a hit leaves in glass. Its draws depend on the lengths it draws, so the model keeps the same loop. */\nexport function createCracksBehavior(): Behavior {\n let cracks: Crack[] = []\n let streaks: BloodStreak[][] = []\n return {\n reset() {\n cracks = []\n streaks = []\n },\n act(host, action, args) {\n if (action === 'blood_streaks') {\n const { x, y } = args as { readonly x: number; readonly y: number }\n const random = host.engine.random\n const group: BloodStreak[] = []\n for (let index = 0; index < STREAKS_PER_HIT; index++) {\n group.push({ x: x - 18 + index * 8, y: y - 10, dx: random.between(-3, 3), dy: random.between(18, 40), width: random.between(5, 9) })\n }\n streaks.push(group)\n return\n }\n if (action !== 'crack') return\n const { x, y, big } = args as { readonly x: number; readonly y: number; readonly big: boolean }\n const random = host.engine.random\n const count = big ? LONG_RAYS : SHORT_RAYS\n const rays: [number, number][][] = []\n for (let ray = 0; ray < count; ray++) {\n let angle = (ray / count) * FULL_TURN_ROUNDED + random.between(-0.3, 0.3)\n const segments: [number, number][] = []\n let remaining = big ? random.between(40, 110) : random.between(15, 45)\n while (remaining > 0) {\n const length = random.between(6, 14)\n angle += random.between(-0.35, 0.35)\n segments.push([Math.cos(angle) * length, Math.sin(angle) * length])\n remaining -= length\n }\n rays.push(segments)\n }\n cracks.push({ x, y, rays, rings: big ? [12, 26] : [8] })\n },\n query(_host, name) {\n if (name === 'cracks') return cracks\n if (name === 'streaks') return streaks\n throw new Error(`cracks: unknown query ${name}`)\n },\n update() {},\n }\n}\n","import type { WorkshopEngine } from '../../workshop/engine'\nimport type { Vec3Tuple } from '../../workshop/types'\n\n/** Wood chips and a puff of dust (`chips`, W849). */\nexport function chips(engine: WorkshopEngine, point: Vec3Tuple, direction: Vec3Tuple, count: number, spread?: number): void {\n for (let index = 0; index < engine.scaled(count); index++) {\n engine.debrisPiece({ p: point, dir: direction, cone: spread || 0.9, sp: [1, 3], size: [0.012, 0.012, 0.09], k: [0.6, 1.4], c: 0xb08a5a, life: 20 })\n }\n engine.burst('normal', count, { p: point, dir: direction, cone: 0.8, sp: [0.3, 1.2], drag: 2, life: [0.8, 1.4], size: [0.08, 0.15], grow: 3, c0: 0x7a6048, a: 0.4 })\n}\n\n/** Sparks off metal (`sparkAt`, W899). */\nexport function sparkAt(engine: WorkshopEngine, point: Vec3Tuple, direction: Vec3Tuple, count: number, spread?: number): void {\n engine.burst('streak', count, { p: point, dir: direction, cone: spread || 0.8, sp: [2.5, 7], life: [0.2, 0.5], len: 0.025, c0: 0xfff0c0, c1: 0xff5010, g: 9.8, gnd: 2 })\n engine.burst('add', 2, { p: point, size: [0.1, 0.18], life: 0.06, c0: 0xffd8a0, fi: 0 })\n}\n\n/** Rust flakes and a little dust (`rustAt`, W900). */\nexport function rustAt(engine: WorkshopEngine, point: Vec3Tuple, count: number): void {\n engine.burst('normal', count, { p: point, r: 0.1, sp: [0.1, 0.5], life: [1, 1.6], size: [0.02, 0.035], c0: 0x7a4a2a, a: 0.9, g: 6, fi: 0, gnd: 1 })\n engine.burst('normal', Math.ceil(count / 2), { p: point, r: 0.08, sp: [0.2, 0.5], drag: 2, life: [0.8, 1.3], size: [0.08, 0.14], grow: 3, c0: 0x6a4a34, a: 0.3 })\n}\n","import type { Behavior, BehaviorHost } from '../behavior'\nimport { sparkAt } from '../builders/shared'\nimport { normalized } from '../vector-math'\nimport type { Vec3Tuple } from '../../workshop/types'\n\nconst SPOT: Vec3Tuple = [-2.2, 0, 2.6]\nconst WALL_X = -8.2\nconst QUARTER_TURN = Math.PI / 2\n\nconst lerp = (from: number, to: number, amount: number): number => from + (to - from) * amount\nconst clamp = (value: number, low: number, high: number): number => (value < low ? low : value > high ? high : value)\n\n/**\n * The pressure cylinder of the page (W1021-1038): an extinguisher that spins on the floor pushed by its own jet, or an oxygen bottle that becomes a\n * rocket and hits the wall. `step` moves it the way the page's tick does, drawing the sparks of the wall hit; the recipe then asks where the jet comes\n * out (`tip`, `back`, `direction`) to emit it.\n */\nexport function createCylinderBehavior(): Behavior {\n let position: Vec3Tuple = SPOT\n let velocity: Vec3Tuple = [0, 0, 0]\n let rotationZ = 0\n let angle = 0\n let spin = 0\n let live = false\n let oxygen = false\n let direction: Vec3Tuple = [1, 0, 0]\n\n const clear = (): void => {\n position = SPOT\n velocity = [0, 0, 0]\n rotationZ = 0\n angle = 0\n spin = 0\n live = false\n direction = [1, 0, 0]\n }\n\n return {\n reset: clear,\n act(host: BehaviorHost, action: string, args: unknown) {\n if (action === 'choose') {\n clear()\n oxygen = (args as { readonly oxygen: boolean }).oxygen\n } else if (action === 'puncture') {\n live = true\n if (oxygen) velocity = [-5, 0, -0.3]\n else spin = 9\n } else if (action === 'step') {\n if (!live) return\n const { dt, t } = args as { readonly dt: number; readonly t: number }\n const k = clamp(1 - (t - 0.6) / 3.4, 0, 1)\n if (!oxygen) {\n rotationZ = lerp(rotationZ, QUARTER_TURN, dt * 8)\n const y = lerp(position[1], 0.09, dt * 8)\n angle += spin * k * dt\n direction = [Math.cos(angle), 0, -Math.sin(angle)]\n position = [position[0] + direction[0] * (-0.6 * k * dt), y + direction[1] * (-0.6 * k * dt), position[2] + direction[2] * (-0.6 * k * dt)]\n return\n }\n let [vx, vy, vz] = velocity\n if (k > 0) vx -= 4 * k * dt\n const damping = Math.exp(-dt * (k > 0 ? 0.3 : 3))\n vx *= damping\n vy *= damping\n vz *= damping\n velocity = [vx, vy, vz]\n rotationZ = lerp(rotationZ, -QUARTER_TURN, dt * 10)\n position = [position[0] + vx * dt, lerp(position[1], 0.12, dt * 10) + vy * dt, position[2] + vz * dt]\n if (position[0] < WALL_X) {\n position = [WALL_X, position[1], position[2]]\n velocity = [velocity[0] * -0.3, velocity[1], velocity[2]]\n host.engine.screen.shake += 0.08\n sparkAt(host.engine, position, normalized([1, 0.5, 0]), 16, 1)\n }\n }\n },\n query(_host, name) {\n switch (name) {\n case 'live': return live\n case 'oxygen': return oxygen\n case 'position': return position\n case 'direction': return direction\n case 'speed_x': return velocity[0]\n case 'rotation_z': return rotationZ\n case 'tip': return [position[0] + direction[0] * 0.3, 0.1, position[2] + direction[2] * 0.3]\n case 'back': return [position[0] + 1.1, position[1], position[2]]\n default: throw new Error(`cylinder: unknown query ${name}`)\n }\n },\n update() {},\n }\n}\n","import type { Behavior } from '../behavior'\n\n/** The geometry of a box plate as three.js lays out its vertices: `BoxGeometry(width, height, depth, columns, rows, 1)` moved by `(shiftX, shiftY)`. */\nexport interface PlateGeometry {\n readonly width: number\n readonly height: number\n readonly columns: number\n readonly rows: number\n readonly shiftX: number\n readonly shiftY: number\n}\n\n/** The metal door of the page: 1.1 x 2.2 m, 14 x 24 segments, hinged at its left edge (W960). */\nexport const METAL_DOOR: PlateGeometry = { width: 1.1, height: 2.2, columns: 14, rows: 24, shiftX: 0.56, shiftY: 1.1 }\n\n/** Every vertex of the plate seen from the front (x, y), the front and back faces and the four thin edges, each vertex once per face it belongs to. */\nexport function plateVertices(geometry: PlateGeometry): readonly (readonly [number, number])[] {\n const { width, height, columns, rows, shiftX, shiftY } = geometry\n const xs = Array.from({ length: columns + 1 }, (_, column) => (column * width) / columns - width / 2)\n const ys = Array.from({ length: rows + 1 }, (_, row) => (row * height) / rows - height / 2)\n const vertices: [number, number][] = []\n // the sides (px, nx): two vertices, front and back, for each row; then top and bottom (py, ny): the same for each column; then the two big faces\n for (const x of [width / 2, -width / 2]) for (const y of ys) for (let depth = 0; depth < 2; depth++) vertices.push([x, y])\n for (const y of [height / 2, -height / 2]) for (const x of xs) for (let depth = 0; depth < 2; depth++) vertices.push([x, y])\n for (let face = 0; face < 2; face++) for (const y of ys) for (const x of xs) vertices.push([x, y])\n return vertices.map(([x, y]) => [x + shiftX, y + shiftY] as const)\n}\n\ninterface DentArgs {\n readonly x: number\n readonly y: number\n readonly radius: number\n readonly depth: number\n}\n\n/**\n * `dentAt` (W1002): pushes the vertices within `radius` of a hit in and draws one random number for each, so how many it draws depends on how the plate\n * is cut. The model counts them; the deformed mesh belongs to whoever draws the door.\n */\nlet doorVertices: ReturnType<typeof plateVertices> | undefined\n\nexport function createPlateBehavior(geometry: PlateGeometry = METAL_DOOR): Behavior {\n const vertices = geometry === METAL_DOOR ? (doorVertices ??= plateVertices(geometry)) : plateVertices(geometry)\n let dents: { x: number; y: number; radius: number; depth: number; vertices: number }[] = []\n return {\n reset() {\n dents = []\n },\n act(host, action, args) {\n if (action !== 'dent') return\n const { x, y, radius, depth } = args as DentArgs\n let touched = 0\n for (const [vertexX, vertexY] of vertices) {\n if (Math.hypot(vertexX - x, vertexY - y) < radius) {\n host.engine.random.next()\n touched++\n }\n }\n dents.push({ x, y, radius, depth, vertices: touched })\n },\n query(_host, name) {\n if (name === 'dents') return dents\n throw new Error(`plate: unknown query ${name}`)\n },\n update() {},\n }\n}\n","import type { Behavior, BehaviorHost } from '../behavior'\n\ninterface InitArgs { readonly count: number; readonly wind: number }\ninterface UpdateArgs { readonly dt: number; readonly splash: boolean }\n\n/**\n * The page's rain (`initRain` and `updateRain`, W321-331) as a prop an effect starts and ticks: `init` fills the box with streaks, `update` moves them and\n * leaves the splashes. The field itself lives in the engine (`engine.rain`), where the renderer reads it; the weather package drives the same field.\n */\nexport function createRainBehavior(): Behavior {\n return {\n reset() {},\n act(host: BehaviorHost, action: string, args: unknown) {\n const { engine, camera } = host\n if (action === 'init') {\n const { count, wind } = args as InitArgs\n engine.rain.init(count, wind)\n } else if (action === 'update') {\n const { dt, splash } = args as UpdateArgs\n engine.rain.update(dt, splash, camera.position)\n }\n },\n query(host, name) {\n if (name === 'count') return host.engine.rain.count\n if (name === 'on') return host.engine.rain.on\n throw new Error(`rain: unknown query ${name}`)\n },\n update() {},\n }\n}\n","import type { Behavior } from '../behavior'\n\nconst WALKER_COUNT = 10\nconst ARRIVAL_DISTANCE = 0.05\nconst SWAY_AMPLITUDE = 0.04\nconst BOB_AMPLITUDE = 0.04\nconst STRIDE_FACTOR = 4.5\n\n/**\n * The `ph` of each walker, the offset of its sway and stride. The page drew them at load (`Math.random() * 6`) after three.js had drawn a few hundred\n * numbers for object ids, so they cannot be replayed; the three that matter (the runners with red eyes, whose trails start at the eyes) were fitted to\n * the recording until every position matched to four decimals. The others only turn a zombie nobody reads.\n */\nconst PHASES = [0, 0, 3.59629, 0, 0, 0, 2.946505, 0, 0, 0, 2.44693] as const\n\ninterface Walker {\n x: number\n z: number\n facing: number\n bob: number\n sway: number\n walk: { toX: number; toZ: number; speed: number } | undefined\n}\n\nfunction createWalkers(): Walker[] {\n return Array.from({ length: WALKER_COUNT }, () => ({ x: 0, z: 0, facing: 0, bob: 0, sway: 0, walk: undefined }))\n}\n\ninterface PlaceArgs { readonly index: number; readonly x: number; readonly z: number; readonly facing: number }\ninterface WalkArgs { readonly index: number; readonly to: readonly [number, number, number]; readonly speed: number }\ninterface MoveArgs { readonly index: number; readonly x: number }\n\n/**\n * The ten walking zombies of the page (W151, W190-197): they walk to a point at a speed, face where they go, bob with each stride and sway. An effect\n * reads where their eyes are (`point`) to start a trail from them. Phases are the page's, so the sway and bob repeat to the last digit.\n */\nexport function createWalkersBehavior(phases: readonly number[] = PHASES): Behavior {\n let walkers = createWalkers()\n return {\n reset() {\n walkers = createWalkers()\n },\n act(_host, action, args) {\n if (action === 'place') {\n const { index, x, z, facing } = args as PlaceArgs\n Object.assign(walkers[index]!, { x, z, facing })\n } else if (action === 'walk') {\n const { index, to, speed } = args as WalkArgs\n walkers[index]!.walk = { toX: to[0], toZ: to[2], speed }\n } else if (action === 'move_x') {\n const { index, x } = args as MoveArgs\n walkers[index]!.x = x\n }\n },\n query(_host, name, args) {\n const walker = walkers[args[0] as number]!\n if (name === 'x') return walker.x\n if (name === 'z') return walker.z\n if (name === 'is_walking') return walker.walk !== undefined\n if (name !== 'point') throw new Error(`walkers: unknown query ${name}`)\n const [localX, localY, localZ] = args[1] as readonly [number, number, number]\n const swayedX = Math.cos(walker.sway) * localX - Math.sin(walker.sway) * localY\n const swayedY = Math.sin(walker.sway) * localX + Math.cos(walker.sway) * localY + walker.bob\n return [Math.cos(walker.facing) * swayedX + Math.sin(walker.facing) * localZ + walker.x, swayedY, -Math.sin(walker.facing) * swayedX + Math.cos(walker.facing) * localZ + walker.z]\n },\n update(_host, dt, elapsed) {\n walkers.forEach((walker, index) => {\n walker.sway = Math.sin(elapsed * 1.3 + phases[index + 1]!) * SWAY_AMPLITUDE\n const walk = walker.walk\n if (!walk) return\n const dx = walk.toX - walker.x\n const dz = walk.toZ - walker.z\n const distance = Math.hypot(dx, dz)\n if (distance <= ARRIVAL_DISTANCE) {\n walker.walk = undefined\n return\n }\n const step = Math.min(distance, walk.speed * dt)\n walker.x += (dx / distance) * step\n walker.z += (dz / distance) * step\n walker.facing = Math.atan2(dx, dz)\n walker.bob = Math.abs(Math.cos(elapsed * walk.speed * STRIDE_FACTOR + phases[index + 1]!)) * BOB_AMPLITUDE\n })\n },\n}\n}\n","import type { Behavior } from './behavior'\nimport { createBlocksBehavior } from './behaviors/blocks'\nimport { createCarBehavior } from './behaviors/car'\nimport { createCracksBehavior } from './behaviors/cracks'\nimport { createCylinderBehavior } from './behaviors/cylinder'\nimport { createPlateBehavior } from './behaviors/plate'\nimport { createRainBehavior } from './behaviors/rain'\nimport { createWalkersBehavior } from './behaviors/walkers'\n\n/** How to build each prop the Workshop animates by hand, by id (see `Behavior`). A game adds its own the same way. */\nexport const WORKSHOP_BEHAVIORS: Readonly<Record<string, () => Behavior>> = {\n walkers: () => createWalkersBehavior(),\n cracks: () => createCracksBehavior(),\n blocks: () => createBlocksBehavior(),\n plate: () => createPlateBehavior(),\n cylinder: () => createCylinderBehavior(),\n car: () => createCarBehavior(),\n rain: () => createRainBehavior(),\n}\n\n/** A fresh set of the props the Workshop animates by hand, by id; one per runner, since they keep state. */\nexport function createWorkshopBehaviors(factories: Readonly<Record<string, () => Behavior>> = WORKSHOP_BEHAVIORS): Record<string, Behavior> {\n return Object.fromEntries(Object.entries(factories).map(([id, make]) => [id, make()]))\n}\n","export interface Schedule {\n /** Runs `action` once `now` reaches `time`. */\n at(time: number, action: () => void): void\n /** Runs what is due at `now`, from the last one added to the first (the Lab's L1085 and the Workshop's W1280 walk the list backwards, and so does this). */\n update(now: number): void\n clear(): void\n readonly pending: number\n}\n\ninterface Entry {\n time: number\n action: () => void\n}\n\nconst NOOP = (): void => {}\n\nexport function createSchedule(): Schedule {\n const entries: Entry[] = []\n const spare: Entry[] = []\n return {\n at(time, action) {\n const entry = spare.pop() ?? { time: 0, action: NOOP }\n entry.time = time\n entry.action = action\n entries.push(entry)\n },\n update(now) {\n for (let index = entries.length - 1; index >= 0; index--) {\n const entry = entries[index]\n if (!entry || entry.time > now) continue\n const action = entry.action\n entries.splice(index, 1)\n entry.action = NOOP\n spare.push(entry)\n action()\n }\n },\n clear() {\n for (const entry of entries) {\n entry.action = NOOP\n spare.push(entry)\n }\n entries.length = 0\n },\n get pending() {\n return entries.length\n },\n }\n}\n","import type { RandomStream } from '../random'\nimport { normalized } from './vector-math'\n\n/** A value an effect computes: a number, a flag, a name or a vector. */\nexport type EffectValue = number | boolean | string | readonly number[]\n\n/** A computed field of a recipe. Plain data: `{ $: 'add', a: [1, { $: 't' }] }`. Built with the helpers in `dsl.ts`. */\nexport interface Expr {\n readonly $: string\n readonly a?: readonly unknown[]\n}\n\nexport interface ExprContext {\n readonly random: RandomStream\n readonly param: number\n readonly time: number\n readonly dt: number\n readonly index: number\n readonly variables: ReadonlyMap<string, unknown>\n readonly scaled: (count: number) => number\n readonly quality: number\n /** True on the first run after the effect was chosen, false on its repeats (the page's `fresh`). */\n readonly fresh: boolean\n /** Which way the played effect points and which surface it hit, in the world (`play`'s `direction` and `normal`). */\n readonly direction: readonly number[]\n readonly normal: readonly number[]\n readonly query: (behavior: string, name: string, args: readonly unknown[]) => unknown\n readonly camera: { readonly position: readonly number[]; readonly forward: (local: readonly number[]) => readonly number[] }\n}\n\nexport function isExpr(value: unknown): value is Expr {\n return typeof value === 'object' && value !== null && !Array.isArray(value) && '$' in value\n}\n\ntype Vector = readonly number[]\nconst asNumber = (value: unknown): number => value as number\nconst asVector = (value: unknown): Vector => value as Vector\nconst EASE_LIMIT = 1\n\nfunction ease(value: number): number {\n return value < 0 ? 0 : value > EASE_LIMIT ? EASE_LIMIT : value * value * (3 - 2 * value)\n}\n\nfunction reduceVectors(operands: readonly unknown[], combine: (accumulated: number, next: number) => number): Vector {\n const [first, ...rest] = operands.map(asVector)\n return rest.reduce<Vector>((total, next) => [combine(total[0]!, next[0]!), combine(total[1]!, next[1]!), combine(total[2]!, next[2]!)], first!)\n}\n\nfunction evaluateCall(name: string, args: readonly unknown[]): unknown {\n const [first, second, third] = args\n switch (name) {\n case 'concat': return args.map(String).join('')\n case 'add': return args.reduce<number>((total, next) => total + asNumber(next), 0)\n case 'sub': return asNumber(first) - asNumber(second)\n case 'mul': return args.reduce<number>((total, next) => total * asNumber(next), 1)\n case 'div': return asNumber(first) / asNumber(second)\n case 'mod': return asNumber(first) % asNumber(second)\n case 'neg': return -asNumber(first)\n case 'abs': return Math.abs(asNumber(first))\n case 'sqrt': return Math.sqrt(asNumber(first))\n case 'exp': return Math.exp(asNumber(first))\n case 'sin': return Math.sin(asNumber(first))\n case 'cos': return Math.cos(asNumber(first))\n case 'atan2': return Math.atan2(asNumber(first), asNumber(second))\n case 'min': return Math.min(...args.map(asNumber))\n case 'max': return Math.max(...args.map(asNumber))\n case 'floor': return Math.floor(asNumber(first))\n case 'ceil': return Math.ceil(asNumber(first))\n case 'round': return Math.round(asNumber(first))\n case 'clamp': return asNumber(first) < asNumber(second) ? asNumber(second) : asNumber(first) > asNumber(third) ? asNumber(third) : asNumber(first)\n case 'lerp': return asNumber(first) + (asNumber(second) - asNumber(first)) * asNumber(third)\n case 'ease': return ease(asNumber(first))\n case 'lt': return asNumber(first) < asNumber(second)\n case 'gt': return asNumber(first) > asNumber(second)\n case 'le': return asNumber(first) <= asNumber(second)\n case 'ge': return asNumber(first) >= asNumber(second)\n case 'eq': return first === second\n case 'ne': return first !== second\n case 'not': return !first\n case 'vadd': return reduceVectors(args, (total, next) => total + next)\n case 'vsub': return reduceVectors(args, (total, next) => total - next)\n case 'vscale': return asVector(first).map((component) => component * asNumber(second))\n case 'vnorm': return normalized(asVector(first) as readonly [number, number, number])\n case 'vneg': return asVector(first).map((component) => -component)\n case 'vlerp': return asVector(first).map((component, axis) => component + (asVector(second)[axis]! - component) * asNumber(third))\n case 'vx': return asVector(first)[0]\n case 'vy': return asVector(first)[1]\n case 'vz': return asVector(first)[2]\n case 'vdist': {\n const dx = asVector(first)[0]! - asVector(second)[0]!\n const dy = asVector(first)[1]! - asVector(second)[1]!\n const dz = asVector(first)[2]! - asVector(second)[2]!\n return Math.sqrt(dx * dx + dy * dy + dz * dz)\n }\n case 'with': return asVector(first).map((component, axis) => (axis === asNumber(second) ? asNumber(third) : component))\n default: throw new Error(`unknown expression: ${name}`)\n }\n}\n\n/** Resolves every `Expr` inside `value` (arrays and plain objects are walked in order, so the draws follow the order the page wrote its fields). */\nexport function resolve(value: unknown, context: ExprContext): unknown {\n if (Array.isArray(value)) return value.map((entry) => resolve(entry, context))\n if (typeof value !== 'object' || value === null) return value\n if (!isExpr(value)) return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, resolve(entry, context)]))\n const args = value.a ?? []\n switch (value.$) {\n case 'rnd': return context.random.between(asNumber(resolve(args[0], context)), asNumber(resolve(args[1], context)))\n case 'rand': return context.random.next()\n case 'param': return (args[0] as readonly unknown[])[context.param]\n case 'paramIndex': return context.param\n case 't': return context.time\n case 'dt': return context.dt\n case 'index': return context.index\n case 'quality': return context.quality\n case 'fresh': return context.fresh\n case 'direction': return context.direction\n case 'normal': return context.normal\n case 'scaled': return context.scaled(asNumber(resolve(args[0], context)))\n case 'var': return context.variables.get(args[0] as string)\n case 'at': return (resolve(args[0], context) as readonly unknown[])[asNumber(resolve(args[1], context))]\n case 'query': return context.query(args[0] as string, args[1] as string, args.slice(2).map((entry) => resolve(entry, context)))\n case 'cameraPosition': return context.camera.position\n case 'cameraForward': return context.camera.forward(asVector(resolve(args[0], context)))\n case 'let': return resolve(args[2], { ...context, variables: new Map([...context.variables, [args[0] as string, resolve(args[1], context)]]) })\n case 'ite': return resolve(resolve(args[0], context) ? args[1] : args[2], context)\n case 'and': return args.reduce<unknown>((result, next) => (result ? resolve(next, context) : result), true)\n case 'or': return args.reduce<unknown>((result, next) => (result ? result : resolve(next, context)), false)\n default: return evaluateCall(value.$, args.map((entry) => resolve(entry, context)))\n }\n}\n","import type { WorkshopEngine } from '../../workshop/engine'\nimport type { SpriteSpawn, Vec3Tuple } from '../../workshop/types'\nimport { normalized, plus } from '../vector-math'\n\nconst UP: Vec3Tuple = [0, 1, 0]\nconst ZERO: Vec3Tuple = [0, 0, 0]\nconst FULL_TURN_DRAWN = 6.283\n\nexport interface FireEmitOptions {\n readonly wind?: Vec3Tuple\n readonly dir?: Vec3Tuple\n readonly smoke?: number\n readonly smokeColor?: number\n readonly smokeAlpha?: number\n readonly noSmoke?: boolean\n}\n\nfunction scaledSizes(range: readonly [number, number], factor: number): readonly [number, number] {\n return [range[0] * Math.sqrt(factor), range[1] * Math.sqrt(factor)]\n}\n\n/** `fireEmit` (W399-405): flames, a hot core, embers and smoke for one frame, from a point and a radius, scaled by `factor`. */\nexport function fireEmit(engine: WorkshopEngine, key: string, dt: number, centre: Vec3Tuple, radius: number, factor: number, options: FireEmitOptions = {}): void {\n const wind = options.wind ?? ZERO\n const sqrtFactor = Math.sqrt(factor)\n engine.emit(`${key}f`, 'add', 90 * factor, dt, () => ({\n p: centre, r: radius, flat: true, dir: options.dir ?? UP, cone: 0.35, sp: [0.4, 1.4], v: wind, life: [0.35, 0.85], size: scaledSizes([0.22, 0.5], factor), grow: 0.35, c0: 0xffd27a, c1: 0x8a1a05, a: 0.85, fi: 0.15, g: -2.2,\n }))\n engine.emit(`${key}c`, 'add', 30 * factor, dt, () => ({\n p: centre, r: radius * 0.6, flat: true, dir: UP, cone: 0.3, sp: [0.3, 0.8], life: [0.2, 0.4], size: scaledSizes([0.25, 0.4], factor), grow: 0.6, c0: 0xfff2c0, c1: 0xff8a20, a: 0.8, fi: 0.1, g: -1,\n }))\n if (engine.quality < 2) {\n engine.emit(`${key}e`, 'streak', 10 * factor, dt, () => {\n const jitter = plus(wind, engine.random.between(-0.4, 0.4), 0, engine.random.between(-0.4, 0.4))\n return { p: centre, box: [radius, 0, radius], dir: UP, cone: 0.5, sp: [1.5, 3.5], v: jitter, life: [0.8, 1.8], len: 0.03, c0: 0xffb050, c1: 0x601000, g: -0.6, drag: 0.4 }\n })\n }\n if (options.noSmoke) return\n engine.emit(`${key}s`, 'normal', (options.smoke || 14) * factor, dt, () => ({\n p: plus(centre, 0, 0.8 * sqrtFactor, 0), r: radius * 0.7, dir: UP, cone: 0.35, sp: [0.6, 1.4], v: wind, drag: 0.15, life: [2.5, 4.5], size: scaledSizes([0.4, 0.8], factor), grow: 4, c0: options.smokeColor || 0x2a2522, c1: 0x121318, a: options.smokeAlpha || 0.42, fi: 0.2, g: -0.25,\n }))\n}\n\nfunction ringOfDust(engine: WorkshopEngine, point: Vec3Tuple, scale: number): void {\n for (let index = 0; index < engine.scaled(44); index++) {\n const angle = engine.random.next() * FULL_TURN_DRAWN\n const spec: SpriteSpawn = {\n p: plus(point, Math.cos(angle) * 0.5, 0.15, Math.sin(angle) * 0.5), dir: [Math.cos(angle), 0.08, Math.sin(angle)], cone: 0.15, sp: [4, 9 * scale], drag: 2.4, life: [1.4, 2.6], size: [0.5, 0.9], grow: 4, c0: 0x5d574d, c1: 0x2a2724, a: 0.5, fi: 0.1,\n }\n engine.spawn('normal', spec)\n }\n}\n\n/** `explode` (W406-416): the Workshop's blast, scaled by `scale`, that the Lab's profiles later replaced. `cameraPosition` turns the second ring to face the viewer. */\nexport function explode(engine: WorkshopEngine, point: Vec3Tuple, scale: number, cameraPosition: Vec3Tuple, chipColor?: number): void {\n engine.flash([plus(point, 0, 1, 0), 0xffa040, 14 * scale, 0.5, 30, 'boom'])\n engine.screen.shake += 0.7 * scale\n engine.screen.white += 0.18 * scale\n engine.burst('add', 55 * scale, { p: point, r: 0.3 * scale, sp: [2, 7 * scale], drag: 4, life: [0.25, 0.6], size: [0.7 * scale, 1.4 * scale], grow: 1.7, c0: 0xfff0b0, c1: 0x9a2a08, fi: 0.04 })\n engine.burst('add', 28 * scale, { p: plus(point, 0, 0.3, 0), r: 0.3, dir: UP, cone: 0.7, sp: [2, 5 * scale], drag: 1.5, life: [0.6, 1.1], size: [0.6 * scale, 1 * scale], grow: 1.3, c0: 0xffc060, c1: 0x5a1205, g: -2, fi: 0.05 })\n engine.ring([plus(point, 0, 0.05, 0), 0.3, 7 * scale, 0.55, 0xffd8a0])\n const toCamera = normalized([cameraPosition[0] - point[0], cameraPosition[1] - point[1], cameraPosition[2] - point[2]])\n engine.ring([plus(point, 0, 1, 0), 0.3, 5 * scale, 0.35, 0xfff0d0, toCamera])\n engine.burst('normal', 32 * scale, { p: plus(point, 0, 0.6, 0), r: 0.8 * scale, dir: UP, cone: 1.1, sp: [1, 3], drag: 1.2, life: [3, 6], size: [1 * scale, 1.8 * scale], grow: 2.6, c0: 0x2b2724, c1: 0x0f1013, a: 0.6, fi: 0.18, g: -0.35 })\n ringOfDust(engine, point, scale)\n if (engine.quality < 2) engine.burst('streak', 44 * scale, { p: point, dir: UP, cone: 1.45, sp: [10, 26], life: [0.3, 0.7], len: 0.03, c0: 0xffc070, c1: 0x802000, g: 9.8, gnd: 2 })\n for (let index = 0; index < engine.scaled(16 * scale); index++) {\n engine.debrisPiece({ p: point, dir: UP, cone: 1.2, sp: [3, 9], size: [0.12, 0.06, 0.1], k: [0.6, 1.6], c: chipColor || 0x2a2622, life: 9 })\n }\n engine.decal([point[0], engine.context.groundAt(point[0], point[2]), point[2], 3 * scale, 0x0a0a0a, { grow: 0.15, life: 60 }])\n}\n","import type { WorkshopEngine } from '../../workshop/engine'\nimport type { Vec3Tuple } from '../../workshop/types'\nimport { normalized, plus, plusScaled, scaledBy } from '../vector-math'\n\nconst FULL_TURN_DRAWN = 6.283\n\nexport const WEAPON_NAMES = ['glock', 'ak', 'r870', 'barrett'] as const\nexport type WeaponName = (typeof WEAPON_NAMES)[number]\n\n/** Per weapon: flash size, petals, light, smoke puffs and length of the petals (W417). */\nconst WEAPON_SIZES: Readonly<Record<WeaponName, readonly [number, number, number, number, number]>> = {\n glock: [0.35, 4, 3, 3, 0.25],\n ak: [0.6, 5, 5, 5, 0.45],\n r870: [0.95, 8, 7, 10, 0.6],\n barrett: [1.45, 6, 11, 14, 0.5],\n}\n\nfunction barrettBrake(engine: WorkshopEngine, tip: Vec3Tuple, direction: Vec3Tuple): void {\n const side = normalized([direction[2], 0, -direction[0]])\n for (const sign of [-1, 1]) {\n const sideways = scaledBy(side, sign)\n for (let index = 0; index < engine.scaled(8); index++) {\n engine.spawn('add', { p: plusScaled(tip, side, sign * 0.06), dir: sideways, cone: 0.25, sp: [2, 5], drag: 6, life: 0.09, size: [0.25, 0.4], c0: 0xffc070, c1: 0xff6010, fi: 0 })\n }\n engine.burst('normal', 10, { p: tip, dir: normalized(plus(sideways, 0, 0.2, 0)), cone: 0.3, sp: [1.5, 3], drag: 2, life: [1.2, 2], size: [0.15, 0.25], grow: 5, c0: 0x8a8a86, a: 0.3, g: -0.2 })\n }\n for (let index = 0; index < engine.scaled(26); index++) {\n const angle = engine.random.next() * FULL_TURN_DRAWN\n engine.spawn('normal', { p: [tip[0] + Math.cos(angle) * 0.3, 0.05, tip[2] + Math.sin(angle) * 0.3], dir: [Math.cos(angle), 0.15, Math.sin(angle)], cone: 0.2, sp: [1.5, 3.5], drag: 2, life: [1, 1.8], size: [0.2, 0.35], grow: 3, c0: 0x5a554b, a: 0.4 })\n }\n}\n\n/** `muzzle` (W417-429): flash core, petals along the barrel and smoke, scaled by weapon; the Barrett also vents sideways and lifts the dust. */\nexport function muzzle(engine: WorkshopEngine, weapon: WeaponName, tip: Vec3Tuple, direction: Vec3Tuple): void {\n const [flashSize, petals, light, smoke, length] = WEAPON_SIZES[weapon]\n engine.burst('add', 3, { p: tip, size: [flashSize * 0.45, flashSize * 0.75], life: 0.055, c0: 0xfff4d0, fi: 0 })\n for (let index = 0; index < engine.scaled(petals * 2); index++) {\n engine.spawn('add', { p: plusScaled(tip, direction, engine.random.between(0, length)), r: 0.03, size: [flashSize * 0.22, flashSize * 0.38], life: 0.06, c0: 0xffb050, c1: 0xff7020, fi: 0 })\n }\n engine.burst('streak', petals, { p: tip, dir: direction, cone: 0.55, sp: [6, 12], life: 0.05, len: 0.025, c0: 0xffd080 })\n if (weapon === 'barrett') barrettBrake(engine, tip, direction)\n engine.burst('normal', smoke, { p: tip, dir: direction, cone: 0.45, sp: [0.4, 1.6], drag: 2.2, life: [1, 2.2], size: [0.06, 0.14], grow: 6, c0: 0x8a8a86, c1: 0x55565a, a: 0.22, g: -0.25, fi: 0.05 })\n engine.flash([tip, 0xffc070, light, 0.05, 10])\n}\n\nexport type CasingType = 'shell' | '50' | ''\n\n/** `casing` (W430): a brass case, or a red shell, thrown out of the port. */\nexport function casing(engine: WorkshopEngine, port: Vec3Tuple, type: CasingType): void {\n const size = type === 'shell' ? ([0.03, 0.03, 0.07] as const) : type === '50' ? ([0.024, 0.024, 0.13] as const) : ([0.014, 0.014, 0.05] as const)\n engine.debrisPiece({ p: port, dir: normalized([0.8, 0.6, 0.2]), cone: 0.3, sp: [1.6, 2.6], size, c: type === 'shell' ? 0xb5281e : 0xc9a24a, spin: 20, bounce: 0.45, life: 20 })\n}\n","import type { WorkshopEngine } from '../workshop/engine'\nimport type { Vec3Tuple } from '../workshop/types'\nimport { bloodHit, impact } from './builders/blood'\nimport type { ImpactMaterial } from './builders/blood'\nimport { explode, fireEmit } from './builders/fire'\nimport type { FireEmitOptions } from './builders/fire'\nimport { chips, rustAt, sparkAt } from './builders/shared'\nimport { casing, muzzle } from './builders/weapon'\nimport type { CasingType, WeaponName } from './builders/weapon'\n\nexport interface MacroContext {\n readonly engine: WorkshopEngine\n readonly cameraPosition: Vec3Tuple\n readonly dt: number\n}\n\ntype MacroArgs = readonly unknown[]\n\n/** The helpers the page's recipes share (`bloodHit`, `impact`, `fireEmit`, `explode`, `muzzle`, `casing`, `chips`, `sparkAt`, `rustAt`), called by name from a step. */\nexport const MACROS: Readonly<Record<string, (context: MacroContext, args: MacroArgs) => void>> = {\n blood_hit: ({ engine }, [point, direction, age, scale]) => bloodHit(engine, point as Vec3Tuple, direction as Vec3Tuple, age as number, scale as number | undefined),\n impact: ({ engine }, [material, point, normal, scale, age]) => impact(engine, material as ImpactMaterial, point as Vec3Tuple, normal as Vec3Tuple, scale as number | undefined, age as number | undefined),\n fire_emit: ({ engine, dt }, [key, centre, radius, factor, options]) => fireEmit(engine, key as string, dt, centre as Vec3Tuple, radius as number, factor as number, options as FireEmitOptions | undefined),\n explode: ({ engine, cameraPosition }, [point, scale, chipColor]) => explode(engine, point as Vec3Tuple, scale as number, cameraPosition, chipColor as number | undefined),\n muzzle: ({ engine }, [weapon, tip, direction]) => muzzle(engine, weapon as WeaponName, tip as Vec3Tuple, direction as Vec3Tuple),\n casing: ({ engine }, [port, type]) => casing(engine, port as Vec3Tuple, type as CasingType),\n chips: ({ engine }, [point, direction, count, spread]) => chips(engine, point as Vec3Tuple, direction as Vec3Tuple, count as number, spread as number | undefined),\n spark_at: ({ engine }, [point, direction, count, spread]) => sparkAt(engine, point as Vec3Tuple, direction as Vec3Tuple, count as number, spread as number | undefined),\n rust_at: ({ engine }, [point, count]) => rustAt(engine, point as Vec3Tuple, count as number),\n}\n","import { createSchedule } from '../schedule'\nimport type { ScreenState } from '../types'\nimport type { Placement, WorkshopEngine } from '../workshop/engine'\nimport type { DebrisSpawn, MeshSpawn, SpriteSpawn, Vec3Tuple } from '../workshop/types'\nimport type { Behavior, BehaviorHost } from './behavior'\nimport type { ExprContext } from './expression'\nimport { resolve } from './expression'\nimport { MACROS } from './macros'\nimport type { CueRequest, EffectDef, EffectStep, ScreenKey } from './types'\n\nconst DEFAULT_DIRECTION: Vec3Tuple = [0, 0, -1]\nconst DEFAULT_NORMAL: Vec3Tuple = [0, 1, 0]\nconst SCREEN_FIELDS: Readonly<Record<ScreenKey, keyof ScreenState>> = {\n shake: 'shake', white: 'white', red: 'red', dirL: 'directionLeft', dirR: 'directionRight', chroma: 'chroma', desat: 'desat', gas: 'gas', beat: 'beat',\n}\n\n/** What an effect reads off the view: where it is and which way the camera's own axes point. The Workshop's bench also moves it (`camera` steps). */\nexport interface EffectView {\n readonly position: Vec3Tuple\n forward(local: readonly number[]): Vec3Tuple\n goTo?(eye: Vec3Tuple, target: Vec3Tuple): void\n place?(eye: Vec3Tuple, target: Vec3Tuple): void\n}\n\nexport interface InstanceDeps {\n readonly engine: WorkshopEngine\n readonly view: EffectView\n /** The prop models the recipe can call, by id (see `Behavior`). */\n readonly behavior: (id: string) => Behavior\n /** Every behaviour made so far, to age them every frame. */\n readonly behaviors: () => Iterable<Behavior>\n readonly onCue?: (cue: CueRequest) => void\n}\n\n/** Where and how a played effect sits in the world. */\nexport interface InstanceSetup {\n readonly placement?: Placement\n /** Which way the effect points in the world (`direction` in a recipe). */\n readonly direction?: Vec3Tuple\n /** The surface it hit (`normal` in a recipe). */\n readonly normal?: Vec3Tuple\n /** Named numbers the recipe reads with `variable(name)`; they are in place before its first step. */\n readonly values?: Readonly<Record<string, number>>\n}\n\n/**\n * One run of one effect (W1259-1295): `begin` runs the recipe's `setup` and `timeline`, `step` runs the timers that came due (from the last added to the\n * first) and the `tick`, once per frame. Step by step it draws random numbers where the page did and in the same order.\n */\nexport class EffectInstance {\n readonly variables = new Map<string, unknown>()\n def: EffectDef | undefined\n param = 0\n /** True until the first run of a chosen effect is over: a repeat of the loop is not fresh (`f.fresh`). */\n fresh = false\n time = 0\n /** Seconds since the effect was chosen: unlike `time`, a restart of the loop does not reset it (`el`). */\n elapsed = 0\n setup: InstanceSetup = {}\n private dt = 0\n private index = 0\n private readonly schedule = createSchedule()\n private readonly host: BehaviorHost\n private readonly context: ExprContext\n\n constructor(private readonly deps: InstanceDeps) {\n const instance = (): EffectInstance => this\n this.host = {\n engine: deps.engine,\n camera: deps.view,\n variables: this.variables,\n get param() { return instance().param },\n get time() { return instance().time },\n after: (seconds, action) => this.schedule.at(seconds, action),\n }\n this.context = {\n get random() { return deps.engine.random },\n get param() { return instance().param },\n get time() { return instance().time },\n get dt() { return instance().dt },\n get index() { return instance().index },\n variables: this.variables,\n scaled: (count) => deps.engine.scaled(count),\n query: (behavior, name, args) => deps.behavior(behavior).query(this.host, name, args),\n get quality() { return deps.engine.quality },\n get fresh() { return instance().fresh },\n get direction() { return instance().setup.direction ?? DEFAULT_DIRECTION },\n get normal() { return instance().setup.normal ?? DEFAULT_NORMAL },\n camera: { get position() { return deps.view.position }, forward: (local) => deps.view.forward(local) },\n }\n }\n\n /** The effect starts (`select` + `startLoop`): the recipe's `setup` and `timeline` run now. */\n begin(def: EffectDef, param: number | undefined, setup: InstanceSetup = {}): void {\n this.def = def\n this.param = param ?? def.params?.default ?? 0\n this.elapsed = 0\n this.fresh = true\n this.setup = setup\n this.restart()\n }\n\n /** The loop starts over (`startLoop`): timers, variables and props go back to the start. */\n restart(): void {\n const def = this.def\n if (!def) return\n this.schedule.clear()\n this.variables.clear()\n for (const [name, value] of Object.entries(this.setup.values ?? {})) this.variables.set(name, value)\n for (const behavior of this.deps.behaviors()) behavior.reset(this.host)\n this.time = 0\n this.within(() => {\n this.execute(def.setup ?? [])\n this.execute(def.timeline)\n })\n this.fresh = false\n }\n\n /** The effect's part of one frame: the timers due, then the tick, then the props. The pools are aged by whoever owns the engine. */\n step(dt: number): void {\n const def = this.def\n this.dt = dt\n if (def) {\n this.time += dt\n this.elapsed += dt\n this.within(() => {\n this.schedule.update(this.time)\n this.execute(def.tick ?? [])\n })\n }\n for (const behavior of this.deps.behaviors()) behavior.update(this.host, dt, this.elapsed)\n }\n\n /** True once a looping effect has run its seconds, or a one-shot has nothing left to fire. */\n get finished(): boolean {\n const def = this.def\n if (!def) return true\n if (def.loop && def.loop > 0) return this.time >= def.loop\n return false\n }\n\n /** The recipe's own camera shake on top of the screen's (`panico` trembles the aim). */\n get tremor(): number {\n return (this.variables.get('tremor') as number | undefined) ?? 0\n }\n\n private within(action: () => void): void {\n const engine = this.deps.engine\n const previous = engine.placement\n engine.placement = this.setup.placement\n try {\n action()\n } finally {\n engine.placement = previous\n }\n }\n\n private value(expression: unknown): unknown {\n return resolve(expression, this.context)\n }\n\n execute(steps: readonly EffectStep[]): void {\n for (const step of steps) this.run(step)\n }\n\n private run(step: EffectStep): void {\n const engine = this.deps.engine\n switch (step.op) {\n case 'burst': return engine.burst(step.system, this.value(step.count) as number, this.value(step.spawn) as SpriteSpawn)\n case 'spawn': return engine.spawn(step.system, this.value(step.spawn) as SpriteSpawn)\n case 'emit': return engine.emit(step.key, step.system, this.value(step.rate) as number, this.dt, () => this.value(step.spawn) as SpriteSpawn)\n case 'mesh': { engine.mesh(this.value(step.spawn) as MeshSpawn); return }\n case 'debris': return engine.debrisPiece(this.value(step.spawn) as DebrisSpawn)\n case 'decal': return engine.decal(this.value(step.args) as Parameters<WorkshopEngine['decal']>[0])\n case 'ring': return engine.ring(this.value(step.args) as Parameters<WorkshopEngine['ring']>[0])\n case 'flash': return engine.flash(this.value(step.args) as Parameters<WorkshopEngine['flash']>[0])\n case 'lens': return engine.lensBlob(...(this.value(step.args) as [number, number, number, string, number]))\n case 'screen': return this.changeScreen(step)\n case 'fire_light': return engine.fireLight(step.slot, this.value(step.point) as Vec3Tuple, this.value(step.intensity) as number, this.value(step.time) as number)\n case 'fire_light_off': engine.lights.fire[step.slot]!.intensity = 0; return\n case 'cue': return this.cue(step)\n case 'call': return MACROS[step.macro]!({ engine, cameraPosition: this.deps.view.position, dt: this.dt }, this.value(step.args) as readonly unknown[])\n case 'at': return this.scheduleSteps(step)\n case 'when': return this.execute(this.value(step.if) ? step.steps : (step.otherwise ?? []))\n case 'repeat': return this.repeat(step)\n case 'set': { this.variables.set(step.name, this.value(step.value)); return }\n case 'append': { this.variables.set(step.name, [...((this.variables.get(step.name) as unknown[] | undefined) ?? []), this.value(step.value)]); return }\n case 'camera': return this.moveView(step)\n case 'post': { Object.assign(engine.post, this.value(step.values)); return }\n case 'behavior': return this.deps.behavior(step.id).act(this.host, step.action, this.value(step.args))\n }\n }\n\n private moveView(step: Extract<EffectStep, { op: 'camera' }>): void {\n const eye = this.value(step.eye) as Vec3Tuple\n const target = this.value(step.target) as Vec3Tuple\n if (step.instant) this.deps.view.place?.(eye, target)\n else this.deps.view.goTo?.(eye, target)\n }\n\n private changeScreen(step: Extract<EffectStep, { op: 'screen' }>): void {\n const screen = this.deps.engine.screen\n for (const [key, expression] of Object.entries(step.add ?? {})) screen[SCREEN_FIELDS[key as ScreenKey]] += this.value(expression) as number\n for (const [key, expression] of Object.entries(step.set ?? {})) screen[SCREEN_FIELDS[key as ScreenKey]] = this.value(expression) as number\n for (const [key, expression] of Object.entries(step.max ?? {})) screen[SCREEN_FIELDS[key as ScreenKey]] = Math.max(screen[SCREEN_FIELDS[key as ScreenKey]], this.value(expression) as number)\n }\n\n private cue(step: Extract<EffectStep, { op: 'cue' }>): void {\n const args = this.value(step.args)\n for (let draw = 0; draw < (step.draws ?? 0); draw++) this.deps.engine.random.next()\n this.deps.onCue?.({ cue: step.cue, args, time: this.time })\n }\n\n private scheduleSteps(step: Extract<EffectStep, { op: 'at' }>): void {\n const captured = this.index\n this.schedule.at(this.value(step.time) as number, () => {\n const previous = this.index\n this.index = captured\n this.execute(step.steps)\n this.index = previous\n })\n }\n\n private repeat(step: Extract<EffectStep, { op: 'repeat' }>): void {\n const previous = this.index\n const count = this.value(step.count) as number\n for (let index = 0; index < count; index++) {\n this.index = index\n this.execute(step.steps)\n }\n this.index = previous\n }\n}\n","import { lehmer } from '../random'\nimport type { RandomStream } from '../random'\nimport type { Follow } from '../types'\nimport type { Placement, WorkshopEngine } from '../workshop/engine'\nimport type { Vec3Tuple } from '../workshop/types'\nimport type { Behavior } from './behavior'\nimport { WORKSHOP_BEHAVIORS } from './behaviors'\nimport { EffectInstance } from './instance'\nimport type { EffectView } from './instance'\nimport type { CueRequest, EffectDef } from './types'\n\nconst ORIGIN: Vec3Tuple = [0, 0, 0]\nconst DEFAULT_HEADING: Vec3Tuple = [0, 0, -1]\nconst DEFAULT_MAX_INSTANCES = 48\n/** A recipe with no end of its own (`loop: 0`: a campfire, a fire in a building) plays this long unless it is stopped. */\nconst ENDLESS_SECONDS = 60\n\nexport interface PlayOptions {\n /** Which way the effect points in the world (the muzzle's barrel, the spray of a jet). */\n readonly direction?: Vec3Tuple\n /** Which way the thing it belongs to is turned, as a unit quaternion `[x, y, z, w]`: the effect is placed with this turn, pitch and roll included, in place of the turn about the vertical axis `direction` gives. */\n readonly orientation?: readonly [number, number, number, number]\n /** The surface it hit. */\n readonly normal?: Vec3Tuple\n /** Repeats the same visual: a LAN client draws the same numbers from the same seed. */\n readonly seed?: number\n /** The recipe's parameter (age, material, weapon...). */\n readonly param?: number | string\n /** Named numbers the recipe reads with `variable(name)` (a size, a length of time): set before its `setup` runs, and again whenever its loop starts over. */\n readonly values?: Readonly<Record<string, number>>\n /** Moves with a thing: the effect sits at its position plus the point it was played at. */\n readonly follow?: Follow\n /** Stops after this many seconds (default: the recipe's loop). */\n readonly seconds?: number\n}\n\nexport interface EffectPlayerOptions {\n readonly engine: WorkshopEngine\n readonly effects: Readonly<Record<string, EffectDef>>\n readonly behaviors?: Readonly<Record<string, () => Behavior>>\n /** Where the viewer is, for the recipes that read it. */\n readonly view: EffectView\n /** A stream for a seed (default `lehmer`). */\n readonly streamFor?: (seed: number) => RandomStream\n readonly maxInstances?: number\n readonly onCue?: (cue: CueRequest) => void\n}\n\nexport interface EffectPlayer {\n /** Plays `id` at `point`; `null` when no recipe has that id or the cap of live effects is reached. Returns a handle for `stop`. */\n play(id: string, point: Vec3Tuple, options?: PlayOptions): number | null\n stop(handle: number): void\n /** Runs the timers and ticks of every live effect (the engine's pools are aged by its owner). */\n update(dt: number): void\n clear(): void\n readonly active: number\n has(id: string): boolean\n ids(): readonly string[]\n}\n\ninterface Playing {\n readonly handle: number\n readonly instance: EffectInstance\n readonly random: RandomStream | undefined\n readonly follow: Follow | undefined\n readonly origin: Vec3Tuple\n readonly anchor: Vec3Tuple\n readonly yaw: number\n readonly orientation: readonly [number, number, number, number] | undefined\n readonly seconds: number\n}\n\nconst positionOf = (follow: Follow): Vec3Tuple => {\n const source = 'position' in follow ? follow.position : follow\n return [source.x, source.y, source.z]\n}\n\n/** A parameter given by its option id (`'ak'`, `'concrete'`) as the index of the option; a number is taken as it is. */\nfunction paramIndex(def: EffectDef, param: number | string | undefined): number | undefined {\n if (typeof param !== 'string') return param\n const index = def.params?.options.indexOf(param) ?? -1\n return index >= 0 ? index : undefined\n}\n\n/** The turn about the vertical axis that takes the recipe's `heading` onto `direction` (three.js turns by `x' = cos x + sin z`). */\nfunction yawOf(heading: Vec3Tuple, direction: Vec3Tuple | undefined): number {\n if (!direction || (direction[0] === 0 && direction[2] === 0)) return 0\n return Math.atan2(direction[0], direction[2]) - Math.atan2(heading[0], heading[2])\n}\n\n/**\n * Plays the Workshop's recipes anywhere in the world, many at once, each with its own clock, variables and props, a place and a way it faces. They all\n * spawn into the one engine, so the pools stay shared and bounded. This is what `VfxWorld.play` and the host's `EffectSink` use.\n */\nexport function createEffectPlayer(options: EffectPlayerOptions): EffectPlayer {\n const { engine, view } = options\n const factories = options.behaviors ?? WORKSHOP_BEHAVIORS\n const streamOf = options.streamFor ?? ((seed: number) => lehmer(seed))\n const limit = options.maxInstances ?? DEFAULT_MAX_INSTANCES\n const playing: Playing[] = []\n let next = 0\n\n function makeInstance(): EffectInstance {\n const made = new Map<string, Behavior>()\n return new EffectInstance({\n engine,\n view,\n behavior: (id) => {\n let behavior = made.get(id)\n if (!behavior) {\n const make = factories[id]\n if (!make) throw new Error(`no behaviour ${id}`)\n behavior = make()\n made.set(id, behavior)\n }\n return behavior\n },\n behaviors: () => made.values(),\n ...(options.onCue ? { onCue: options.onCue } : {}),\n })\n }\n\n function placementOf(entry: Playing): Placement {\n const origin = entry.follow ? [entry.origin[0] + positionOf(entry.follow)[0], entry.origin[1] + positionOf(entry.follow)[1], entry.origin[2] + positionOf(entry.follow)[2]] as const : entry.origin\n return { origin, anchor: entry.anchor, yaw: entry.yaw, ...(entry.orientation ? { orientation: entry.orientation } : {}) }\n }\n\n /** Runs `action` drawing from the effect's own stream, if it has one. */\n function drawing(entry: Playing, action: () => void): void {\n const context = engine.context\n const shared = context.random\n if (entry.random) context.random = entry.random\n try {\n action()\n } finally {\n context.random = shared\n }\n }\n\n return {\n play(id, point, playOptions = {}) {\n const def = options.effects[id]\n if (!def || playing.length >= limit) return null\n const heading = def.heading ?? DEFAULT_HEADING\n const instance = makeInstance()\n const entry: Playing = {\n handle: ++next,\n instance,\n random: playOptions.seed !== undefined ? streamOf(playOptions.seed) : undefined,\n follow: playOptions.follow,\n origin: point,\n anchor: def.anchor ?? ORIGIN,\n yaw: yawOf(heading, playOptions.direction),\n orientation: playOptions.orientation,\n seconds: playOptions.seconds ?? (def.loop && def.loop > 0 ? def.loop : ENDLESS_SECONDS),\n }\n playing.push(entry)\n drawing(entry, () =>\n instance.begin(def, paramIndex(def, playOptions.param), {\n placement: placementOf(entry),\n ...(playOptions.direction ? { direction: playOptions.direction } : {}),\n ...(playOptions.normal ? { normal: playOptions.normal } : {}),\n ...(playOptions.values ? { values: playOptions.values } : {}),\n }),\n )\n return entry.handle\n },\n stop(handle) {\n const index = playing.findIndex((entry) => entry.handle === handle)\n if (index >= 0) playing.splice(index, 1)\n },\n update(dt) {\n for (let index = playing.length - 1; index >= 0; index--) {\n const entry = playing[index]!\n const instance = entry.instance\n instance.setup = { ...instance.setup, placement: placementOf(entry) }\n drawing(entry, () => instance.step(dt))\n if (instance.time >= entry.seconds) playing.splice(index, 1)\n }\n },\n clear() {\n playing.length = 0\n },\n get active() {\n return playing.length\n },\n has: (id) => id in options.effects,\n ids: () => Object.keys(options.effects),\n }\n}\n","import type { Rgb } from './color'\nimport { hexToRgb, srgbChannelToLinear } from './color'\n\n/** One of the 4 blast lights (L349-365). */\nexport interface BlastLight {\n x: number\n y: number\n z: number\n distance: number\n peak: number\n life: number\n t: number\n color: Rgb\n /** The level before the flash layer is applied. */\n level: number\n intensity: number\n}\n\n/** One of the 2 fire lights: they go to the two largest fires. */\nexport interface FireLight {\n x: number\n y: number\n z: number\n distance: number\n intensity: number\n}\n\n/** The glow the puff shader reads (`uGlowPos`, `uGlowCol`, `uGlowI`): where the brightest light is, in linear colour. */\nexport interface LightGlow {\n x: number\n y: number\n z: number\n color: [number, number, number]\n intensity: number\n}\n\nexport interface LightPool {\n readonly blast: BlastLight[]\n readonly fire: FireLight[]\n readonly glow: LightGlow\n /** Which blast light the next explosion takes (the Lab's `xlIdx`). */\n cursor: number\n}\n\nexport const BLAST_LIGHTS = 4\nexport const FIRE_LIGHTS = 2\nconst DEFAULT_BLAST_COLOR = 0xffa050\nconst FIRE_GLOW_COLOR: Rgb = [1, 0.42, 0.12]\nconst PEAK_HOLD = 0.05\n\nexport function createLightPool(): LightPool {\n return {\n blast: Array.from({ length: BLAST_LIGHTS }, () => ({ x: 0, y: -50, z: 0, distance: 30, peak: 0, life: 1, t: 9, color: hexToRgb(DEFAULT_BLAST_COLOR), level: 0, intensity: 0 })),\n fire: Array.from({ length: FIRE_LIGHTS }, () => ({ x: 0, y: -50, z: 0, distance: 16, intensity: 0 })),\n glow: { x: 0, y: 0, z: 0, color: [1, 0.5, 0.2], intensity: 0 },\n cursor: 0,\n }\n}\n\nexport function resetLightPool(pool: LightPool): void {\n for (const light of pool.blast) {\n light.t = 9\n light.level = 0\n light.intensity = 0\n }\n pool.cursor = 0\n}\n\nexport interface BlastLightSpec {\n readonly x: number\n readonly y: number\n readonly z: number\n readonly peak: number\n readonly distance: number\n readonly life: number\n readonly color: number\n}\n\n/** `boomLight` (L357-358): the next blast light, round robin. */\nexport function lightBlast(pool: LightPool, spec: BlastLightSpec): void {\n const light = pool.blast[pool.cursor++ % pool.blast.length]!\n light.x = spec.x\n light.y = spec.y\n light.z = spec.z\n light.distance = spec.distance\n light.peak = spec.peak\n light.life = spec.life\n light.t = 0\n light.color = hexToRgb(spec.color)\n}\n\nexport interface FireForLight {\n readonly x: number\n readonly y: number\n readonly z: number\n readonly r: number\n readonly fade: number\n}\n\nexport interface LightStep {\n readonly dt: number\n /** 1 while the flash layer is on, 0 while it is off (the Lab multiplies by it). */\n readonly flashLayer: number\n readonly clock: number\n readonly fires: readonly FireForLight[]\n}\n\nfunction largestFire(fires: readonly FireForLight[], skip: FireForLight | null): FireForLight | null {\n let best: FireForLight | null = null\n for (const fire of fires) {\n if (fire === skip) continue\n if (best === null || fire.r > best.r) best = fire\n }\n return best\n}\n\nfunction updateBlastLights(pool: LightPool, step: LightStep): BlastLight | null {\n let brightest: BlastLight | null = null\n for (const light of pool.blast) {\n light.t += step.dt\n const age = light.t / light.life\n const level = age < 1 ? light.peak * (light.t < PEAK_HOLD ? 1 : Math.pow(1 - age, 2.2)) * (0.85 + 0.15 * Math.sin(light.t * 60)) : 0\n light.level = level\n light.intensity = level * step.flashLayer\n if (level > (brightest?.level ?? 0)) brightest = light\n }\n return brightest\n}\n\n/** `updateLights` (L350-364): blast lights fade by their curve, the two fire lights follow the two largest fires, and the glow follows the brightest light. */\nexport function updateLights(pool: LightPool, step: LightStep): void {\n const brightest = updateBlastLights(pool, step)\n const brightness = brightest?.level ?? 0\n const first = largestFire(step.fires, null)\n const second = largestFire(step.fires, first)\n const largest = [first, second]\n for (let index = 0; index < FIRE_LIGHTS; index++) {\n const fire = largest[index]\n const light = pool.fire[index]!\n if (!fire) {\n light.intensity = 0\n continue\n }\n light.x = fire.x\n light.y = fire.y + Math.max(0.8, fire.r * 0.6)\n light.z = fire.z\n light.intensity = (1.4 + fire.r * 0.6) * (0.75 + 0.25 * Math.sin(step.clock * 17 + index * 3) + 0.1 * Math.sin(step.clock * 41)) * fire.fade\n light.distance = 10 + fire.r * 4\n }\n if (brightest && brightness > 0.05) {\n pool.glow.x = brightest.x\n pool.glow.y = brightest.y\n pool.glow.z = brightest.z\n pool.glow.color[0] = srgbChannelToLinear(brightest.color[0])\n pool.glow.color[1] = srgbChannelToLinear(brightest.color[1])\n pool.glow.color[2] = srgbChannelToLinear(brightest.color[2])\n pool.glow.intensity = brightness * 0.06 * step.flashLayer\n } else if (first) {\n pool.glow.x = first.x\n pool.glow.y = first.y + 0.5\n pool.glow.z = first.z\n pool.glow.color[0] = FIRE_GLOW_COLOR[0]\n pool.glow.color[1] = FIRE_GLOW_COLOR[1]\n pool.glow.color[2] = FIRE_GLOW_COLOR[2]\n pool.glow.intensity = (0.35 + first.r * 0.12) * first.fade\n } else pool.glow.intensity = 0\n}\n","import type { LodBands } from './types'\n\n/** The game's bands: distance minus the visual radius, 15 / 40 / 80 m, scales 1 / .5 / .2 / .1 (G68203, the Workshop's quality bands). The default. */\nexport const LOD_GAME: LodBands = { edges: [15, 40, 80], scales: [1, 0.5, 0.2, 0.1], radiusFactor: 1 }\n\n/** The Lab's own bands (L481): distance minus three visual radii, 20 / 45 / 85 m, scales 1 / .5 / .2 / .08. Used on the bench and in the goldens. */\nexport const LOD_LAB: LodBands = { edges: [20, 45, 85], scales: [1, 0.5, 0.2, 0.08], radiusFactor: 3 }\n\n/** The scale of an effect `distance` metres from the camera, for an effect whose fireball has `visualRadius`. */\nexport function lodScale(bands: LodBands, distance: number, visualRadius: number): number {\n const effective = distance - visualRadius * bands.radiusFactor\n if (effective < bands.edges[0]) return bands.scales[0]\n if (effective < bands.edges[1]) return bands.scales[1]\n if (effective < bands.edges[2]) return bands.scales[2]\n return bands.scales[3]\n}\n","import type { GroundField } from './ground'\nimport type { RandomStream } from './random'\n\n/** What an explosion leaves on the ground, as data: the renderer builds the meshes (L443-476). */\nexport interface ScorchMark {\n readonly kind: 'scorch'\n readonly x: number\n readonly z: number\n readonly radius: number\n /** Which of the three scorch textures. */\n readonly variant: number\n readonly lift: number\n readonly rotation: number\n}\n\nexport interface DirtMark {\n readonly kind: 'dirt'\n readonly x: number\n readonly z: number\n readonly radius: number\n readonly lift: number\n readonly rotation: number\n}\n\n/** A piece of broken asphalt on the rim of a crater. */\nexport interface RockMark {\n readonly kind: 'rock'\n readonly x: number\n readonly y: number\n readonly z: number\n readonly scale: readonly [number, number, number]\n readonly rotation: readonly [number, number, number]\n readonly plate: boolean\n readonly color: number\n}\n\nexport interface BloodMark {\n readonly kind: 'blood'\n readonly x: number\n readonly y: number\n readonly z: number\n readonly scaleX: number\n readonly scaleZ: number\n readonly rotation: number\n}\n\nexport type Mark = ScorchMark | DirtMark | RockMark | BloodMark\n\nexport type MarkEvent = { readonly type: 'add'; readonly mark: Mark } | { readonly type: 'clear' }\n\nexport interface MarkStore {\n readonly entries: Mark[]\n /** Goes up on every mark added or cleared. */\n readonly version: number\n readonly bloodCount: number\n /** The counter that lifts each decal a hair higher than the last (the Lab's `decalY`). */\n lift: number\n add(mark: Mark): void\n clear(): void\n /** Tells a renderer the moment a mark is made (the ground it conforms to is the ground of that moment) or taken away. */\n subscribe(listener: (event: MarkEvent) => void): () => void\n}\n\nconst BLOOD_LIMIT = 450\nconst SCORCH_VARIANTS = 3\nconst RIM_COLORS = [0x2b2c2f, 0x3a3836] as const\n\nexport function createMarkStore(): MarkStore {\n const entries: Mark[] = []\n let version = 0\n let bloodCount = 0\n const listeners = new Set<(event: MarkEvent) => void>()\n return {\n entries,\n get version() {\n return version\n },\n get bloodCount() {\n return bloodCount\n },\n lift: 0,\n add(mark) {\n entries.push(mark)\n if (mark.kind === 'blood') bloodCount++\n version++\n for (const listener of listeners) listener({ type: 'add', mark })\n },\n clear() {\n entries.length = 0\n bloodCount = 0\n this.lift = 0\n version++\n for (const listener of listeners) listener({ type: 'clear' })\n },\n subscribe(listener) {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n }\n}\n\nfunction nextLift(store: MarkStore): number {\n return store.lift++ % 20\n}\n\n/** `scorch` (L458): a burn mark; the texture and the turn are drawn in that order. */\nexport function addScorch(store: MarkStore, random: RandomStream, x: number, z: number, radius: number): void {\n const variant = Math.floor(random.next() * SCORCH_VARIANTS)\n const lift = 0.03 + nextLift(store) * 0.001\n const rotation = random.next() * 6\n store.add({ kind: 'scorch', x, z, radius, variant, lift, rotation })\n}\n\nfunction addRimRocks(store: MarkStore, ground: GroundField | undefined, random: RandomStream, x: number, z: number, radius: number): void {\n const count = Math.round(6 + radius * 7)\n for (let index = 0; index < count; index++) {\n const angle = (index / count) * Math.PI * 2 + random.between(-0.15, 0.15)\n const distance = radius * random.between(0.95, 1.3)\n const size = random.between(0.12, 0.26) * Math.min(1.6, 0.6 + radius * 0.4)\n const px = x + Math.cos(angle) * distance\n const pz = z + Math.sin(angle) * distance\n const plate = random.next() < 0.6\n const color = RIM_COLORS[random.next() < 0.5 ? 0 : 1]\n const tiltX = random.between(-0.5, 0.5)\n const tiltZ = random.between(-0.6, 0.2)\n store.add({\n kind: 'rock',\n x: px,\n y: (ground?.heightAt(px, pz) ?? 0) + size * 0.12,\n z: pz,\n scale: [size * 2, size * 0.35, size * 1.4],\n rotation: [tiltX, angle, tiltZ],\n plate,\n color,\n })\n }\n}\n\n/** `crater` (L460-468): digs the ground (always), then, when marks are on, lays the dirt, and broken asphalt around the rim. */\nexport function addCrater(store: MarkStore, ground: GroundField | undefined, random: RandomStream, spec: { x: number; z: number; radius: number; depth: number; marks: boolean }): void {\n ground?.dent(random, spec.x, spec.z, spec.radius, spec.depth)\n if (!spec.marks) return\n const rotation = random.next() * 6\n store.add({ kind: 'dirt', x: spec.x, z: spec.z, radius: spec.radius * 1.02, lift: 0.045, rotation })\n addRimRocks(store, ground, random, spec.x, spec.z, spec.radius)\n}\n\n/** `bloodSplat` (L471-475). */\nexport function addBloodSplat(store: MarkStore, ground: GroundField | undefined, random: RandomStream, spec: { x: number; z: number; radius: number }): void {\n if (store.bloodCount > BLOOD_LIMIT) return\n const y = (ground?.heightAt(spec.x, spec.z) ?? 0) + 0.03 + nextLift(store) * 0.0008\n const scaleX = spec.radius * random.between(1.6, 2.4)\n const scaleZ = spec.radius * random.between(1.2, 2)\n store.add({ kind: 'blood', x: spec.x, y, z: spec.z, scaleX, scaleZ, rotation: random.next() * 6 })\n}\n","import { OrderedPool } from '../pool'\nimport type { Ring } from './puffs'\n\nexport function createRingPool(capacity: number): OrderedPool<Ring> {\n return new OrderedPool<Ring>(capacity, () => ({ x: 0, z: 0, y0: 0, rise: 0, rk: 0, R0: 0, R1: 0, r: 0, roll: 0, t: 0, life: 0 }))\n}\n\nfunction ageOrKeep(ring: Ring, dt: number): boolean {\n ring.t += dt\n return ring.t <= ring.life + 1\n}\n\n/** `updateRings` (L736): a ring is kept one second past the life of its puffs. */\nexport function updateRings(pool: OrderedPool<Ring>, dt: number): void {\n pool.compact(ageOrKeep, dt)\n}\n\nexport function spawnRing(pool: OrderedPool<Ring>, spec: Omit<Ring, 't'>): Ring {\n const ring = pool.acquire()\n ring.x = spec.x\n ring.z = spec.z\n ring.y0 = spec.y0\n ring.rise = spec.rise\n ring.rk = spec.rk\n ring.R0 = spec.R0\n ring.R1 = spec.R1\n ring.r = spec.r\n ring.roll = spec.roll\n ring.t = 0\n ring.life = spec.life\n return ring\n}\n","import { OrderedPool } from '../pool'\nimport type { Puff } from './puffs'\n\n/** A shock wave: an air shell that grows to the lethal radius in 120 ms, and a dust ring on the ground (L319-348). The last block is what the renderer reads. */\nexport interface Shock {\n x: number\n y: number\n z: number\n Rs: number\n Rg: number\n t: number\n dur: number\n shellScale: number\n shellAlpha: number\n ringScale: number\n ringAlpha: number\n}\n\nconst DEFAULT_DURATION = 0.3\nconst SHELL_GROWTH_SECONDS = 0.12\nconst SHELL_FADE_SECONDS = 0.16\nconst PUSH_BAND = 0.8\nconst PUSH_CEILING = 3\nconst PUSH_AGE_LIMIT = 2\n\nexport function createShockPool(capacity: number): OrderedPool<Shock> {\n return new OrderedPool<Shock>(capacity, () => ({ x: 0, y: 0, z: 0, Rs: 0, Rg: 0, t: 0, dur: DEFAULT_DURATION, shellScale: 0, shellAlpha: 0, ringScale: 0, ringAlpha: 0 }))\n}\n\nexport function spawnShock(pool: OrderedPool<Shock>, spec: { x: number; y: number; z: number; shellRadius: number; groundRadius: number; duration: number }): Shock {\n const shock = pool.acquire()\n shock.x = spec.x\n shock.y = spec.y\n shock.z = spec.z\n shock.Rs = spec.shellRadius\n shock.Rg = spec.groundRadius\n shock.t = 0\n shock.dur = spec.duration || DEFAULT_DURATION\n shock.shellScale = 0\n shock.shellAlpha = 0\n shock.ringScale = 0\n shock.ringAlpha = 0\n return shock\n}\n\nfunction pushPuffs(puffs: readonly Puff[], shock: Shock, reach: number, dt: number, age: number): void {\n for (const puff of puffs) {\n if (puff.ring || puff.t > PUSH_AGE_LIMIT) continue\n const dx = puff.x - shock.x\n const dz = puff.z - shock.z\n const distance = Math.hypot(dx, dz) + 0.001\n if (Math.abs(distance - reach) < PUSH_BAND && puff.y < PUSH_CEILING) {\n const strength = 4 * dt * (1 - age)\n puff.vx += (dx / distance) * strength * 6\n puff.vz += (dz / distance) * strength * 6\n }\n }\n}\n\n/** `updateShocks` (L335-348), walking the list backwards like the Lab: the fronts push the smoke and dust they meet. */\nexport function updateShocks(pool: OrderedPool<Shock>, puffs: readonly Puff[], dt: number): void {\n for (let index = pool.items.length - 1; index >= 0; index--) {\n const shock = pool.items[index]!\n shock.t += dt\n const age = shock.t / shock.dur\n const shellEase = 1 - Math.pow(1 - Math.min(1, shock.t / SHELL_GROWTH_SECONDS), 3)\n shock.shellScale = Math.max(0.01, shock.Rs * shellEase)\n shock.shellAlpha = Math.max(0, 1 - shock.t / SHELL_FADE_SECONDS) * 0.9\n const ringEase = 1 - Math.pow(1 - Math.min(1, age), 2.2)\n shock.ringScale = Math.max(0.01, shock.Rg * ringEase)\n shock.ringAlpha = Math.max(0, 1 - age) * 0.85\n pushPuffs(puffs, shock, shock.Rg * ringEase, dt, age)\n if (age >= 1) pool.removeAt(index)\n }\n}\n","import { hexToLinearRgb, hexToRgb } from './color'\nimport { SMOKE_COLORS } from './constants'\nimport type { VfxCore } from './core'\nimport { lightBlast } from './lights'\nimport { lodScale } from './lod'\nimport { addCrater, addScorch } from './marks'\nimport { spawnRing } from './pools/rings'\nimport { spawnShock } from './pools/shocks'\nimport type { RandomStream, MutableVec3 } from './random'\nimport { addFire } from './emitters/fire'\nimport { addChunk, addGlow, addPuff, addSpark } from './spawn'\nimport type { ExplosionDef, MushroomSpec } from './types'\nimport { addScaled, createVec3, distanceBetween, normalize } from './vector'\n\n/** Where and how one explosion goes off. */\nexport interface ExplosionCall {\n readonly x: number\n readonly y: number\n readonly z: number\n /** Where it is aimed (a rocket's direction, a charge's face). */\n readonly direction?: MutableVec3\n /** The stream the whole effect draws from: the world's in `shared` mode, the event's own in `per_event`. */\n readonly random: RandomStream\n readonly handle: number\n}\n\ninterface Context {\n readonly core: VfxCore\n readonly def: ExplosionDef\n readonly call: ExplosionCall\n readonly random: RandomStream\n readonly x: number\n readonly y: number\n readonly z: number\n /** Fireball radius. */\n readonly size: number\n /** LOD scale of this explosion. */\n readonly scale: number\n readonly direction: MutableVec3 | undefined\n readonly smokeColor: readonly [number, number, number]\n /** Where the camera is (for the screen flash), distance to the blast. */\n readonly cameraDistance: number\n}\n\nconst DEFAULT_LIGHT = 40\nconst DEFAULT_LIGHT_LIFE = 0.45\nconst DEFAULT_LIGHT_COLOR = 0xffa050\nconst DEFAULT_FLASH_INTENSITY = 6\nconst DEFAULT_SHOCK_DURATION = 0.32\nconst DEFAULT_CHUNK_COLOR = 0x47423d\nconst ROCK_COLORS = [0x2b2c2f, 0x4a3a2c] as const\nconst SCRATCH = createVec3()\n\nfunction pick<T>(random: RandomStream, items: readonly T[]): T {\n return items[Math.floor(random.next() * items.length)]!\n}\n\nfunction composeFlash(context: Context): void {\n const { core, def, x, y, z, size } = context\n if (!core.layers.flash) return\n const flash = def.flash\n const light = { x, y: y + size * 0.6, z, peak: (flash.light || DEFAULT_LIGHT) * 0.7, distance: (def.injury || 10) * 2.4, life: flash.life || DEFAULT_LIGHT_LIFE, color: flash.color || DEFAULT_LIGHT_COLOR }\n core.trace?.light?.(light)\n lightBlast(core.lights, light)\n core.spawned.lights++\n const blinding = flash.blinding === true\n addGlow(core, { x, y: y + size * 0.4, z, s: size * 1.4, s1: size * 3.2, i: flash.intensity || DEFAULT_FLASH_INTENSITY, life: 0.22, color: [1, 0.92, 0.75], curve: 2.5 })\n if (!blinding) addGlow(core, { x, y: y + size * 0.5, z, s: size * 2.2, s1: size * 3.6, i: 2.2, life: def.fuel ? 1.4 : 0.6, color: [1, 0.45, 0.15], curve: 1.6 })\n if (core.reducedMotion) return\n const distance = context.cameraDistance\n const screen = core.screen\n if (distance < 15) {\n screen.flash = Math.max(screen.flash, (blinding ? 1 : 0.32) * (1 - distance / 15))\n screen.flashDecay = blinding ? 0.55 : 7\n }\n if (blinding && distance < 40) {\n screen.flash = Math.max(screen.flash, 1)\n screen.flashDecay = 0.5\n }\n}\n\nfunction composeFireballs(context: Context): void {\n const { core, def, random, x, y, z, size, scale, direction } = context\n if (!core.layers.fire || !def.fireballs) return\n const count = Math.round(def.fireballs * Math.max(scale, 0.35))\n const fuel = def.fuel === true\n const heat = def.heat ?? 1\n for (let index = 0; index < count; index++) {\n const d = random.direction(SCRATCH)\n d.y = fuel ? Math.abs(d.y) * 0.5 + 0.05 : Math.abs(d.y) * 0.8 + 0.1\n if (direction) normalize(addScaled(d, direction, def.directionBias ?? 1.2))\n const speed = size * random.between(4.5, 7.5) * (fuel ? 0.75 : 1)\n const s1 = size * random.between(0.42, 0.62)\n const sg = size * random.between(0.12, 0.22)\n const life = random.between(def.smoke.life * 0.7, def.smoke.life)\n const h0 = random.between(1.05, 1.35) * heat\n const hl = fuel ? random.between(0.8, 1.4) * (def.heatLifeScale ?? 1) : random.between(0.22, 0.42)\n addPuff(core, random, {\n x: x + d.x * size * 0.15, y: y + size * 0.25 + d.y * size * 0.1, z: z + d.z * size * 0.15,\n vx: d.x * speed, vy: d.y * speed + (fuel ? size * 1.4 : size * 0.6), vz: d.z * speed,\n s0: size * 0.22, s1, sg, gk: 9, life, h0, hl, drag: fuel ? 4.2 : 6.5, buoy: fuel ? 2.2 : 1.2, color: context.smokeColor, kind: 0,\n })\n }\n}\n\nfunction composeSmoke(context: Context): void {\n const { core, def, random, x, y, z, size, scale } = context\n if (!core.layers.smoke || !def.smoke.count) return\n const count = Math.round(def.smoke.count * Math.max(scale, 0.3))\n for (let index = 0; index < count; index++) {\n const d = random.direction(SCRATCH)\n d.y = Math.abs(d.y)\n const vy = size * random.between(0.6, 2.2)\n const s1 = size * random.between(0.55, 0.85)\n const sg = size * random.between(0.18, 0.3)\n const life = random.between(def.smoke.life * 0.7, def.smoke.life * 1.1)\n addPuff(core, random, {\n x: x + d.x * size * 0.6, y: y + size * 0.4 + d.y * size * 0.5, z: z + d.z * size * 0.6,\n vx: d.x * size * 2.4, vy, vz: d.z * size * 2.4, s0: size * 0.25, s1, sg, gk: 4, life, h0: 0.32, hl: 0.25, drag: 3, buoy: 0.45, color: context.smokeColor, kind: def.smoke.kind ?? 0,\n })\n }\n}\n\nfunction composeShock(context: Context): void {\n const { core, def, x, y, z, size, scale } = context\n if (!core.layers.shock || def.shock === false || !(scale > 0.15)) return\n const duration = def.shock ? def.shock.duration : DEFAULT_SHOCK_DURATION\n const spec = { x, y: y + size * 0.3, z, shellRadius: def.lethal * 0.9, groundRadius: def.injury * 0.6, duration }\n core.trace?.shock?.(spec)\n spawnShock(core.shocks, spec)\n}\n\nfunction composeDust(context: Context): void {\n const { core, def, random, x, z, size, scale } = context\n if (!core.layers.smoke || !def.dust.count || !(scale > 0.15)) return\n const count = Math.round(def.dust.count * scale)\n const color = hexToLinearRgb(def.dust.color)\n const cone = def.cone\n for (let index = 0; index < count; index++) {\n let angle = random.next() * Math.PI * 2\n if (cone) angle = coneAngle(context) + random.between(-cone.half, cone.half)\n const speed = random.between(0.6, 1) * def.injury * (cone ? 1.6 : 0.9)\n const vy = random.between(0.2, 1.2)\n const s1 = size * random.between(0.5, 0.8)\n const sg = size * random.between(0.2, 0.35)\n const life = random.between(2.2, 4)\n addPuff(core, random, {\n x: x + Math.cos(angle) * 0.3, y: 0.15, z: z + Math.sin(angle) * 0.3,\n vx: Math.cos(angle) * speed, vy, vz: Math.sin(angle) * speed, s0: size * 0.2, s1, sg, gk: 5, life, drag: 3.6, buoy: 0.2, color, kind: 1,\n })\n }\n}\n\n/** The angle of the cone's axis: the direction it is aimed at, else the one in its profile. */\nfunction coneAngle(context: Context): number {\n const cone = context.def.cone\n if (!cone) return 0\n return context.direction ? Math.atan2(context.direction.z, context.direction.x) : cone.angle\n}\n\nfunction sparkDirection(context: Context): MutableVec3 {\n const { def, random, direction } = context\n const cone = def.cone\n if (cone) {\n const angle = coneAngle(context) + random.between(-cone.half, cone.half)\n SCRATCH.x = Math.cos(angle)\n SCRATCH.y = random.between(-0.05, 0.25)\n SCRATCH.z = Math.sin(angle)\n return SCRATCH\n }\n const d = random.direction(SCRATCH)\n d.y = Math.abs(d.y) * 0.9 + 0.05\n if (direction) normalize(addScaled(d, direction, def.directionBias ?? 1))\n return d\n}\n\nfunction composeEmbers(context: Context): void {\n const { core, def, random, x, y, z, size, scale } = context\n const embers = def.shrapnel.embers\n if (!embers) return\n for (let index = 0; index < embers * scale; index++) {\n const d = random.direction(SCRATCH)\n d.y = Math.abs(d.y)\n const vx = d.x * random.between(2, 6)\n const vy = random.between(3, 8)\n const vz = d.z * random.between(2, 6)\n addSpark(core, { x, y: y + size * 0.5, z, vx, vy, vz, life: random.between(1.2, 2.6), w: 0.04, st: 0.02, kind: 4, drag: 1.6, g: 2.5 })\n }\n}\n\nfunction composeShrapnel(context: Context): void {\n const { core, def, random, x, y, z, size, scale } = context\n if (!core.layers.shrapnel || !def.shrapnel.count) return\n const count = Math.round(def.shrapnel.count * scale)\n for (let index = 0; index < count; index++) {\n const d = sparkDirection(context)\n const speed = def.shrapnel.speed * random.between(0.45, 1)\n addSpark(core, {\n x: x + d.x * 0.1, y: y + size * 0.25, z: z + d.z * 0.1, vx: d.x * speed, vy: d.y * speed, vz: d.z * speed,\n life: random.between(0.35, 1.1), w: def.shrapnel.width ?? 0.035, st: 0.03, kind: def.shrapnel.kind, drag: def.shrapnel.kind === 1 ? 0.15 : 0.9,\n })\n }\n composeEmbers(context)\n}\n\nfunction composeDebris(context: Context): void {\n const { core, def, random, x, y, z, scale, direction } = context\n const chunks = def.chunks\n if (!core.layers.shrapnel || !chunks.count || scale < 0.5) return\n const count = Math.round(chunks.count * (scale >= 1 ? 1 : 0.5))\n for (let index = 0; index < count; index++) {\n const d = random.direction(SCRATCH)\n d.y = Math.abs(d.y) * 0.9 + 0.2\n if (direction) normalize(addScaled(d, direction, 0.8))\n const speed = random.between(4, 11) * (chunks.speed ?? 1)\n const s = random.between(0.07, 0.2) * (chunks.size ?? 1)\n const plate = chunks.plate ? random.next() < 0.6 : random.next() < 0.15\n const sy = plate ? s * 0.3 : s * random.between(0.6, 1)\n const color = hexToRgb(chunks.colors.length > 0 ? pick(random, chunks.colors) : DEFAULT_CHUNK_COLOR)\n const hot = random.next() < chunks.hot\n addChunk(core, random, {\n x: x + d.x * 0.2, y: y + 0.2, z: z + d.z * 0.2, vx: d.x * speed, vy: d.y * speed + 2, vz: d.z * speed,\n sx: plate ? s * 1.6 : s, sy, sz: plate ? s * 1.1 : s, color, plate, hot,\n })\n }\n}\n\nfunction composeRimDebris(context: Context): void {\n const { core, random, x, z } = context\n const radius = context.def.mark.crater ?? 0\n if (!core.layers.shrapnel) return\n const speedScale = context.def.chunks.speed ?? 1\n const count = Math.round(4 + radius * 4)\n for (let index = 0; index < count; index++) {\n const angle = random.next() * Math.PI * 2\n const speed = random.between(3, 8) * speedScale\n const s = random.between(0.12, 0.28)\n const vy = random.between(4, 10)\n const color = hexToRgb(random.next() < 0.6 ? ROCK_COLORS[0] : ROCK_COLORS[1])\n addChunk(core, random, {\n x: x + Math.cos(angle) * 0.3, y: 0.1, z: z + Math.sin(angle) * 0.3, vx: Math.cos(angle) * speed * 0.6, vy, vz: Math.sin(angle) * speed * 0.6,\n sx: s * 1.6, sy: s * 0.35, sz: s * 1.2, color, plate: true,\n })\n }\n}\n\nfunction composeMushroom(context: Context, spec: MushroomSpec): void {\n const { core, random, x, y, z } = context\n core.trace?.mushroom?.(x, y, z, spec)\n const color = spec.color !== undefined ? hexToLinearRgb(spec.color) : SMOKE_COLORS.black\n const ring = spawnRing(core.rings, {\n x, z, y0: y + spec.height * 0.15, rise: spec.height, rk: spec.riseRate ?? 0.55, R0: spec.radius * 0.3, R1: spec.radius, r: spec.radius * 0.42, roll: spec.roll ?? 2.4, life: spec.life || 7,\n })\n const lod = core.clock.lod\n const count = Math.round((spec.count || 40) * Math.max(lod, 0.4))\n for (let index = 0; index < count; index++) {\n const s1 = spec.radius * random.between(0.35, 0.5)\n const life = random.between(ring.life * 0.75, ring.life)\n const h0 = random.between(1, 1.25)\n const hl = random.between(0.9, 1.6)\n const puff = addPuff(core, random, { x, y, z, s0: spec.radius * 0.2, s1, sg: spec.radius * 0.06, gk: 3, life, h0, hl, color, kind: 0 })\n puff.ring = ring\n puff.ph = random.next() * Math.PI * 2\n puff.th = random.next() * Math.PI * 2\n }\n const stemCount = Math.round((spec.stem || 18) * Math.max(lod, 0.4))\n for (let index = 0; index < stemCount; index++) {\n const spread = random.next() * 0.6\n const px = x + random.between(-0.4, 0.4) * spec.radius * 0.2\n const pz = z + random.between(-0.4, 0.4) * spec.radius * 0.2\n const vx = random.between(-0.3, 0.3)\n const vy = spec.height * random.between(0.6, 1.4) * (1 - spread)\n const vz = random.between(-0.3, 0.3)\n const s1 = spec.radius * random.between(0.22, 0.32)\n const life = random.between(3, 5)\n const h0 = random.between(0.9, 1.2)\n const hl = random.between(0.6, 1.1)\n addPuff(core, random, { x: px, y: y + 0.3, z: pz, vx, vy, vz, s0: spec.radius * 0.12, s1, sg: spec.radius * 0.05, gk: 4, life, h0, hl, drag: 1.2, buoy: 0.5, color })\n }\n}\n\nfunction composeAfterEffects(context: Context): void {\n const { core, def, random, x, y, z } = context\n const mark = def.mark\n if (mark.crater && mark.dent && y < 1.2) {\n core.emit({ type: 'crater', x, z, radius: mark.crater, depth: mark.dent })\n core.trace?.crater?.(x, z, mark.crater, mark.dent)\n addCrater(core.marks, core.ground, random, { x, z, radius: mark.crater, depth: mark.dent, marks: core.layers.mark })\n if (core.layers.shrapnel) composeRimDebris(context)\n }\n if (mark.scorch && y < 1.2 && core.layers.mark) {\n core.trace?.scorch?.(x, z, mark.scorch)\n addScorch(core.marks, random, x, z, mark.scorch)\n }\n if (def.mushroom && core.layers.fire) composeMushroom(context, def.mushroom)\n if (def.residualFires && core.layers.fire) {\n for (const fire of def.residualFires) {\n addFire(core, random, {\n x: x + (fire.dx ?? 0), y: 0, z: z + (fire.dz ?? 0), r: fire.radius, life: fire.life, rate: fire.rate, smoke: fire.smoke, startRadius: fire.startRadius, grow: fire.grow,\n color: fire.smokeColor !== undefined ? hexToLinearRgb(fire.smokeColor) : undefined,\n }, context.call.handle)\n }\n }\n}\n\nfunction emitRequests(context: Context): void {\n const { core, def, x, y, z } = context\n const point = [x, y, z] as const\n core.emit({\n type: 'blast_front', explosion: def.id, point, glassRadius: Math.min(def.injury, 20) * 0.85, speed: 340, lethal: def.lethal, injury: def.injury, push: def.push,\n hotParts: def.hotParts === true, blinding: def.flash.blinding === true, cone: def.cone ? { angle: coneAngle(context), half: def.cone.half, length: def.cone.length } : undefined,\n })\n if (def.ignite) core.emit({ type: 'ignite', point, radius: def.ignite })\n}\n\n/**\n * The Lab's layer compositor (`blast`, L642-714), layer by layer in the Lab's order and drawing from the stream in the Lab's order: flash, fireball,\n * its own smoke, shock wave, dust, shrapnel, debris, then marks, mushroom and lingering fires, and last what the host does (glass, characters).\n */\nexport function composeExplosion(core: VfxCore, def: ExplosionDef, call: ExplosionCall): number {\n const cameraDistance = distanceBetween(core.camera.x, core.camera.y, core.camera.z, call.x, call.y, call.z)\n const forced = core.clock.forcedLod\n const bandScale = forced ?? lodScale(core.bands, cameraDistance, def.visualRadius)\n const capScale = core.areaCap ? core.areaCap.register(core.clock.time, call.x, call.z) : 1\n const scale = forced ?? bandScale * capScale\n core.clock.lod = scale\n core.clock.lastBlast = core.clock.time\n core.spawned.puffs = core.spawned.sparks = core.spawned.chunks = core.spawned.lights = 0\n const context: Context = {\n core, def, call, random: call.random, x: call.x, y: call.y, z: call.z, size: def.visualRadius, scale, direction: call.direction,\n smokeColor: hexToLinearRgb(def.smoke.color), cameraDistance,\n }\n composeFlash(context)\n core.screen.shake = Math.max(core.screen.shake, (def.shake || 1) / (1 + cameraDistance * 0.12))\n composeFireballs(context)\n composeSmoke(context)\n composeShock(context)\n composeDust(context)\n composeShrapnel(context)\n composeDebris(context)\n composeAfterEffects(context)\n emitRequests(context)\n return scale\n}\n","import type { VfxCore } from '../core'\nimport type { Chunk } from './chunks'\nimport { addBloodSplat } from '../marks'\nimport { SMOKE_COLORS } from '../constants'\nimport { addPuff, addSpark } from '../spawn'\n\nconst RESTITUTION = -0.32\nconst GROUND_FRICTION = 0.5\nconst REST_SPEED = 0.6\nconst REST_SLIDE = 0.5\nconst TRAIL_SECONDS = 1.8\nconst TRAIL_STEP = 0.025\nconst QUARTER_TURN = Math.PI / 2\n\nfunction bounce(core: VfxCore, chunk: Chunk, floor: number): void {\n chunk.y = floor\n if (chunk.blood && !chunk.splat && core.layers.mark) {\n chunk.splat = true\n addBloodSplat(core.marks, core.ground, chunk.rng, { x: chunk.x, z: chunk.z, radius: chunk.rng.between(0.06, 0.13) })\n }\n chunk.vy *= RESTITUTION\n chunk.vx *= GROUND_FRICTION\n chunk.vz *= GROUND_FRICTION\n chunk.ax *= GROUND_FRICTION\n chunk.ay *= GROUND_FRICTION\n chunk.az *= GROUND_FRICTION\n if (Math.abs(chunk.vy) < REST_SPEED && Math.hypot(chunk.vx, chunk.vz) < REST_SLIDE) {\n chunk.rest = true\n chunk.rx = Math.round(chunk.rx / QUARTER_TURN) * QUARTER_TURN\n chunk.rz = Math.round(chunk.rz / QUARTER_TURN) * QUARTER_TURN\n }\n}\n\nfunction leaveTrail(core: VfxCore, chunk: Chunk, dt: number): void {\n chunk.trail -= dt\n if (chunk.trail > 0) return\n chunk.trail = TRAIL_STEP\n const random = chunk.rng\n addSpark(core, {\n x: chunk.x, y: chunk.y, z: chunk.z, vx: chunk.vx * 0.15, vy: chunk.vy * 0.15, vz: chunk.vz * 0.15,\n life: random.between(0.25, 0.45), w: 0.05 + chunk.sx * 0.2, st: 0.05, kind: 0, g: 0, drag: 1,\n })\n if (random.next() < 0.25) {\n addPuff(core, random, { x: chunk.x, y: chunk.y, z: chunk.z, s0: 0.04, s1: 0.14 + chunk.sx * 0.3, sg: 0.15, life: random.between(1, 1.6), drag: 2, buoy: 0.6, color: SMOKE_COLORS.grey, kind: 0 })\n }\n}\n\n/** `updateChunks` (L383-398): gravity, spin, a bounce that kills speed, a rest pose on a quarter turn, and the ember trail of a hot piece. */\nexport function updateChunks(core: VfxCore, dt: number): void {\n for (const chunk of core.chunks.items) {\n chunk.t += dt\n if (chunk.rest) continue\n chunk.vy -= core.gravity * dt\n chunk.x += chunk.vx * dt\n chunk.y += chunk.vy * dt\n chunk.z += chunk.vz * dt\n chunk.rx += chunk.ax * dt\n chunk.ry += chunk.ay * dt\n chunk.rz += chunk.az * dt\n const floor = chunk.sy * 0.45 + core.groundAt(chunk.x, chunk.z)\n if (chunk.y < floor) bounce(core, chunk, floor)\n if (chunk.hot && chunk.t < TRAIL_SECONDS && core.layers.smoke) leaveTrail(core, chunk, dt)\n }\n}\n","import type { ScreenState } from './types'\n\nconst DECAY = { shake: 5, white: 7, red: 2.2, side: 3, chroma: 3, desat: 0.7, gasSeconds: 5, beat: 6 } as const\nexport const INITIAL_FLASH_DECAY = 6\n\nexport function createScreenState(): ScreenState {\n return { shake: 0, flash: 0, flashDecay: INITIAL_FLASH_DECAY, white: 0, red: 0, directionLeft: 0, directionRight: 0, chroma: 0, desat: 0, gas: 0, beat: 0 }\n}\n\nexport function resetScreenState(state: ScreenState): void {\n Object.assign(state, createScreenState())\n}\n\n/**\n * The decays of the Lab (L1087, L1092: flash by its own rate, shake by 5 per second) and the Workshop (W354-355: white, red, the two sides, chroma,\n * desaturation, gas and the heartbeat), with the same constants.\n */\nexport function updateScreen(state: ScreenState, dt: number): void {\n state.flash *= Math.exp(-state.flashDecay * dt)\n state.shake *= Math.exp(-DECAY.shake * dt)\n state.white *= Math.exp(-DECAY.white * dt)\n state.red *= Math.exp(-DECAY.red * dt)\n state.directionLeft *= Math.exp(-DECAY.side * dt)\n state.directionRight *= Math.exp(-DECAY.side * dt)\n state.chroma *= Math.exp(-DECAY.chroma * dt)\n state.desat *= Math.exp(-DECAY.desat * dt)\n state.gas = Math.max(0, state.gas - dt / DECAY.gasSeconds)\n state.beat *= Math.exp(-DECAY.beat * dt)\n}\n","import type { SpriteKindDef } from './kinds'\n\nconst clamp = (v: number, lo: number, hi: number): number => (v < lo ? lo : v > hi ? hi : v)\nconst lerp = (a: number, b: number, t: number): number => a + (b - a) * t\n\nfunction rgb(hex: number): readonly [number, number, number] {\n return [((hex >> 16) & 255) / 255, ((hex >> 8) & 255) / 255, (hex & 255) / 255]\n}\n\n/** Per-spawn overrides. */\nexport interface SpriteSpawnOptions {\n /** `0xRRGGBB`: replaces the kind's colour ramp. */\n color?: number\n /** Multiplier on the start size. */\n size?: number\n /** Multiplier on the life. */\n life?: number\n /** Multiplier on the peak alpha. */\n alpha?: number\n /** Non-zero: a `land` sprite (a drip) that reaches the ground calls the owner's `onLand` with this tag. */\n tag?: number\n /** Who hears the landing (see `VfxWorld.onLand`). */\n owner?: string\n /** Height the sprite stops or lands at, instead of the ground's under it (a splash on a table top). */\n floor?: number\n /** Multiplier on the kind's upward acceleration (slow steam). */\n rise?: number\n}\n\nexport interface KindPoolHost {\n groundAt(x: number, z: number): number\n gravity: number\n land(owner: string, x: number, y: number, z: number, color: number, tag: number): void\n}\n\n/** The destruction renderer's old generator (mulberry32), so a seed gives the same sprites. */\nexport function spriteRandom(seed: number): () => number {\n let a = seed >>> 0\n return () => {\n a = (a + 0x6d2b79f5) >>> 0\n let t = a\n t = Math.imul(t ^ (t >>> 15), t | 1)\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n }\n}\n\n/**\n * One fixed-size ring of sprites of the game's kinds, in typed arrays a renderer draws as points: `position`, `color`, `alpha` and `size` are what it\n * reads, `drawCount` how many records from the start. Additive and normal blending are two instances.\n */\nexport class SpriteKindPool {\n live = 0\n /** Records from the start the renderer draws (the highest live index + 1). */\n drawCount = 0\n readonly position: Float32Array\n readonly color: Float32Array\n readonly alpha: Float32Array\n readonly size: Float32Array\n private next = 0\n private high = 0\n private readonly velocity: Float32Array\n private readonly age: Float32Array\n private readonly life: Float32Array\n private readonly size0: Float32Array\n private readonly alive: Uint8Array\n private readonly kind: Uint8Array\n private readonly seed: Float32Array\n private readonly alphaMul: Float32Array\n private readonly tag: Int32Array\n private readonly owner: Int32Array\n private readonly colorHex: Int32Array\n private readonly rgbStart: Float32Array\n private readonly rgbEnd: Float32Array\n private readonly bounced: Uint8Array\n private readonly floor: Float32Array\n private readonly riseMul: Float32Array\n\n constructor(\n readonly capacity: number,\n private readonly defs: readonly SpriteKindDef[],\n private readonly owners: string[],\n ) {\n const n = Math.max(1, capacity)\n this.position = new Float32Array(n * 3)\n this.color = new Float32Array(n * 3)\n this.alpha = new Float32Array(n)\n this.size = new Float32Array(n)\n this.velocity = new Float32Array(n * 3)\n this.age = new Float32Array(n)\n this.life = new Float32Array(n)\n this.size0 = new Float32Array(n)\n this.alive = new Uint8Array(n)\n this.kind = new Uint8Array(n)\n this.seed = new Float32Array(n)\n this.alphaMul = new Float32Array(n)\n this.tag = new Int32Array(n)\n this.owner = new Int32Array(n)\n this.colorHex = new Int32Array(n)\n this.rgbStart = new Float32Array(n * 3)\n this.rgbEnd = new Float32Array(n * 3)\n this.bounced = new Uint8Array(n)\n this.floor = new Float32Array(n)\n this.riseMul = new Float32Array(n)\n }\n\n spawn(kind: number, p: readonly number[], v: readonly number[], random: () => number, options?: SpriteSpawnOptions): void {\n if (this.capacity <= 0) return\n const def = this.defs[kind]!\n const i = this.next\n this.next = (this.next + 1) % this.capacity\n if (this.alive[i]) this.live--\n this.alive[i] = 1\n this.live++\n if (i + 1 > this.high) this.high = i + 1\n this.kind[i] = kind\n this.position.set(p, i * 3)\n this.velocity.set(v, i * 3)\n this.age[i] = 0\n this.life[i] = lerp(def.life[0], def.life[1], random()) * (options?.life ?? 1)\n this.size0[i] = lerp(def.size[0], def.size[1], random()) * (options?.size ?? 1)\n this.seed[i] = random()\n this.alphaMul[i] = options?.alpha ?? 1\n this.tag[i] = options?.tag ?? 0\n this.owner[i] = options?.owner === undefined ? -1 : this.ownerIndex(options.owner)\n this.colorHex[i] = options?.color ?? -1\n this.rgbStart.set(rgb(options?.color ?? def.c0), i * 3)\n this.rgbEnd.set(rgb(options?.color ?? def.c1), i * 3)\n this.bounced[i] = 0\n this.floor[i] = options?.floor ?? Number.NaN\n this.riseMul[i] = options?.rise ?? 1\n this.size[i] = this.size0[i]!\n this.alpha[i] = 0\n }\n\n private ownerIndex(name: string): number {\n let index = this.owners.indexOf(name)\n if (index < 0) index = this.owners.push(name) - 1\n return index\n }\n\n private kill(i: number): void {\n if (!this.alive[i]) return\n this.alive[i] = 0\n this.live--\n this.alpha[i] = 0\n this.size[i] = 0\n }\n\n clear(): void {\n this.alive.fill(0)\n this.alpha.fill(0)\n this.size.fill(0)\n this.live = 0\n this.high = 0\n this.next = 0\n this.drawCount = 0\n }\n\n /** Shoves the live sprites within `radius` of a point away from it (a blast). */\n push(cx: number, cy: number, cz: number, radius: number, speed: number): void {\n for (let i = 0; i < this.high; i++) {\n if (!this.alive[i]) continue\n const dx = this.position[i * 3]! - cx\n const dy = this.position[i * 3 + 1]! - cy\n const dz = this.position[i * 3 + 2]! - cz\n const d = Math.hypot(dx, dy, dz)\n if (d > radius || d < 1e-4) continue\n const f = (1 - d / radius) * speed\n this.velocity[i * 3]! += (dx / d) * f\n this.velocity[i * 3 + 1]! += (dy / d) * f\n this.velocity[i * 3 + 2]! += (dz / d) * f\n }\n }\n\n update(dt: number, host: KindPoolHost): void {\n let top = 0\n for (let i = 0; i < this.high; i++) {\n if (!this.alive[i]) continue\n const def = this.defs[this.kind[i]!]!\n const age = (this.age[i]! + dt)\n this.age[i] = age\n const life = this.life[i]!\n if (age >= life) {\n this.kill(i)\n continue\n }\n const t = age / life\n const i3 = i * 3\n let vx = this.velocity[i3]!\n let vy = this.velocity[i3 + 1]!\n let vz = this.velocity[i3 + 2]!\n vy += (def.rise * this.riseMul[i]! - def.gravity * host.gravity) * dt\n if (def.flutter) {\n const phase = age * 7 + this.seed[i]! * 40\n vx += Math.sin(phase) * def.flutter * dt\n vz += Math.cos(phase * 1.3) * def.flutter * dt\n }\n if (def.drag > 0) {\n const k = Math.exp(-def.drag * dt)\n vx *= k\n vy *= k\n vz *= k\n }\n const x = this.position[i3]! + vx * dt\n let y = this.position[i3 + 1]! + vy * dt\n const z = this.position[i3 + 2]! + vz * dt\n if (def.ground !== 'none') {\n const own = this.floor[i]!\n const gy = Number.isNaN(own) ? host.groundAt(x, z) + 0.005 : own\n if (y < gy) {\n if (def.ground === 'land') {\n const tag = this.tag[i]!\n const owner = this.owner[i]!\n if (tag !== 0 && owner >= 0) host.land(this.owners[owner]!, x, gy, z, this.colorHex[i]! >= 0 ? this.colorHex[i]! : def.c0, tag)\n this.kill(i)\n continue\n }\n y = gy\n if (def.ground === 'bounce' && !this.bounced[i]) {\n this.bounced[i] = 1\n vy = Math.abs(vy) * 0.35\n vx *= 0.6\n vz *= 0.6\n } else {\n vx = vy = vz = 0\n }\n }\n }\n this.velocity[i3] = vx\n this.velocity[i3 + 1] = vy\n this.velocity[i3 + 2] = vz\n this.position[i3] = x\n this.position[i3 + 1] = y\n this.position[i3 + 2] = z\n this.size[i] = this.size0[i]! * lerp(1, def.grow, t)\n this.color[i3] = lerp(this.rgbStart[i3]!, this.rgbEnd[i3]!, t)\n this.color[i3 + 1] = lerp(this.rgbStart[i3 + 1]!, this.rgbEnd[i3 + 1]!, t)\n this.color[i3 + 2] = lerp(this.rgbStart[i3 + 2]!, this.rgbEnd[i3 + 2]!, t)\n let envelope = t < def.fadeIn ? t / def.fadeIn : 1 - (t - def.fadeIn) / (1 - def.fadeIn)\n if (def.flicker) envelope *= 1 - def.flicker * (0.5 + 0.5 * Math.sin(age * 38 + this.seed[i]! * 50))\n this.alpha[i] = clamp(envelope, 0, 1) * def.alpha * this.alphaMul[i]!\n top = i + 1\n }\n this.high = top\n this.drawCount = top\n }\n}\n\nexport { rgb as hexToUnitRgb }\n","{\n \"fire\": {\n \"id\": \"fire\",\n \"additive\": true,\n \"life\": [\n 0.45,\n 0.9\n ],\n \"size\": [\n 0.1,\n 0.2\n ],\n \"grow\": 0.3,\n \"gravity\": 0,\n \"rise\": 2.4,\n \"drag\": 1.2,\n \"c0\": 16760896,\n \"c1\": 12591104,\n \"alpha\": 0.9,\n \"fadeIn\": 0.12,\n \"cone\": 0.7,\n \"ground\": \"none\",\n \"flicker\": 0.35\n },\n \"smoke\": {\n \"id\": \"smoke\",\n \"additive\": false,\n \"life\": [\n 1.8,\n 3.2\n ],\n \"size\": [\n 0.14,\n 0.24\n ],\n \"grow\": 3.2,\n \"gravity\": 0,\n \"rise\": 0.7,\n \"drag\": 0.8,\n \"c0\": 6710886,\n \"c1\": 10132122,\n \"alpha\": 0.35,\n \"fadeIn\": 0.15,\n \"cone\": 0.6,\n \"ground\": \"none\"\n },\n \"sparks\": {\n \"id\": \"sparks\",\n \"additive\": true,\n \"life\": [\n 0.25,\n 0.6\n ],\n \"size\": [\n 0.018,\n 0.03\n ],\n \"grow\": 0.5,\n \"gravity\": 1,\n \"rise\": 0,\n \"drag\": 0.3,\n \"c0\": 16773792,\n \"c1\": 16738832,\n \"alpha\": 1,\n \"fadeIn\": 0.02,\n \"cone\": 0.55,\n \"ground\": \"bounce\"\n },\n \"dust\": {\n \"id\": \"dust\",\n \"additive\": false,\n \"life\": [\n 1,\n 2\n ],\n \"size\": [\n 0.08,\n 0.14\n ],\n \"grow\": 2.5,\n \"gravity\": 0.05,\n \"rise\": 0,\n \"drag\": 2.5,\n \"c0\": 9274745,\n \"c1\": 9274745,\n \"alpha\": 0.4,\n \"fadeIn\": 0.1,\n \"cone\": 0.4,\n \"ground\": \"none\"\n },\n \"shards\": {\n \"id\": \"shards\",\n \"additive\": true,\n \"life\": [\n 0.8,\n 1.4\n ],\n \"size\": [\n 0.02,\n 0.035\n ],\n \"grow\": 0.8,\n \"gravity\": 1,\n \"rise\": 0,\n \"drag\": 0.2,\n \"c0\": 14677247,\n \"c1\": 10473727,\n \"alpha\": 0.9,\n \"fadeIn\": 0.03,\n \"cone\": 0.5,\n \"ground\": \"bounce\",\n \"flicker\": 0.5\n },\n \"splinters\": {\n \"id\": \"splinters\",\n \"additive\": false,\n \"life\": [\n 0.8,\n 1.5\n ],\n \"size\": [\n 0.02,\n 0.035\n ],\n \"grow\": 1,\n \"gravity\": 1,\n \"rise\": 0,\n \"drag\": 0.3,\n \"c0\": 10516042,\n \"c1\": 8017200,\n \"alpha\": 1,\n \"fadeIn\": 0.02,\n \"cone\": 0.5,\n \"ground\": \"bounce\"\n },\n \"chips\": {\n \"id\": \"chips\",\n \"additive\": false,\n \"life\": [\n 0.8,\n 1.5\n ],\n \"size\": [\n 0.025,\n 0.04\n ],\n \"grow\": 1,\n \"gravity\": 1,\n \"rise\": 0,\n \"drag\": 0.3,\n \"c0\": 9077624,\n \"c1\": 7235679,\n \"alpha\": 1,\n \"fadeIn\": 0.02,\n \"cone\": 0.5,\n \"ground\": \"bounce\"\n },\n \"fibers\": {\n \"id\": \"fibers\",\n \"additive\": false,\n \"life\": [\n 1.5,\n 2.5\n ],\n \"size\": [\n 0.02,\n 0.03\n ],\n \"grow\": 1,\n \"gravity\": 0.15,\n \"rise\": 0,\n \"drag\": 2,\n \"c0\": 12101768,\n \"c1\": 12101768,\n \"alpha\": 0.9,\n \"fadeIn\": 0.05,\n \"cone\": 0.4,\n \"ground\": \"stop\",\n \"flutter\": 2.5\n },\n \"leaves\": {\n \"id\": \"leaves\",\n \"additive\": false,\n \"life\": [\n 1.5,\n 2.8\n ],\n \"size\": [\n 0.03,\n 0.05\n ],\n \"grow\": 1,\n \"gravity\": 0.15,\n \"rise\": 0,\n \"drag\": 2,\n \"c0\": 6003260,\n \"c1\": 4882224,\n \"alpha\": 0.95,\n \"fadeIn\": 0.05,\n \"cone\": 0.4,\n \"ground\": \"stop\",\n \"flutter\": 2.8\n },\n \"drips\": {\n \"id\": \"drips\",\n \"additive\": false,\n \"life\": [\n 2,\n 3\n ],\n \"size\": [\n 0.025,\n 0.035\n ],\n \"grow\": 1,\n \"gravity\": 1,\n \"rise\": 0,\n \"drag\": 0,\n \"c0\": 6974058,\n \"c1\": 6974058,\n \"alpha\": 1,\n \"fadeIn\": 0.01,\n \"cone\": 0.2,\n \"ground\": \"land\"\n },\n \"ash\": {\n \"id\": \"ash\",\n \"additive\": false,\n \"life\": [\n 2,\n 4\n ],\n \"size\": [\n 0.02,\n 0.04\n ],\n \"grow\": 1.2,\n \"gravity\": 0.08,\n \"rise\": 0,\n \"drag\": 1.5,\n \"c0\": 2762275,\n \"c1\": 1710102,\n \"alpha\": 0.8,\n \"fadeIn\": 0.1,\n \"cone\": 0.5,\n \"ground\": \"stop\",\n \"flutter\": 0.6\n },\n \"steam\": {\n \"id\": \"steam\",\n \"additive\": false,\n \"life\": [\n 1,\n 2\n ],\n \"size\": [\n 0.12,\n 0.2\n ],\n \"grow\": 3,\n \"gravity\": 0,\n \"rise\": 0.9,\n \"drag\": 1,\n \"c0\": 16777215,\n \"c1\": 14739694,\n \"alpha\": 0.35,\n \"fadeIn\": 0.15,\n \"cone\": 0.6,\n \"ground\": \"none\"\n },\n \"flash\": {\n \"id\": \"flash\",\n \"additive\": true,\n \"life\": [\n 0.12,\n 0.2\n ],\n \"size\": [\n 1.2,\n 1.8\n ],\n \"grow\": 1.6,\n \"gravity\": 0,\n \"rise\": 0,\n \"drag\": 0,\n \"c0\": 16774080,\n \"c1\": 16748592,\n \"alpha\": 1,\n \"fadeIn\": 0.1,\n \"cone\": 0,\n \"ground\": \"none\"\n },\n \"foam\": {\n \"id\": \"foam\",\n \"additive\": false,\n \"life\": [\n 1,\n 1.8\n ],\n \"size\": [\n 0.06,\n 0.1\n ],\n \"grow\": 1.5,\n \"gravity\": 0.6,\n \"rise\": 0,\n \"drag\": 1.5,\n \"c0\": 16054527,\n \"c1\": 14542576,\n \"alpha\": 0.8,\n \"fadeIn\": 0.05,\n \"cone\": 0.5,\n \"ground\": \"stop\"\n }\n}\n","import kindData from '../data/sprite-kinds.json'\n\n/** The kinds of sprite the game's pools make: debris, sparks, smoke, steam, drips... (the destruction renderer's old `ParticleKind`). */\nexport type SpriteKind =\n | 'fire' | 'smoke' | 'sparks' | 'dust' | 'shards' | 'splinters' | 'chips' | 'fibers' | 'leaves' | 'drips' | 'ash' | 'steam' | 'flash' | 'foam'\n\n/** What happens to a sprite that reaches the ground. */\nexport type SpriteGround = 'none' | 'bounce' | 'stop' | 'land'\n\nexport interface SpriteKindDef {\n readonly id: SpriteKind\n readonly additive: boolean\n /** Life range in seconds. */\n readonly life: readonly [number, number]\n /** Start size range in metres (billboard diameter). */\n readonly size: readonly [number, number]\n /** Size multiplier reached at the end of the life. */\n readonly grow: number\n /** Gravity multiplier (1 = full gravity). */\n readonly gravity: number\n /** Constant upward acceleration in m/s² (heat, steam). */\n readonly rise: number\n /** Velocity damping per second. */\n readonly drag: number\n /** `0xRRGGBB` at the start and at the end of the life. */\n readonly c0: number\n readonly c1: number\n readonly alpha: number\n /** Fraction of the life spent fading in. */\n readonly fadeIn: number\n /** How much of the emission direction follows the surface normal (1) versus a random direction (0). */\n readonly cone: number\n readonly ground: SpriteGround\n readonly flutter?: number\n readonly flicker?: number\n}\n\nexport const SPRITE_KINDS: Readonly<Record<SpriteKind, SpriteKindDef>> = kindData as unknown as Record<SpriteKind, SpriteKindDef>\n\n/** A copy of the kinds with some changed (a partial replaces the fields it names), added or removed (`null`); a game tunes its sprites without touching the core. */\nexport function patchSpriteKinds(\n base: Readonly<Record<string, SpriteKindDef>>,\n changes: Readonly<Record<string, Partial<SpriteKindDef> | SpriteKindDef | null>>,\n): Record<string, SpriteKindDef> {\n const result: Record<string, SpriteKindDef> = { ...base }\n for (const [id, change] of Object.entries(changes)) {\n if (change === null) delete result[id]\n else result[id] = { ...(base[id] ?? {}), ...change, id } as SpriteKindDef\n }\n return result\n}\n","import { SpriteKindPool, spriteRandom } from './kind-pool'\nimport type { SpriteSpawnOptions } from './kind-pool'\nimport { SPRITE_KINDS } from './kinds'\nimport type { SpriteKind, SpriteKindDef } from './kinds'\n\ntype Vec3 = readonly [number, number, number]\n\nexport interface SpriteKindSystemOptions {\n /** Most live sprites at once (additive takes 40 %, normal the rest). */\n readonly capacity: number\n /** Count multiplier for bursts (0.6 on low quality). */\n readonly density: number\n readonly seed: number\n readonly groundAt: (x: number, z: number) => number\n readonly gravity: number\n readonly kinds?: Readonly<Record<string, SpriteKindDef>>\n}\n\nexport type LandListener = (x: number, y: number, z: number, color: number, tag: number) => void\n\n/**\n * The game's sprites of fixed kinds (debris, sparks, smoke, steam, drips...), burst or spawned one by one: two pooled rings, additive and normal, that\n * a renderer draws as points. This is the destruction renderer's old particle system with the maths unchanged and the drawing taken out.\n */\nexport class SpriteKindSystem {\n readonly additive: SpriteKindPool\n readonly normal: SpriteKindPool\n private readonly random: () => number\n private readonly ids: readonly string[]\n private readonly defs: readonly SpriteKindDef[]\n private readonly index = new Map<string, number>()\n private readonly owners: string[] = []\n private readonly listeners = new Map<string, LandListener>()\n\n constructor(private readonly options: SpriteKindSystemOptions) {\n const kinds: Readonly<Record<string, SpriteKindDef>> = options.kinds ?? SPRITE_KINDS\n this.ids = Object.keys(kinds)\n this.defs = this.ids.map((id) => kinds[id]!)\n this.ids.forEach((id, at) => this.index.set(id, at))\n const additiveCapacity = Math.floor(options.capacity * 0.4)\n this.additive = new SpriteKindPool(additiveCapacity, this.defs, this.owners)\n this.normal = new SpriteKindPool(options.capacity - additiveCapacity, this.defs, this.owners)\n this.random = spriteRandom(options.seed)\n }\n\n get capacity(): number {\n return this.additive.capacity + this.normal.capacity\n }\n\n get live(): number {\n return this.additive.live + this.normal.live\n }\n\n /** Hears the landing of the drips of `owner` (the ones spawned with `{ owner, tag }`). Returns the function that stops listening. */\n onLand(owner: string, listener: LandListener): () => void {\n this.listeners.set(owner, listener)\n return () => {\n if (this.listeners.get(owner) === listener) this.listeners.delete(owner)\n }\n }\n\n private kindOf(kind: string): { index: number; def: SpriteKindDef } {\n const index = this.index.get(kind)\n if (index === undefined) throw new Error(`unknown sprite kind ${kind}`)\n return { index, def: this.defs[index]! }\n }\n\n /** One sprite with an explicit velocity. */\n spawn(kind: SpriteKind | string, point: Vec3, velocity: Vec3, options?: SpriteSpawnOptions): void {\n const { index, def } = this.kindOf(kind)\n ;(def.additive ? this.additive : this.normal).spawn(index, point, velocity, this.random, options)\n }\n\n /** `count` sprites leaving `point` around `normal` at about `speed` m/s. */\n burst(kind: SpriteKind | string, point: Vec3, normal: Vec3, count: number, speed = 1, options?: SpriteSpawnOptions): void {\n const { def } = this.kindOf(kind)\n const n = Math.max(0, Math.round(count * this.options.density))\n const rnd = this.random\n const length = Math.hypot(normal[0], normal[1], normal[2]) || 1\n const nx = normal[0] / length\n const ny = normal[1] / length\n const nz = normal[2] / length\n for (let k = 0; k < n; k++) {\n const u = rnd() * 2 - 1\n const phase = rnd() * Math.PI * 2\n const s = Math.sqrt(1 - u * u)\n const rx = s * Math.cos(phase)\n const ry = u\n const rz = s * Math.sin(phase)\n let dx = nx * def.cone + rx * (1 - def.cone)\n let dy = ny * def.cone + ry * (1 - def.cone)\n let dz = nz * def.cone + rz * (1 - def.cone)\n // a little scatter even for a pure cone, and never push into the surface for debris\n dx += rx * 0.35\n dy += ry * 0.35\n dz += rz * 0.35\n const l = Math.hypot(dx, dy, dz) || 1\n const sp = speed * (0.35 + 0.65 * rnd())\n const jitter = 0.01 + (def.size[1] > 0.5 ? 0.15 : 0.02)\n this.spawn(\n def.id,\n [point[0] + (rnd() - 0.5) * jitter, point[1] + (rnd() - 0.5) * jitter, point[2] + (rnd() - 0.5) * jitter],\n [(dx / l) * sp, (dy / l) * sp, (dz / l) * sp],\n options,\n )\n }\n }\n\n /** Blast wave: shoves the live sprites away from a point. */\n push(point: Vec3, radius: number, speed: number): void {\n this.additive.push(point[0], point[1], point[2], radius, speed)\n this.normal.push(point[0], point[1], point[2], radius, speed)\n }\n\n update(dt: number): void {\n const host = {\n groundAt: this.options.groundAt,\n gravity: this.options.gravity,\n land: (owner: string, x: number, y: number, z: number, color: number, tag: number) => this.listeners.get(owner)?.(x, y, z, color, tag),\n }\n if (this.additive.live > 0 || this.additive.drawCount > 0) this.additive.update(dt, host)\n if (this.normal.live > 0 || this.normal.drawCount > 0) this.normal.update(dt, host)\n }\n\n clear(): void {\n this.additive.clear()\n this.normal.clear()\n }\n}\n","import type { RandomStream, MutableVec3 } from '../random'\nimport { addScaled, normalize } from '../vector'\nimport type { SpriteRange, Vec3Tuple } from './types'\n\nconst TWO_PI_ROUNDED = 6.2832\nconst WIDE_CONE = 3.1\nconst UP: Vec3Tuple = [0, 1, 0]\n\nexport function drawRange(random: RandomStream, range: SpriteRange | undefined): number {\n if (Array.isArray(range)) return random.between(range[0], range[1])\n return (range as number | undefined) || 0\n}\n\n/** `randUnit` (W166): the page rounds a turn to 6.2832, so this is not `RandomStream.direction`. */\nexport function randomUnit(random: RandomStream, out: MutableVec3): MutableVec3 {\n const up = random.next() * 2 - 1\n const angle = random.next() * TWO_PI_ROUNDED\n const ring = Math.sqrt(1 - up * up)\n out.x = ring * Math.cos(angle)\n out.y = up\n out.z = ring * Math.sin(angle)\n return out\n}\n\nconst tangentA: MutableVec3 = { x: 0, y: 0, z: 0 }\nconst tangentB: MutableVec3 = { x: 0, y: 0, z: 0 }\nconst axis: MutableVec3 = { x: 0, y: 0, z: 0 }\n\nfunction cross(out: MutableVec3, a: MutableVec3, b: MutableVec3): MutableVec3 {\n const x = a.y * b.z - a.z * b.y\n const y = a.z * b.x - a.x * b.z\n const z = a.x * b.y - a.y * b.x\n out.x = x\n out.y = y\n out.z = z\n return out\n}\n\n/** `coneDir` (W169-171): a direction inside `cone` radians of `direction`, two draws (a full sphere when the cone is wide). */\nexport function coneDirection(random: RandomStream, direction: Vec3Tuple | undefined, cone: number, out: MutableVec3): MutableVec3 {\n if (cone >= WIDE_CONE) return randomUnit(random, out)\n const [dx, dy, dz] = direction ?? UP\n const cosTilt = 1 - random.next() * (1 - Math.cos(cone))\n const sinTilt = Math.sqrt(1 - cosTilt * cosTilt)\n const phase = random.next() * TWO_PI_ROUNDED\n axis.x = dx\n axis.y = dy\n axis.z = dz\n tangentA.x = 1\n tangentA.y = 0\n tangentA.z = 0\n if (Math.abs(dx) > 0.9) {\n tangentA.x = 0\n tangentA.y = 1\n }\n normalize(cross(tangentB, axis, tangentA))\n normalize(cross(tangentA, tangentB, axis))\n out.x = dx * cosTilt\n out.y = dy * cosTilt\n out.z = dz * cosTilt\n addScaled(out, tangentA, sinTilt * Math.cos(phase))\n addScaled(out, tangentB, sinTilt * Math.sin(phase))\n return out\n}\n","import type { MutableVec3 } from '../random'\nimport { coneDirection, drawRange } from './direction'\nimport type { DebrisSpawn, WorkshopContext } from './types'\n\nconst CAPACITY = 500\nconst GRAVITY = 9.8\nconst QUARTER_TURN = 1.5708\nconst FADE_SECONDS = 0.5\nconst DEFAULT_SIZE = [0.05, 0.05, 0.05] as const\nconst DEFAULT_SPIN = 12\nconst DEFAULT_COLOR = 0x888888\nconst DEFAULT_LIFE = 8\nconst DEFAULT_BOUNCE = 0.3\nconst FULL_TURN_DRAWN = 6\n\nexport interface DebrisPiece {\n on: boolean\n x: number\n y: number\n z: number\n vx: number\n vy: number\n vz: number\n sx: number\n sy: number\n sz: number\n rx: number\n ry: number\n rz: number\n ax: number\n ay: number\n az: number\n life: number\n bounce: number\n rest: boolean\n color: number\n}\n\nfunction createPiece(): DebrisPiece {\n return { on: false, x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, sx: 0, sy: 0, sz: 0, rx: 0, ry: 0, rz: 0, ax: 0, ay: 0, az: 0, life: 0, bounce: 0, rest: false, color: DEFAULT_COLOR }\n}\n\n/** The Workshop's physics debris (W248-263): 500 boxes in a ring buffer, the newest overwriting the oldest, that tumble, bounce, settle on a face and shrink away. */\nexport class DebrisPool {\n readonly pieces: DebrisPiece[] = Array.from({ length: CAPACITY }, createPiece)\n count = 0\n private next = 0\n private readonly direction: MutableVec3 = { x: 0, y: 0, z: 0 }\n\n constructor(private readonly context: WorkshopContext) {}\n\n spawn(options: DebrisSpawn): void {\n const random = this.context.random\n const piece = this.pieces[this.next++ % CAPACITY]!\n coneDirection(random, options.dir, options.cone ?? 3.2, this.direction)\n const speed = drawRange(random, options.sp)\n const size = options.size ?? DEFAULT_SIZE\n const scale = options.k ? drawRange(random, options.k) : 1\n const box = options.box\n const spin = options.spin || DEFAULT_SPIN\n piece.on = true\n piece.x = options.p[0] + (box ? random.between(-box[0], box[0]) : 0)\n piece.y = options.p[1] + (box ? random.between(-box[1], box[1]) : 0)\n piece.z = options.p[2] + (box ? random.between(-box[2], box[2]) : 0)\n piece.vx = this.direction.x * speed + (options.v ? options.v[0] : 0)\n piece.vy = this.direction.y * speed + (options.v ? options.v[1] : 0)\n piece.vz = this.direction.z * speed + (options.v ? options.v[2] : 0)\n piece.sx = (size[0] as number) * scale\n piece.sy = (size[1] as number) * scale\n piece.sz = (size[2] as number) * scale\n piece.rx = random.between(0, FULL_TURN_DRAWN)\n piece.ry = random.between(0, FULL_TURN_DRAWN)\n piece.rz = random.between(0, FULL_TURN_DRAWN)\n piece.ax = random.between(-1, 1) * spin\n piece.ay = random.between(-1, 1) * spin\n piece.az = random.between(-1, 1) * spin\n piece.life = options.life || DEFAULT_LIFE\n piece.bounce = options.bounce ?? DEFAULT_BOUNCE\n piece.rest = false\n piece.color = options.c ?? DEFAULT_COLOR\n }\n\n clear(): void {\n for (const piece of this.pieces) piece.on = false\n this.count = 0\n }\n\n update(dt: number): void {\n this.count = 0\n for (const piece of this.pieces) {\n if (!piece.on) continue\n this.count++\n piece.life -= dt\n if (piece.life <= 0) {\n piece.on = false\n continue\n }\n if (!piece.rest) this.move(piece, dt)\n }\n }\n\n /** Scale at the end of its life, 1 until the last half second (the renderer's `sh`). */\n static fade(piece: DebrisPiece): number {\n return Math.min(1, piece.life / FADE_SECONDS)\n }\n\n private move(piece: DebrisPiece, dt: number): void {\n piece.vy -= GRAVITY * dt\n piece.x += piece.vx * dt\n piece.y += piece.vy * dt\n piece.z += piece.vz * dt\n piece.rx += piece.ax * dt\n piece.ry += piece.ay * dt\n piece.rz += piece.az * dt\n const groundHeight = this.context.groundAt(piece.x, piece.z) + Math.min(piece.sx, piece.sy, piece.sz) * 0.5\n if (piece.y >= groundHeight) return\n piece.y = groundHeight\n piece.vy *= -piece.bounce\n piece.vx *= 0.55\n piece.vz *= 0.55\n piece.ax *= 0.5\n piece.ay *= 0.5\n piece.az *= 0.5\n if (Math.abs(piece.vy) < 0.5 && Math.hypot(piece.vx, piece.vz) < 0.3) {\n piece.rest = true\n piece.rx = Math.round(piece.rx / QUARTER_TURN) * QUARTER_TURN\n piece.rz = Math.round(piece.rz / QUARTER_TURN) * QUARTER_TURN\n }\n }\n}\n","import type { DecalOptions, Vec3Tuple, WorkshopContext } from './types'\n\nconst CAPACITY = 400\nconst UP: Vec3Tuple = [0, 1, 0]\nconst DEFAULT_LIFE = 60\nconst DEFAULT_GROW = 1.2\n\n/** The seconds a fluid decal waits and then takes to evaporate away (W276); blood, bile and oil only darken. */\nconst EVAPORATION = { fuel: [6, 5], water: [40, 20] } as const\n\nexport interface Decal {\n on: boolean\n x: number\n y: number\n z: number\n sx: number\n sy: number\n t: number\n life: number\n grow: number\n color: number\n /** Where the surface faces, and the colour a fluid dries to (`-1`: it does not dry). */\n normal: Vec3Tuple\n fluid: DecalOptions['fluid'] | undefined\n dryColor: number\n dryAt: number\n dryTime: number\n}\n\nfunction createDecal(): Decal {\n return { on: false, x: 0, y: 0, z: 0, sx: 0, sy: 0, t: 0, life: 0, grow: 0, color: 0, normal: UP, fluid: undefined, dryColor: -1, dryAt: 0, dryTime: 0 }\n}\n\n/** The Workshop's decals (W265-306): 400 marks on the ground in a ring buffer. Wet ones spread and evaporate (fuel, water), the rest last their life. */\nexport class DecalPool {\n readonly decals: Decal[] = Array.from({ length: CAPACITY }, createDecal)\n count = 0\n private next = 0\n\n constructor(private readonly context: WorkshopContext) {}\n\n add(x: number, y: number, z: number, size: number, color: number, options: DecalOptions = {}): void {\n const random = this.context.random\n const index = this.next++ % CAPACITY\n const decal = this.decals[index]!\n const normal = options.n ?? UP\n random.between(0, 6.28)\n const offset = 0.004 + (index % 40) * 0.0004\n decal.on = true\n decal.x = x + normal[0] * offset\n decal.y = y + normal[1] * offset\n decal.z = z + normal[2] * offset\n decal.sx = size * (options.ar ? 1 : random.between(0.8, 1.2))\n decal.sy = size * (options.ar || random.between(0.65, 1.1))\n decal.t = -(options.delay || 0)\n decal.grow = options.grow ?? DEFAULT_GROW\n decal.life = options.life || DEFAULT_LIFE\n decal.color = color\n decal.normal = normal\n const fluid = options.fluid ?? (options.dark ? 'blood' : undefined)\n decal.fluid = fluid\n decal.dryAt = options.dark && !options.fluid ? 0 : options.dryAt ?? (fluid === 'blood' || fluid === 'bile' ? 40 : 0)\n decal.dryTime = options.dark && !options.fluid ? options.dark : options.dryT ?? (fluid === 'blood' || fluid === 'bile' ? 30 : 0)\n decal.dryColor = options.dry ?? (fluid === 'bile' ? 0x3e4220 : -1)\n }\n\n clear(): void {\n for (const decal of this.decals) decal.on = false\n this.count = 0\n }\n\n update(dt: number): void {\n this.count = 0\n for (const decal of this.decals) {\n if (!decal.on) continue\n this.count++\n decal.t += dt\n if (decal.t < 0) continue\n decal.life -= dt\n if (decal.life <= 0) {\n decal.on = false\n continue\n }\n const evaporation = decal.fluid === 'fuel' || decal.fluid === 'water' ? EVAPORATION[decal.fluid] : undefined\n if (evaporation && (decal.t - evaporation[0]) / evaporation[1] >= 1) decal.on = false\n }\n }\n}\n","import type { MeshSpawn, MeshShape, Vec3Tuple } from './types'\n\nconst CAPACITY = 400\nconst IDENTITY: Quaternion = [0, 0, 0, 1]\n\n/** A unit quaternion `[x, y, z, w]`. */\nexport type Quaternion = readonly [number, number, number, number]\n\n/** One solid particle as the pool keeps it; a renderer draws it from these fields and never changes them. */\nexport interface MeshParticle {\n /** Grows by one for every particle ever spawned: a renderer knows a new one by it. */\n readonly id: number\n readonly shape: MeshShape\n readonly radius: number\n readonly height: number\n readonly segments: number\n readonly segments2: number\n readonly color: number\n readonly additive: boolean\n readonly lit: boolean\n /** Where the group is, how it is turned (placement and roll), where the piece sits in it (before the scale) and how the piece is turned in it (Euler XYZ, changes with `spin`). */\n x: number\n y: number\n z: number\n readonly frame: Quaternion\n readonly at: Vec3Tuple\n rx: number\n ry: number\n rz: number\n scale: number\n opacity: number\n age: number\n readonly life: number\n vx: number\n vy: number\n vz: number\n readonly spin: Vec3Tuple\n readonly gravity: number\n readonly drag: number\n readonly grow: number\n readonly ramp: readonly [number, number] | undefined\n readonly fade: boolean\n readonly startOpacity: number\n}\n\nfunction multiply(a: Quaternion, b: Quaternion): Quaternion {\n const [ax, ay, az, aw] = a\n const [bx, by, bz, bw] = b\n return [aw * bx + ax * bw + ay * bz - az * by, aw * by - ax * bz + ay * bw + az * bx, aw * bz + ax * by - ay * bx + az * bw, aw * bw - ax * bx - ay * by - az * bz]\n}\n\n/** The turn of `angle` about the x axis. */\nconst aboutX = (angle: number): Quaternion => [Math.sin(angle / 2), 0, 0, Math.cos(angle / 2)]\n\n/**\n * The solids of the Workshop's recipes: the gallery's `Particles` (thrown with a velocity and a spin, slowed, lifted by a negative gravity, grown and faded) without\n * the floor, in order of birth. The oldest goes when the pool is full. Each frame does what the gallery's did, in its order: age, fade, gravity, drag, move, spin, grow.\n */\nexport class MeshPool {\n readonly particles: MeshParticle[] = []\n private serial = 0\n\n constructor(readonly capacity: number = CAPACITY) {}\n\n get count(): number {\n return this.particles.length\n }\n\n spawn(options: MeshSpawn, frame: Quaternion = IDENTITY): MeshParticle {\n const velocity = options.v ?? [0, 0, 0]\n const world = options.wv ?? [0, 0, 0]\n const particle: MeshParticle = {\n id: ++this.serial,\n shape: options.shape,\n radius: options.r,\n height: options.h ?? 0,\n segments: options.seg ?? 0,\n segments2: options.seg2 ?? 0,\n color: options.c0,\n additive: options.add === true,\n lit: options.lit === true,\n x: options.p[0],\n y: options.p[1],\n z: options.p[2],\n frame: options.roll ? multiply(frame, aboutX(options.roll)) : frame,\n at: options.at ?? [0, 0, 0],\n rx: options.rot?.[0] ?? 0,\n ry: options.rot?.[1] ?? 0,\n rz: options.rot?.[2] ?? 0,\n scale: 1,\n opacity: options.a,\n age: 0,\n life: options.life,\n vx: velocity[0] + world[0],\n vy: velocity[1] + world[1],\n vz: velocity[2] + world[2],\n spin: options.spin ?? [0, 0, 0],\n gravity: options.g ?? 0,\n drag: options.drag ?? 0,\n grow: options.grow ?? 0,\n ramp: options.ramp,\n fade: options.fade === true,\n startOpacity: options.a,\n }\n this.particles.push(particle)\n if (this.particles.length > this.capacity) this.particles.shift()\n return particle\n }\n\n update(dt: number): void {\n const live = this.particles\n for (let index = 0; index < live.length; index++) {\n const particle = live[index]!\n particle.age += dt\n if (particle.ramp ? particle.age >= particle.life : particle.age > particle.life) {\n live.splice(index--, 1)\n continue\n }\n if (particle.fade) particle.opacity = Math.max(0, particle.startOpacity * (1 - particle.age / particle.life))\n particle.vy -= particle.gravity * dt\n if (particle.drag) {\n const keep = Math.max(0, 1 - particle.drag * dt)\n particle.vx *= keep\n particle.vy *= keep\n particle.vz *= keep\n }\n particle.x += particle.vx * dt\n particle.y += particle.vy * dt\n particle.z += particle.vz * dt\n particle.rx += particle.spin[0] * dt\n particle.ry += particle.spin[1] * dt\n particle.rz += particle.spin[2] * dt\n if (particle.grow) particle.scale *= 1 + particle.grow * dt\n if (particle.ramp) particle.scale = particle.ramp[0] + Math.min(1, particle.age / particle.life) * particle.ramp[1]\n }\n }\n\n clear(): void {\n this.particles.length = 0\n }\n}\n","import { hexToRgb } from '../color'\nimport { lerp } from '../vector'\nimport type { MutableVec3, RandomStream } from '../random'\nimport { coneDirection, drawRange, randomUnit } from './direction'\nimport type { SpriteSpawn, WorkshopContext } from './types'\n\n/** Colour, size and alpha of a particle at its current age: what the renderer reads (`col`, `siz`, `alp`). */\nconst DEFAULT_FADE_IN = 0.1\nconst FLOATS_PER_VECTOR = 3\nconst BYTE_MAX = 255\n\nexport interface DecalFromSprite {\n (x: number, y: number, z: number, size: number, color: number, options: { readonly grow: number; readonly fluid: 'blood' }): void\n}\n\n/**\n * The Workshop's `PSys` (W179-220): a pool of sprites in typed arrays, as the page keeps them. A dead record is replaced by the last one, so the order\n * in the arrays is the page's, and the golden hashes the arrays as they are. Additive and normal-blend systems are two instances.\n */\nexport class SpritePool {\n count = 0\n readonly position: Float32Array\n readonly velocity: Float32Array\n readonly colorStart: Float32Array\n readonly colorEnd: Float32Array\n readonly life: Float32Array\n readonly maxLife: Float32Array\n readonly startSize: Float32Array\n readonly growth: Float32Array\n readonly startAlpha: Float32Array\n readonly gravity: Float32Array\n readonly drag: Float32Array\n readonly fadeIn: Float32Array\n readonly groundMode: Uint8Array\n readonly decalSize: Float32Array\n /** Written by `update`: what the renderer draws. */\n readonly color: Float32Array\n readonly size: Float32Array\n readonly alpha: Float32Array\n private readonly vectorArrays: readonly Float32Array[]\n private readonly scalarArrays: readonly (Float32Array | Uint8Array)[]\n private readonly direction: MutableVec3 = { x: 0, y: 0, z: 0 }\n private readonly unit: MutableVec3 = { x: 0, y: 0, z: 0 }\n\n constructor(readonly capacity: number, private readonly context: WorkshopContext) {\n const vectorArray = (): Float32Array => new Float32Array(capacity * FLOATS_PER_VECTOR)\n const scalarArray = (): Float32Array => new Float32Array(capacity)\n this.position = vectorArray()\n this.velocity = vectorArray()\n this.colorStart = vectorArray()\n this.colorEnd = vectorArray()\n this.color = vectorArray()\n this.life = scalarArray()\n this.maxLife = scalarArray()\n this.startSize = scalarArray()\n this.growth = scalarArray()\n this.startAlpha = scalarArray()\n this.gravity = scalarArray()\n this.drag = scalarArray()\n this.fadeIn = scalarArray()\n this.decalSize = scalarArray()\n this.size = scalarArray()\n this.alpha = scalarArray()\n this.groundMode = new Uint8Array(capacity)\n this.vectorArrays = [this.position, this.velocity, this.colorStart, this.colorEnd]\n this.scalarArrays = [this.life, this.maxLife, this.startSize, this.growth, this.startAlpha, this.gravity, this.drag, this.fadeIn, this.groundMode, this.decalSize]\n }\n\n spawn(options: SpriteSpawn): void {\n if (this.count >= this.capacity) return\n const random: RandomStream = this.context.random\n const index = this.count++\n const offset = index * FLOATS_PER_VECTOR\n let [x, y, z] = options.p\n if (options.r) {\n randomUnit(random, this.unit)\n const reach = Math.cbrt(random.next()) * options.r\n x += this.unit.x * reach\n y += this.unit.y * reach * (options.flat ? 0 : 1)\n z += this.unit.z * reach\n }\n if (options.box) {\n x += random.between(-options.box[0], options.box[0])\n y += random.between(-options.box[1], options.box[1])\n z += random.between(-options.box[2], options.box[2])\n }\n this.position[offset] = x\n this.position[offset + 1] = y\n this.position[offset + 2] = z\n coneDirection(random, options.dir, options.cone ?? 3.2, this.direction)\n const speed = drawRange(random, options.sp)\n this.velocity[offset] = this.direction.x * speed + (options.v ? options.v[0] : 0)\n this.velocity[offset + 1] = this.direction.y * speed + (options.v ? options.v[1] : 0)\n this.velocity[offset + 2] = this.direction.z * speed + (options.v ? options.v[2] : 0)\n const start = hexToRgb(options.c0 ?? 0xffffff)\n const end = options.c1 != null ? hexToRgb(options.c1) : start\n for (let channel = 0; channel < FLOATS_PER_VECTOR; channel++) {\n this.colorStart[offset + channel] = start[channel]\n this.colorEnd[offset + channel] = end[channel]\n }\n this.life[index] = this.maxLife[index] = drawRange(random, options.life || 1)\n this.startSize[index] = drawRange(random, options.size || 0.1)\n this.growth[index] = options.grow || 1\n this.startAlpha[index] = options.a ?? 1\n this.gravity[index] = options.g || 0\n this.drag[index] = options.drag || 0\n this.fadeIn[index] = options.fi ?? DEFAULT_FADE_IN\n this.groundMode[index] = options.gnd || 0\n this.decalSize[index] = options.ds ? drawRange(random, options.ds) : 0.1\n }\n\n kill(index: number): void {\n const last = --this.count\n if (index === last) return\n for (const array of this.vectorArrays) {\n for (let component = 0; component < FLOATS_PER_VECTOR; component++) array[index * FLOATS_PER_VECTOR + component] = array[last * FLOATS_PER_VECTOR + component]\n }\n for (const array of this.scalarArrays) array[index] = array[last]\n }\n\n clear(): void {\n this.count = 0\n }\n\n update(dt: number, leaveDecal: DecalFromSprite): void {\n const { position, velocity } = this\n for (let index = this.count - 1; index >= 0; index--) {\n this.life[index] -= dt\n if (this.life[index] <= 0) {\n this.kill(index)\n continue\n }\n const offset = index * FLOATS_PER_VECTOR\n const age = 1 - this.life[index] / this.maxLife[index]\n velocity[offset + 1] -= this.gravity[index] * dt\n const factor = Math.max(0, 1 - this.drag[index] * dt)\n velocity[offset] *= factor\n velocity[offset + 1] *= factor\n velocity[offset + 2] *= factor\n position[offset] += velocity[offset] * dt\n position[offset + 1] += velocity[offset + 1] * dt\n position[offset + 2] += velocity[offset + 2] * dt\n const mode = this.groundMode[index]\n if (mode) {\n const groundHeight = this.context.groundAt(position[offset], position[offset + 2])\n if (position[offset + 1] < groundHeight) {\n if (mode === 1) {\n this.kill(index)\n continue\n }\n if (mode === 2) {\n position[offset + 1] = groundHeight + 0.001\n velocity[offset + 1] *= -0.3\n velocity[offset] *= 0.55\n velocity[offset + 2] *= 0.55\n }\n if (mode === 3) {\n if (this.context.quality.level < 2) {\n const red = (this.colorStart[offset] * BYTE_MAX) << 16\n const green = (this.colorStart[offset + 1] * BYTE_MAX) << 8\n leaveDecal(position[offset], groundHeight, position[offset + 2], this.decalSize[index], red | green | (this.colorStart[offset + 2] * BYTE_MAX), { grow: 0.15, fluid: 'blood' })\n }\n this.kill(index)\n continue\n }\n }\n }\n this.size[index] = this.startSize[index] * (1 + (this.growth[index] - 1) * age)\n const fadeIn = this.fadeIn[index]\n this.alpha[index] = this.startAlpha[index] * (fadeIn > 0 && age < fadeIn ? age / fadeIn : 1 - (age - fadeIn) / (1 - fadeIn))\n for (let channel = 0; channel < FLOATS_PER_VECTOR; channel++) this.color[offset + channel] = lerp(this.colorStart[offset + channel], this.colorEnd[offset + channel], age)\n }\n }\n}\n","import { hexToRgb } from '../color'\nimport type { MutableVec3 } from '../random'\nimport { lerp } from '../vector'\nimport { coneDirection, drawRange } from './direction'\nimport type { SpriteSpawn, WorkshopContext } from './types'\n\nconst FLOATS_PER_VECTOR = 3\nconst SEGMENT_FLOATS = 6\nconst TAIL_BRIGHTNESS = 0.15\n\n/** The Workshop's `Streaks` (W223-245): short lines that follow their own velocity (sparks, tracers, rain-like glints). Same swap-with-last pool as `SpritePool`. */\nexport class StreakPool {\n count = 0\n readonly point: Float32Array\n readonly velocity: Float32Array\n readonly colorStart: Float32Array\n readonly colorEnd: Float32Array\n readonly life: Float32Array\n readonly maxLife: Float32Array\n readonly length: Float32Array\n readonly gravity: Float32Array\n readonly drag: Float32Array\n readonly groundMode: Uint8Array\n readonly startAlpha: Float32Array\n /** Two end points and two colours per line: what the renderer draws. */\n readonly segment: Float32Array\n readonly segmentColor: Float32Array\n private readonly vectorArrays: readonly Float32Array[]\n private readonly scalarArrays: readonly (Float32Array | Uint8Array)[]\n private readonly direction: MutableVec3 = { x: 0, y: 0, z: 0 }\n\n constructor(readonly capacity: number, private readonly context: WorkshopContext) {\n const vectorArray = (): Float32Array => new Float32Array(capacity * FLOATS_PER_VECTOR)\n const scalarArray = (): Float32Array => new Float32Array(capacity)\n this.point = vectorArray()\n this.velocity = vectorArray()\n this.colorStart = vectorArray()\n this.colorEnd = vectorArray()\n this.life = scalarArray()\n this.maxLife = scalarArray()\n this.length = scalarArray()\n this.gravity = scalarArray()\n this.drag = scalarArray()\n this.startAlpha = scalarArray()\n this.groundMode = new Uint8Array(capacity)\n this.segment = new Float32Array(capacity * SEGMENT_FLOATS)\n this.segmentColor = new Float32Array(capacity * SEGMENT_FLOATS)\n this.vectorArrays = [this.point, this.velocity, this.colorStart, this.colorEnd]\n this.scalarArrays = [this.life, this.maxLife, this.length, this.gravity, this.drag, this.groundMode, this.startAlpha]\n }\n\n spawn(options: SpriteSpawn): void {\n if (this.count >= this.capacity) return\n const random = this.context.random\n const index = this.count++\n const offset = index * FLOATS_PER_VECTOR\n let [x, y, z] = options.p\n if (options.box) {\n x += random.between(-options.box[0], options.box[0])\n y += random.between(-options.box[1], options.box[1])\n z += random.between(-options.box[2], options.box[2])\n }\n this.point[offset] = x\n this.point[offset + 1] = y\n this.point[offset + 2] = z\n coneDirection(random, options.dir, options.cone ?? 3.2, this.direction)\n const speed = drawRange(random, options.sp)\n this.velocity[offset] = this.direction.x * speed + (options.v ? options.v[0] : 0)\n this.velocity[offset + 1] = this.direction.y * speed + (options.v ? options.v[1] : 0)\n this.velocity[offset + 2] = this.direction.z * speed + (options.v ? options.v[2] : 0)\n const start = hexToRgb(options.c0 ?? 0xffd080)\n const end = options.c1 != null ? hexToRgb(options.c1) : start\n for (let channel = 0; channel < FLOATS_PER_VECTOR; channel++) {\n this.colorStart[offset + channel] = start[channel]\n this.colorEnd[offset + channel] = end[channel]\n }\n this.life[index] = this.maxLife[index] = drawRange(random, options.life || 0.4)\n this.length[index] = options.len || 0.02\n this.gravity[index] = options.g || 0\n this.drag[index] = options.drag || 0\n this.groundMode[index] = options.gnd || 0\n this.startAlpha[index] = options.a ?? 1\n }\n\n kill(index: number): void {\n const last = --this.count\n if (index === last) return\n for (const array of this.vectorArrays) {\n for (let component = 0; component < FLOATS_PER_VECTOR; component++) array[index * FLOATS_PER_VECTOR + component] = array[last * FLOATS_PER_VECTOR + component]\n }\n for (const array of this.scalarArrays) array[index] = array[last]\n }\n\n clear(): void {\n this.count = 0\n }\n\n update(dt: number): void {\n const { point, velocity } = this\n for (let index = this.count - 1; index >= 0; index--) {\n this.life[index] -= dt\n if (this.life[index] <= 0) {\n this.kill(index)\n continue\n }\n const offset = index * FLOATS_PER_VECTOR\n const age = 1 - this.life[index] / this.maxLife[index]\n velocity[offset + 1] -= this.gravity[index] * dt\n const factor = Math.max(0, 1 - this.drag[index] * dt)\n velocity[offset] *= factor\n velocity[offset + 1] *= factor\n velocity[offset + 2] *= factor\n point[offset] += velocity[offset] * dt\n point[offset + 1] += velocity[offset + 1] * dt\n point[offset + 2] += velocity[offset + 2] * dt\n if (this.groundMode[index]) {\n const groundHeight = this.context.groundAt(point[offset], point[offset + 2])\n if (point[offset + 1] < groundHeight) {\n if (this.groundMode[index] === 1) {\n this.kill(index)\n continue\n }\n point[offset + 1] = groundHeight + 0.001\n velocity[offset + 1] *= -0.35\n velocity[offset] *= 0.6\n velocity[offset + 2] *= 0.6\n }\n }\n const length = this.length[index]\n const alpha = this.startAlpha[index] * (1 - age)\n const at = index * SEGMENT_FLOATS\n this.segment[at] = point[offset]\n this.segment[at + 1] = point[offset + 1]\n this.segment[at + 2] = point[offset + 2]\n this.segment[at + 3] = point[offset] - velocity[offset] * length\n this.segment[at + 4] = point[offset + 1] - velocity[offset + 1] * length\n this.segment[at + 5] = point[offset + 2] - velocity[offset + 2] * length\n for (let channel = 0; channel < FLOATS_PER_VECTOR; channel++) {\n const shade = lerp(this.colorStart[offset + channel], this.colorEnd[offset + channel], age) * alpha\n this.segmentColor[at + channel] = shade\n this.segmentColor[at + 3 + channel] = shade * TAIL_BRIGHTNESS\n }\n }\n }\n}\n","import type { Vec3Tuple, WorkshopContext } from './types'\n\nconst RING_CAPACITY = 120\nconst LIGHT_CAPACITY = 4\nconst UP: Vec3Tuple = [0, 1, 0]\nconst DEFAULT_LIGHT_DISTANCE = 14\n\nexport interface WaveRing {\n on: boolean\n point: Vec3Tuple\n from: number\n to: number\n t: number\n duration: number\n color: number\n normal: Vec3Tuple\n hold: number\n}\n\n/** The Workshop's expanding rings on the ground (W308-315): 120 in a ring buffer. A ring with `hold` stays at that brightness until cleared. */\nexport class WaveRingPool {\n readonly rings: WaveRing[] = Array.from({ length: RING_CAPACITY }, () => ({ on: false, point: UP, from: 0, to: 0, t: 0, duration: 1, color: 0, normal: UP, hold: 0 }))\n count = 0\n private next = 0\n\n add(point: Vec3Tuple, from: number, to: number, duration: number, color: number, normal?: Vec3Tuple, hold?: number): void {\n const ring = this.rings[this.next++ % RING_CAPACITY]!\n ring.on = true\n ring.point = [point[0], point[1], point[2]]\n ring.from = from\n ring.to = to\n ring.t = 0\n ring.duration = duration\n ring.color = color\n ring.normal = normal ?? UP\n ring.hold = hold || 0\n }\n\n clear(): void {\n for (const ring of this.rings) ring.on = false\n this.count = 0\n }\n\n update(dt: number): void {\n this.count = 0\n for (const ring of this.rings) {\n if (!ring.on) continue\n this.count++\n ring.t += dt\n if (ring.t / ring.duration >= 1 && !ring.hold) ring.on = false\n }\n }\n}\n\nexport interface FlashLight {\n x: number\n y: number\n z: number\n color: number\n distance: number\n peak: number\n duration: number\n t: number\n intensity: number\n /** Holds `peak` until `duration` is over. */\n steady: boolean\n}\n\nexport interface FireLightSlot {\n x: number\n y: number\n z: number\n intensity: number\n}\n\n/** The four short flashes of the Workshop (W333-337), taken in turn, and its two fire lights. */\nexport class FlashLightPool {\n readonly lights: FlashLight[] = Array.from({ length: LIGHT_CAPACITY }, () => ({ x: 0, y: 0, z: 0, color: 0xffc070, distance: DEFAULT_LIGHT_DISTANCE, peak: 0, duration: 1, t: 0, intensity: 0, steady: false }))\n readonly fire: FireLightSlot[] = [\n { x: 0, y: 0, z: 0, intensity: 0 },\n { x: 0, y: 0, z: 0, intensity: 0 },\n ]\n private next = 0\n\n constructor(private readonly context: WorkshopContext) {}\n\n get lit(): number {\n return this.lights.filter((light) => light.intensity > 0).length\n }\n\n flash(point: Vec3Tuple, color: number, intensity: number, duration: number, distance?: number, force?: boolean | 'boom', steady?: boolean): void {\n const { level } = this.context.quality\n if (level >= 1 && !force) return\n if (level >= 2 && force !== 'boom') return\n const light = this.lights[this.next++ % LIGHT_CAPACITY]!\n light.x = point[0]\n light.y = point[1]\n light.z = point[2]\n light.color = color\n light.distance = distance || DEFAULT_LIGHT_DISTANCE\n light.peak = intensity\n light.duration = duration\n light.t = 0\n light.steady = steady === true\n light.intensity = intensity\n }\n\n clear(): void {\n for (const light of this.lights) light.intensity = 0\n for (const light of this.fire) light.intensity = 0\n }\n\n update(dt: number): void {\n for (const light of this.lights) {\n if (light.intensity <= 0) continue\n light.t += dt\n const age = light.t / light.duration\n light.intensity = age >= 1 ? 0 : light.steady ? light.peak : light.peak * (1 - age) * (1 - age)\n }\n }\n}\n","import { PROFILES } from '../budget'\nimport type { RandomStream } from '../random'\nimport { createScreenState, resetScreenState, updateScreen } from '../screen'\nimport type { ScreenState, VfxProfile } from '../types'\nimport { DebrisPool } from './debris'\nimport { DecalPool } from './decals'\nimport { RainField } from './rain'\nimport { MeshPool } from './meshes'\nimport type { MeshParticle, Quaternion } from './meshes'\nimport { SpritePool } from './sprites'\nimport { StreakPool } from './streaks'\nimport type { DebrisSpawn, DecalOptions, MeshSpawn, SpriteSpawn, SpriteSystem, Vec3Tuple, WorkshopContext, WorkshopTrace } from './types'\nimport { FlashLightPool, WaveRingPool } from './waves'\n\nconst QUALITY_MULTIPLIERS = [1, 0.5, 0.2] as const\nconst FIRE_FLICKER_SWAY = 0.25\nconst FIRE_FLICKER_JITTER = 0.08\nconst LENS_FADE = 0.16\n\n/** Water or blood on the camera lens: a soft blob that fades and runs down (W351-352). */\nexport interface LensBlob {\n x: number\n y: number\n radius: number\n color: string\n alpha: number\n fall: number\n}\n\n/** The page's `P0` (W347, W1277): what an effect asks of the post-process for this frame only; it is emptied before every frame's tick. */\nexport interface PostProcess {\n desat?: number\n tunnel?: number\n blur?: number\n cold?: number\n chroma?: number\n hue?: number\n red?: number\n tintAlpha?: number\n tintColor?: string\n}\n\n/**\n * Where an effect is put when it is played at a point instead of on the Workshop's set: what the recipe wrote at `anchor` lands at `origin`, turned\n * about the vertical axis by `yaw`. Positions are moved and turned, directions and velocities only turned. With an `orientation` (a unit quaternion) the turn is\n * that one, pitch and roll included, and `yaw` is not used.\n */\nexport interface Placement {\n readonly origin: Vec3Tuple\n readonly anchor: Vec3Tuple\n readonly yaw: number\n readonly orientation?: Quaternion\n}\n\n/** The vector turned by the unit quaternion (three.js's `applyQuaternion`). */\nfunction rotate(q: Quaternion, v: Vec3Tuple): Vec3Tuple {\n const [qx, qy, qz, qw] = q\n const ix = qw * v[0] + qy * v[2] - qz * v[1]\n const iy = qw * v[1] + qz * v[0] - qx * v[2]\n const iz = qw * v[2] + qx * v[1] - qy * v[0]\n const iw = -qx * v[0] - qy * v[1] - qz * v[2]\n return [ix * qw + iw * -qx + iy * -qz - iz * -qy, iy * qw + iw * -qy + iz * -qx - ix * -qz, iz * qw + iw * -qz + ix * -qy - iy * -qx]\n}\n\nexport interface WorkshopOptions {\n readonly random: RandomStream\n readonly profile?: VfxProfile\n readonly groundAt?: (x: number, z: number) => number\n readonly trace?: WorkshopTrace\n}\n\n/** The default ground of the Workshop scene: a raised kerb 12 cm high between z -4.5 and -2.9 (W29). */\nexport function workshopGround(x: number, z: number): number {\n return z < -2.9 && z > -4.5 && Math.abs(x) < 13 ? 0.12 : 0\n}\n\n/**\n * Everything the Workshop's recipes spawn into and the loop that ages it (W168-371, W1285-1287): sprites, streaks, debris, decals, rings, lights, the\n * screen state and the lens blobs, plus the quality level that thins what a recipe asks for and the accumulators that turn a rate into spawns.\n */\nexport class WorkshopEngine {\n readonly context: WorkshopContext\n readonly add: SpritePool\n readonly normal: SpritePool\n readonly streaks: StreakPool\n readonly debris: DebrisPool\n readonly decals: DecalPool\n readonly rings = new WaveRingPool()\n readonly lights: FlashLightPool\n readonly rain = new RainField(this)\n readonly meshes = new MeshPool()\n readonly screen: ScreenState = createScreenState()\n readonly lens: LensBlob[] = []\n post: PostProcess = {}\n /** Set by whoever runs an effect for the time it runs; `undefined` leaves every number as the recipe wrote it. */\n placement: Placement | undefined\n private readonly accumulators = new Map<string, number>()\n\n constructor(options: WorkshopOptions) {\n const profile = options.profile ?? PROFILES.high\n this.context = { random: options.random, groundAt: options.groundAt ?? workshopGround, quality: { level: 0, mult: 1 }, trace: options.trace }\n this.add = new SpritePool(profile.sprites.add, this.context)\n this.normal = new SpritePool(profile.sprites.normal, this.context)\n this.streaks = new StreakPool(profile.streaks, this.context)\n this.debris = new DebrisPool(this.context)\n this.decals = new DecalPool(this.context)\n this.lights = new FlashLightPool(this.context)\n }\n\n get random(): RandomStream {\n return this.context.random\n }\n\n get quality(): number {\n return this.context.quality.level\n }\n\n setQuality(level: number): void {\n this.context.quality.level = level\n this.context.quality.mult = QUALITY_MULTIPLIERS[level] ?? 1\n }\n\n /** `n` (W340): a count thinned by the quality level, never to zero when something was asked. */\n scaled(count: number): number {\n return Math.max(count > 0 ? 1 : 0, Math.round(count * this.context.quality.mult))\n }\n\n /** A point of the recipe's space in the world (`undefined` placement: as is). */\n place(point: Vec3Tuple): Vec3Tuple {\n const placement = this.placement\n if (!placement) return point\n const { anchor, origin, yaw, orientation } = placement\n if (orientation) {\n const turned = rotate(orientation, [point[0] - anchor[0], point[1] - anchor[1], point[2] - anchor[2]])\n return [origin[0] + turned[0], origin[1] + turned[1], origin[2] + turned[2]]\n }\n const x = point[0] - anchor[0]\n const z = point[2] - anchor[2]\n const cos = Math.cos(yaw)\n const sin = Math.sin(yaw)\n return [origin[0] + cos * x + sin * z, origin[1] + point[1] - anchor[1], origin[2] - sin * x + cos * z]\n }\n\n /** A direction or velocity of the recipe's space in the world. */\n turn(vector: Vec3Tuple): Vec3Tuple {\n const placement = this.placement\n if (!placement) return vector\n if (placement.orientation) return rotate(placement.orientation, vector)\n const cos = Math.cos(placement.yaw)\n const sin = Math.sin(placement.yaw)\n return [cos * vector[0] + sin * vector[2], vector[1], -sin * vector[0] + cos * vector[2]]\n }\n\n private placed<T extends { readonly p: Vec3Tuple; readonly dir?: Vec3Tuple; readonly v?: Vec3Tuple }>(options: T): T {\n if (!this.placement) return options\n return { ...options, p: this.place(options.p), ...(options.dir ? { dir: this.turn(options.dir) } : {}), ...(options.v ? { v: this.turn(options.v) } : {}) }\n }\n\n spawn(system: SpriteSystem, recipe: SpriteSpawn): void {\n const options = this.placed(recipe)\n this.context.trace?.(system, [options])\n if (system === 'add') this.add.spawn(options)\n else if (system === 'normal') this.normal.spawn(options)\n else this.streaks.spawn(options)\n }\n\n burst(system: SpriteSystem, count: number, options: SpriteSpawn): void {\n const total = this.scaled(count)\n for (let spawned = 0; spawned < total; spawned++) this.spawn(system, options)\n }\n\n /** `emit` (W343-345): `rate` per second, thinned by the quality, carried over between frames under `key`. `options` is read again for every particle. */\n emit(key: string, system: SpriteSystem, rate: number, dt: number, options: () => SpriteSpawn): void {\n let carried = (this.accumulators.get(key) ?? 0) + rate * this.context.quality.mult * dt\n while (carried >= 1) {\n carried -= 1\n this.spawn(system, options())\n }\n this.accumulators.set(key, carried)\n }\n\n /** A solid particle (a cone, a sphere): the group sits at `p` turned by the placement, `v` is turned with it and `wv` is not. */\n mesh(recipe: MeshSpawn): MeshParticle {\n const placement = this.placement\n const options: MeshSpawn = placement ? { ...recipe, p: this.place(recipe.p), ...(recipe.v ? { v: this.turn(recipe.v) } : {}) } : recipe\n this.context.trace?.('mesh', [options])\n // a piece of a group (`at`, `roll`) is turned with the placement; a free one (smoke, a flame) keeps its own turn in the world\n const grouped = recipe.at !== undefined || recipe.roll !== undefined\n return this.meshes.spawn(options, placement && grouped ? (placement.orientation ?? ([0, Math.sin(placement.yaw / 2), 0, Math.cos(placement.yaw / 2)] as const)) : undefined)\n }\n\n debrisPiece(recipe: DebrisSpawn): void {\n const options = this.placed(recipe)\n this.context.trace?.('debris', [options])\n this.debris.spawn(options)\n }\n\n decal(recipe: readonly [number, number, number, number, number, DecalOptions?]): void {\n let args = recipe\n if (this.placement) {\n const [x, y, z] = this.place([recipe[0], recipe[1], recipe[2]])\n const options = recipe[5]\n args = [x, y, z, recipe[3], recipe[4], options?.n ? { ...options, n: this.turn(options.n) } : options]\n }\n this.context.trace?.('decal', args)\n this.decals.add(args[0], args[1], args[2], args[3], args[4], args[5])\n }\n\n ring(recipe: readonly [Vec3Tuple, number, number, number, number, Vec3Tuple?, number?]): void {\n const args = this.placement ? ([this.place(recipe[0]), recipe[1], recipe[2], recipe[3], recipe[4], recipe[5] && this.turn(recipe[5]), recipe[6]] as const) : recipe\n this.context.trace?.('ring', args)\n this.rings.add(args[0], args[1], args[2], args[3], args[4], args[5], args[6])\n }\n\n /** `steady` (a seventh number): the light holds its intensity for its duration and goes out, where the Workshop's fades with the square of its age. */\n flash(recipe: readonly [Vec3Tuple, number, number, number, number?, (boolean | 'boom')?, boolean?]): void {\n const args = this.placement ? ([this.place(recipe[0]), recipe[1], recipe[2], recipe[3], recipe[4], recipe[5], recipe[6]] as const) : recipe\n this.context.trace?.('flash', args.slice(0, 6))\n this.lights.flash(args[0], args[1], args[2], args[3], args[4], args[5], args[6])\n }\n\n /** `fireLight` (W400): sways with the clock and flickers with one draw. */\n fireLight(slot: 0 | 1, recipe: Vec3Tuple, intensity: number, time: number): void {\n const light = this.lights.fire[slot]!\n const point = this.place(recipe)\n light.x = point[0]\n light.y = point[1]\n light.z = point[2]\n light.intensity = intensity * (0.75 + FIRE_FLICKER_SWAY * Math.sin(time * 23) * Math.sin(time * 7.3) + this.random.between(-FIRE_FLICKER_JITTER, FIRE_FLICKER_JITTER))\n }\n\n lensBlob(x: number, y: number, radius: number, color: string, alpha: number): void {\n this.lens.push({ x, y, radius, color, alpha, fall: this.random.between(2, 8) })\n }\n\n /** The order of the page's loop (W1285-1287): the sprites, streaks, debris, decals, rings and lights, then the screen and its lens. */\n update(dt: number): void {\n const leaveDecal = (x: number, y: number, z: number, size: number, color: number, options: DecalOptions): void => {\n this.decal([x, y, z, size, color, options])\n }\n this.add.update(dt, leaveDecal)\n this.normal.update(dt, leaveDecal)\n this.streaks.update(dt)\n this.debris.update(dt)\n this.decals.update(dt)\n this.rings.update(dt)\n this.meshes.update(dt)\n this.lights.update(dt)\n updateScreen(this.screen, dt)\n for (let index = this.lens.length - 1; index >= 0; index--) {\n const blob = this.lens[index]!\n blob.alpha -= dt * LENS_FADE\n blob.y += blob.fall * dt\n if (blob.alpha <= 0) this.lens.splice(index, 1)\n }\n }\n\n clear(): void {\n this.add.clear()\n this.normal.clear()\n this.streaks.clear()\n this.debris.clear()\n this.decals.clear()\n this.rings.clear()\n this.meshes.clear()\n this.lights.clear()\n this.rain.clear()\n this.accumulators.clear()\n resetScreenState(this.screen)\n this.lens.length = 0\n this.post = {}\n }\n}\n","import type { Rgb } from './color'\n\nexport type { Rgb }\n\nexport type Vec3Like = readonly [number, number, number]\n\n/** The six layers of an explosion the viewer can turn off one by one (L478): flash and light, fire, shock wave, smoke and dust, shrapnel and debris, marks on the ground. */\nexport type VfxLayer = 'flash' | 'fire' | 'shock' | 'smoke' | 'shrapnel' | 'mark'\n\nexport const VFX_LAYERS: readonly VfxLayer[] = ['flash', 'fire', 'shock', 'smoke', 'shrapnel', 'mark']\n\n/** A chain of things that move: anything with a position, or a position. */\nexport type Follow = { readonly x: number; readonly y: number; readonly z: number } | { readonly position: { readonly x: number; readonly y: number; readonly z: number } }\n\n/** A residual fire left behind by an explosion (L798 `residual`), or started by `world.fire`. */\nexport interface FireSpec {\n /** Offsets from the blast point. */\n readonly dx?: number\n readonly dz?: number\n readonly radius: number\n readonly life: number\n readonly rate?: number\n readonly smoke?: number\n /** Grows from `startRadius` to `radius` over `grow` seconds. */\n readonly startRadius?: number\n readonly grow?: number\n /** `0xRRGGBB` of the smoke the fire gives (default soot black). */\n readonly smokeColor?: number\n}\n\nexport interface MushroomSpec {\n readonly height: number\n readonly radius: number\n readonly count: number\n readonly stem: number\n readonly life: number\n /** How fast the ring climbs towards `height` (the Lab's `rk`, default .55). */\n readonly riseRate?: number\n readonly roll?: number\n readonly color?: number\n}\n\n/** The Lab's explosion profile with English names; every number is the Lab's (`sources` lists the lines they come from). */\nexport interface ExplosionDef {\n readonly id: string\n /** Radius (m) inside which a body is torn apart, and the one inside which it is hurt. */\n readonly lethal: number\n readonly injury: number\n /** Radius of the fireball (`vis`). */\n readonly visualRadius: number\n readonly fireballs: number\n readonly fuel?: boolean\n readonly heat?: number\n readonly heatLifeScale?: number\n readonly smoke: { readonly color: number; readonly count: number; readonly life: number; readonly kind?: 0 | 3 }\n readonly dust: { readonly color: number; readonly count: number }\n readonly shrapnel: { readonly count: number; readonly kind: 0 | 1 | 2 | 3 | 4; readonly speed: number; readonly width?: number; readonly embers?: number }\n readonly chunks: { readonly count: number; readonly colors: readonly number[]; readonly plate?: boolean; readonly hot: number; readonly speed?: number; readonly size?: number }\n readonly mark: { readonly scorch?: number; readonly crater?: number; readonly dent?: number }\n readonly flash: { readonly light: number; readonly life?: number; readonly color?: number; readonly intensity?: number; readonly blinding?: boolean }\n readonly shock?: false | { readonly duration: number }\n readonly shake: number\n readonly push: number\n readonly mushroom?: MushroomSpec\n readonly residualFires?: readonly FireSpec[]\n /** Radius (m) in which characters catch fire. */\n readonly ignite?: number\n /** The pieces that fly are hot (the characters' parts burn). */\n readonly hotParts?: boolean\n readonly cone?: { readonly angle: number; readonly half: number; readonly length: number }\n readonly directionBias?: number\n readonly sources: readonly string[]\n}\n\n/** What a host does with what an effect cannot do by itself (break the windows, hurt the characters, dig the ground). */\nexport type VfxSideRequest =\n | { readonly type: 'crater'; readonly x: number; readonly z: number; readonly radius: number; readonly depth: number }\n | { readonly type: 'ignite'; readonly point: Vec3Like; readonly radius: number }\n | {\n readonly type: 'blast_front'\n readonly explosion: string\n readonly point: Vec3Like\n /** Where the glass breaks, damage and push reach: the Lab's `min(injury, 20) * .85`. */\n readonly glassRadius: number\n /** Metres per second the front travels (the speed of sound): the host delays what it breaks by `distance / speed`. */\n readonly speed: number\n readonly lethal: number\n readonly injury: number\n readonly push: number\n readonly hotParts: boolean\n readonly blinding: boolean\n readonly cone?: { readonly angle: number; readonly half: number; readonly length: number }\n }\n\nexport interface VfxProfile {\n readonly puffs: number\n readonly sparks: number\n readonly glows: number\n readonly chunks: number\n readonly sprites: { readonly add: number; readonly normal: number }\n readonly streaks: number\n readonly debris: number\n}\n\nexport type VfxProfileName = 'low' | 'medium' | 'high'\n\nexport interface LodBands {\n /** Distances (m) at which the scale drops. */\n readonly edges: readonly [number, number, number]\n /** The scale in each of the four bands. */\n readonly scales: readonly [number, number, number, number]\n /** How many visual radii are taken off the distance: a big blast stays detailed farther away. */\n readonly radiusFactor: number\n}\n\nexport interface AreaCap {\n readonly radius: number\n readonly window: number\n readonly count: number\n readonly excessScale: number\n}\n\nexport interface ScreenState {\n /** Camera shake amplitude (the Lab's `shakeAmp`). */\n shake: number\n /** White flash of the Lab, 0..1, and how fast it fades. */\n flash: number\n flashDecay: number\n white: number\n red: number\n directionLeft: number\n directionRight: number\n chroma: number\n desat: number\n gas: number\n beat: number\n}\n\nexport interface VfxStats {\n readonly puffs: number\n readonly sparks: number\n readonly glows: number\n readonly chunks: number\n readonly fires: number\n readonly shocks: number\n readonly rings: number\n /** Blast lights and fire lights that are lit. */\n readonly blastLights: number\n readonly fireLights: number\n /** What the last explosion made: puffs, sparks, chunks, lights. */\n readonly lastBlast: { readonly puffs: number; readonly sparks: number; readonly chunks: number; readonly lights: number }\n /** The scale the last explosion was made at (LOD times the area cap). */\n readonly lod: number\n /** Seconds since the last explosion, or `-1` before the first. */\n readonly blastTime: number\n}\n","import { AREA_CAP_GAME, createAreaCap } from './area-cap'\nimport { PROFILES } from './budget'\nimport { EXPLOSIONS, EXPLOSION_ALIASES } from './catalog'\nimport { hexToLinearRgb } from './color'\nimport { DEFAULT_GRAVITY, DEFAULT_WIND } from './constants'\nimport type { RandomMode, VfxCore } from './core'\nimport { addFire, createFirePool, updateFires } from './emitters/fire'\nimport type { Fire } from './emitters/fire'\nimport { GAME_EFFECTS } from './effects/catalog'\nimport { createEffectPlayer } from './effects/player'\nimport type { EffectPlayer, PlayOptions } from './effects/player'\nimport type { EffectDef } from './effects/types'\nimport { composeExplosion } from './explosion'\nimport type { GroundField } from './ground'\nimport { createLightPool, resetLightPool, updateLights } from './lights'\nimport type { LightPool } from './lights'\nimport { LOD_GAME } from './lod'\nimport { createMarkStore } from './marks'\nimport type { MarkEvent, MarkStore } from './marks'\nimport type { OrderedPool } from './pool'\nimport { updateChunks } from './pools/chunk-motion'\nimport { createChunkPool } from './pools/chunks'\nimport type { Chunk } from './pools/chunks'\nimport { createGlowPool, updateGlows } from './pools/glows'\nimport type { Glow } from './pools/glows'\nimport { createPuffPool, updatePuffs } from './pools/puffs'\nimport type { Puff, Ring } from './pools/puffs'\nimport { createRingPool, updateRings } from './pools/rings'\nimport { createShockPool, updateShocks } from './pools/shocks'\nimport type { Shock } from './pools/shocks'\nimport { createSparkPool, updateSparks } from './pools/sparks'\nimport type { Spark } from './pools/sparks'\nimport { lehmer, streamFor } from './random'\nimport type { MutableVec3, RandomStream } from './random'\nimport { createSchedule } from './schedule'\nimport { createScreenState, resetScreenState, updateScreen } from './screen'\nimport { SpriteKindSystem } from './sprites/system'\nimport type { LandListener } from './sprites/system'\nimport type { SpriteSpawnOptions } from './sprites/kind-pool'\nimport type { SpriteKind, SpriteKindDef } from './sprites/kinds'\nimport type { VfxTrace } from './trace'\nimport { WorkshopEngine } from './workshop/engine'\nimport type { Vec3Tuple } from './workshop/types'\nimport type { AreaCap, ExplosionDef, Follow, LodBands, ScreenState, VfxLayer, VfxProfile, VfxProfileName, VfxSideRequest, VfxStats, Vec3Like } from './types'\nimport { VFX_LAYERS } from './types'\n\nexport interface VfxOptions {\n readonly seed?: number\n /** `per_event` (default): every explosion draws from a stream of its own seed, so a LAN client repeats it from a few bytes. `shared`: one stream for everything, like the Lab page (the goldens). */\n readonly randomMode?: RandomMode\n readonly profile?: VfxProfileName | VfxProfile\n readonly lod?: LodBands\n /** The cap on explosions in one area; `false` turns it off. Default the game's: 6 in 4 s within 40 m, the extra ones at 0.3. */\n readonly areaCap?: AreaCap | false\n /** Height of the ground for what bounces. Default: the `ground` field, else flat at 0. */\n readonly groundAt?: (x: number, z: number) => number\n /** A deformable ground the craters dig into (see `createGroundField`). */\n readonly ground?: GroundField\n readonly wind?: Vec3Like\n readonly gravity?: number\n /** The catalogue by id (default the Lab's 16 and `generic`); build another with `patchExplosions(EXPLOSIONS, ...)`. */\n readonly explosions?: Readonly<Record<string, ExplosionDef>>\n readonly aliases?: Readonly<Record<string, string>>\n /** The Workshop's recipes by id for `play` (default `GAME_EFFECTS`; build another with `patchEffects`). */\n readonly effects?: Readonly<Record<string, EffectDef>>\n /** The sprites of fixed kinds (`burst`, `spawn`): pool size and count multiplier (default 2500 and 1; a `low` profile halves the pool and uses 0.6) and the kinds by id. */\n readonly sprites?: { readonly capacity?: number; readonly density?: number; readonly kinds?: Readonly<Record<string, SpriteKindDef>> }\n readonly reducedMotion?: boolean\n /** Hears everything that spawns, in order (the goldens use it). */\n readonly trace?: VfxTrace\n readonly onRequest?: (request: VfxSideRequest) => void\n}\n\nexport interface ExplosionOptions {\n readonly direction?: Vec3Like\n /** Repeats the same visual: used in `per_event` mode. */\n readonly seed?: number\n /** Multiplies the size of the effect (radius, marks, mushroom, fires). */\n readonly scale?: number\n /** Seconds from now until it goes off (a chain). */\n readonly delay?: number\n}\n\nexport interface FireOptions {\n readonly point: Vec3Like\n readonly radius: number\n readonly life: number\n /** Flames per second per square-ish metre (the Lab's `rate`, default 22) and smoke puffs (default .6). */\n readonly rate?: number\n readonly smoke?: number\n /** Grows from `startRadius` to `radius` over `grow` seconds. */\n readonly startRadius?: number\n readonly grow?: number\n /** `0xRRGGBB` of the smoke (default soot black). */\n readonly smokeColor?: number\n /** Moves with a thing (a burning part): the fire sits at its position plus `offset`. */\n readonly follow?: Follow\n readonly offset?: Vec3Like\n readonly seed?: number\n}\n\n/** The pools as the renderer reads them: live records, oldest first. */\nexport interface VfxPoolsView {\n readonly puffs: OrderedPool<Puff>\n readonly sparks: OrderedPool<Spark>\n readonly glows: OrderedPool<Glow>\n readonly chunks: OrderedPool<Chunk>\n readonly shocks: OrderedPool<Shock>\n readonly rings: OrderedPool<Ring>\n readonly lights: LightPool\n readonly marks: MarkStore\n readonly ground: GroundField | undefined\n}\n\nexport interface VfxWorld {\n /** Sets off an explosion of the catalogue by id (an explosive id or an alias); `null` when the id is unknown. */\n explosion(id: string, point: Vec3Like, options?: ExplosionOptions): number | null\n /** A lingering area fire (a molotov's pool, a burning part). Returns a handle for `stop`. */\n fire(options: FireOptions): number\n /**\n * Plays a recipe of the Workshop (a blood hit, a muzzle flash, a collapse) at `point`, facing `options.direction`, into the world's Workshop pools\n * (`workshop()`); `null` when the id is unknown. Returns a handle for `stop`.\n */\n play(id: string, point: Vec3Like, options?: PlayOptions): number | null\n /** The Workshop's pools (sprites, streaks, debris, decals, rings, lights, screen): what the renderer draws for `play`. Made on first use. */\n workshop(): WorkshopEngine\n /** The Workshop engine if something already made it (`play`, `workshop()`); never creates it. */\n peekWorkshop(): WorkshopEngine | undefined\n /** The ids `play` accepts. */\n effectIds(): readonly string[]\n /** `count` sprites of a kind leaving `point` around `normal` at about `speed` m/s (debris, sparks, smoke, steam, drips...). */\n burst(kind: SpriteKind | string, point: Vec3Like, normal: Vec3Like, count: number, speed?: number, options?: SpriteSpawnOptions): void\n /** One sprite of a kind with an explicit velocity. */\n spawn(kind: SpriteKind | string, point: Vec3Like, velocity: Vec3Like, options?: SpriteSpawnOptions): void\n /** Hears where the sprites of `owner` that land (drips spawned with `{ owner, tag }`) touch the ground. Returns the function that stops listening. */\n onLand(owner: string, listener: LandListener): () => void\n /** The sprite pools as the renderer reads them. */\n sprites(): SpriteKindSystem\n /** Pushes the smoke and dust within `radius` of `point` outwards, as fast as `speed` at the centre. */\n push(point: Vec3Like, radius: number, speed: number): void\n stop(handle: number): void\n setLayer(layer: VfxLayer, on: boolean): void\n layer(layer: VfxLayer): boolean\n setCamera(position: Vec3Like): void\n /** The wind that carries the smoke and dust, in m/s (`VfxOptions.wind` is where it starts). The weather moves it. */\n setWind(wind: Vec3Like): void\n wind(): Vec3Tuple\n /** Forces the scale of every explosion from now on (`null`: back to the distance bands and the cap). */\n forceLod(scale: number | null): void\n update(dt: number): void\n screen(): Readonly<ScreenState>\n stats(): VfxStats\n pools(): VfxPoolsView\n /** The live lingering fires, oldest first. */\n fires(): readonly Fire[]\n /** Hears every mark the moment it is made, and when they are cleared. Returns the function that stops listening. */\n onMarks(listener: (event: MarkEvent) => void): () => void\n /** Seconds the world has run since it was created or cleared. */\n time(): number\n /** The state of the shared stream (`randomMode: 'shared'`): what a golden compares after a run. */\n randomState(): number\n /** Takes everything away (pools, marks, ground, timers, lights, screen) and restarts the clock. */\n clear(): void\n /** The catalogue ids `explosion` accepts, aliases included. */\n catalog(): readonly string[]\n}\n\nconst FIRE_CAPACITY = 64\nconst RING_CAPACITY = 16\nconst SHOCK_CAPACITY = 16\nconst AUTO_SEED = 0x2545f491\n\nfunction resolveProfile(profile: VfxOptions['profile']): VfxProfile {\n if (profile === undefined) return PROFILES.medium\n return typeof profile === 'string' ? PROFILES[profile] : profile\n}\n\nfunction scaleDefinition(def: ExplosionDef, factor: number): ExplosionDef {\n if (factor === 1) return def\n const mushroom = def.mushroom && { ...def.mushroom, height: def.mushroom.height * factor, radius: def.mushroom.radius * factor }\n const residual = def.residualFires?.map((fire) => ({ ...fire, radius: fire.radius * factor, dx: (fire.dx ?? 0) * factor, dz: (fire.dz ?? 0) * factor }))\n return {\n ...def,\n lethal: def.lethal * factor,\n injury: def.injury * factor,\n visualRadius: def.visualRadius * factor,\n mark: { scorch: def.mark.scorch && def.mark.scorch * factor, crater: def.mark.crater && def.mark.crater * factor, dent: def.mark.dent && def.mark.dent * factor },\n ...(mushroom ? { mushroom } : {}),\n ...(residual ? { residualFires: residual } : {}),\n }\n}\n\nexport function createVfx(options: VfxOptions = {}): VfxWorld {\n const profile = resolveProfile(options.profile)\n const sharedRandom = lehmer(options.seed ?? 1)\n const explosions = options.explosions ?? EXPLOSIONS\n const aliases = options.aliases ?? EXPLOSION_ALIASES\n const randomMode = options.randomMode ?? 'per_event'\n const wind: MutableVec3 = { x: DEFAULT_WIND.x, y: DEFAULT_WIND.y, z: DEFAULT_WIND.z }\n if (options.wind) [wind.x, wind.y, wind.z] = options.wind\n const camera: MutableVec3 = { x: 0, y: 1.6, z: 0 }\n const layers = Object.fromEntries(VFX_LAYERS.map((layer) => [layer, true])) as Record<VfxLayer, boolean>\n const ground = options.ground\n const groundAt = options.groundAt ?? (ground ? (x: number, z: number) => ground.heightAt(x, z) : () => 0)\n const handles = { next: 0, events: 0 }\n const low = options.profile === 'low'\n const spriteSystem = new SpriteKindSystem({\n capacity: options.sprites?.capacity ?? (low ? 1250 : 2500),\n density: options.sprites?.density ?? (low ? 0.6 : 1),\n seed: 0x9e3779b1 + (options.seed ?? 0),\n groundAt,\n gravity: options.gravity ?? DEFAULT_GRAVITY,\n ...(options.sprites?.kinds ? { kinds: options.sprites.kinds } : {}),\n })\n const core: VfxCore = {\n profile,\n puffs: createPuffPool(profile.puffs),\n sparks: createSparkPool(profile.sparks),\n glows: createGlowPool(profile.glows),\n chunks: createChunkPool(profile.chunks, sharedRandom),\n fires: createFirePool(FIRE_CAPACITY, sharedRandom),\n shocks: createShockPool(SHOCK_CAPACITY),\n rings: createRingPool(RING_CAPACITY),\n lights: createLightPool(),\n marks: createMarkStore(),\n screen: createScreenState(),\n layers,\n schedule: createSchedule(),\n wind,\n camera,\n ground,\n bands: options.lod ?? LOD_GAME,\n areaCap: options.areaCap === false ? undefined : createAreaCap(options.areaCap ?? AREA_CAP_GAME),\n randomMode,\n sharedRandom,\n reducedMotion: options.reducedMotion ?? false,\n gravity: options.gravity ?? DEFAULT_GRAVITY,\n trace: options.trace,\n spawned: { puffs: 0, sparks: 0, chunks: 0, lights: 0 },\n clock: { time: 0, lastBlast: -1, lod: 1, forcedLod: null },\n groundAt,\n emit: (request) => options.onRequest?.(request),\n }\n\n function definitionOf(id: string): ExplosionDef | undefined {\n return explosions[id] ?? explosions[aliases[id] ?? '']\n }\n\n function streamOf(seed: number | undefined): RandomStream {\n if (randomMode === 'shared') return sharedRandom\n handles.events++\n return streamFor(seed ?? Math.imul(handles.events, AUTO_SEED) + (options.seed ?? 0))\n }\n\n function detonate(def: ExplosionDef, point: Vec3Like, call: { direction: MutableVec3 | undefined; random: RandomStream; handle: number }): void {\n composeExplosion(core, def, { x: point[0], y: point[1], z: point[2], direction: call.direction, random: call.random, handle: call.handle })\n }\n\n let engine: WorkshopEngine | undefined\n let player: EffectPlayer | undefined\n const effects = options.effects ?? GAME_EFFECTS\n const view = { get position(): Vec3Tuple { return [camera.x, camera.y, camera.z] }, forward: (local: readonly number[]): Vec3Tuple => [local[0] ?? 0, local[1] ?? 0, local[2] ?? 0] }\n\n function workshop(): WorkshopEngine {\n return (engine ??= new WorkshopEngine({ random: sharedRandom, profile, groundAt }))\n }\n\n function effectPlayer(): EffectPlayer {\n return (player ??= createEffectPlayer({ engine: workshop(), effects, view }))\n }\n\n return {\n play(id, point, playOptions = {}) {\n if (!(id in effects)) return null\n const seed = playOptions.seed ?? (randomMode === 'shared' ? undefined : Math.imul(++handles.events, AUTO_SEED) + (options.seed ?? 0))\n return effectPlayer().play(id, [point[0], point[1], point[2]], { ...playOptions, ...(seed !== undefined ? { seed } : {}) })\n },\n burst: (kind, point, normal, count, speed, spriteOptions) => spriteSystem.burst(kind, point, normal, count, speed, spriteOptions),\n spawn: (kind, point, velocity, spriteOptions) => spriteSystem.spawn(kind, point, velocity, spriteOptions),\n onLand: (owner, listener) => spriteSystem.onLand(owner, listener),\n sprites: () => spriteSystem,\n workshop,\n peekWorkshop: () => engine,\n effectIds: () => Object.keys(effects),\n explosion(id, point, explosionOptions = {}) {\n const base = definitionOf(id)\n if (!base) return null\n const def = scaleDefinition(base, explosionOptions.scale ?? 1)\n const handle = ++handles.next\n const direction = explosionOptions.direction && { x: explosionOptions.direction[0], y: explosionOptions.direction[1], z: explosionOptions.direction[2] }\n const random = streamOf(explosionOptions.seed)\n if (explosionOptions.delay) core.schedule.at(core.clock.time + explosionOptions.delay, () => detonate(def, point, { direction, random, handle }))\n else detonate(def, point, { direction, random, handle })\n return handle\n },\n fire(fireOptions) {\n const handle = ++handles.next\n const { point, offset, seed, radius, smokeColor, ...rest } = fireOptions\n addFire(core, streamOf(seed), {\n ...rest, r: radius, x: point[0], y: point[1], z: point[2], fx: offset?.[0] ?? 0, fy: offset?.[1] ?? 0, fz: offset?.[2] ?? 0,\n ...(smokeColor !== undefined ? { color: hexToLinearRgb(smokeColor) } : {}),\n }, handle)\n return handle\n },\n push(point, radius, speed) {\n spriteSystem.push(point, radius, speed)\n for (const puff of core.puffs.items) {\n const dx = puff.x - point[0]\n const dy = puff.y - point[1]\n const dz = puff.z - point[2]\n const distance = Math.sqrt(dx * dx + dy * dy + dz * dz)\n if (distance >= radius) continue\n const strength = (speed * (1 - distance / radius)) / (distance + 0.001)\n puff.vx += dx * strength\n puff.vy += dy * strength\n puff.vz += dz * strength\n }\n },\n stop(handle) {\n player?.stop(handle)\n const fires = core.fires\n for (let index = fires.items.length - 1; index >= 0; index--) if (fires.items[index]!.handle === handle) fires.removeAt(index)\n },\n setLayer(layer, on) {\n layers[layer] = on\n },\n layer: (layer) => layers[layer],\n setCamera(position) {\n camera.x = position[0]\n camera.y = position[1]\n camera.z = position[2]\n },\n setWind(next) {\n wind.x = next[0]\n wind.y = next[1]\n wind.z = next[2]\n },\n wind: () => [wind.x, wind.y, wind.z],\n forceLod(scale) {\n core.clock.forcedLod = scale\n },\n update(dt) {\n core.clock.time += dt\n core.schedule.update(core.clock.time)\n updateRings(core.rings, dt)\n updatePuffs(core.puffs, dt, wind)\n updateSparks(core.sparks, { dt, groundAt })\n updateGlows(core.glows, dt)\n updateShocks(core.shocks, core.puffs.items, dt)\n updateChunks(core, dt)\n updateFires(core, dt)\n updateLights(core.lights, { dt, flashLayer: layers.flash ? 1 : 0, clock: core.clock.time, fires: core.fires.items })\n updateScreen(core.screen, dt)\n spriteSystem.update(dt)\n if (engine) {\n engine.post = {}\n player?.update(dt)\n engine.update(dt)\n }\n },\n screen: () => core.screen,\n stats() {\n const lights = core.lights\n return {\n puffs: core.puffs.count,\n sparks: core.sparks.count,\n glows: core.glows.count,\n chunks: core.chunks.count,\n fires: core.fires.count,\n shocks: core.shocks.count,\n rings: core.rings.count,\n blastLights: lights.blast.filter((light) => light.intensity > 0.05).length,\n fireLights: lights.fire.filter((light) => light.intensity > 0.05).length,\n lastBlast: { ...core.spawned },\n lod: core.clock.lod,\n blastTime: core.clock.lastBlast < 0 ? -1 : core.clock.time - core.clock.lastBlast,\n }\n },\n pools: () => ({\n puffs: core.puffs, sparks: core.sparks, glows: core.glows, chunks: core.chunks, shocks: core.shocks, rings: core.rings, lights: core.lights, marks: core.marks, ground,\n }),\n fires: () => core.fires.items,\n onMarks: (listener) => core.marks.subscribe(listener),\n time: () => core.clock.time,\n randomState: () => sharedRandom.state,\n clear() {\n for (const pool of [core.puffs, core.sparks, core.glows, core.chunks, core.fires, core.shocks, core.rings] as OrderedPool<object>[]) pool.clear()\n core.marks.clear()\n core.schedule.clear()\n core.areaCap?.clear()\n ground?.reset()\n resetLightPool(core.lights)\n resetScreenState(core.screen)\n spriteSystem.clear()\n player?.clear()\n engine?.clear()\n core.clock.time = 0\n core.clock.lastBlast = -1\n core.clock.lod = 1\n core.spawned.puffs = core.spawned.sparks = core.spawned.chunks = core.spawned.lights = 0\n },\n catalog: () => [...new Set([...Object.keys(explosions), ...Object.keys(aliases)])],\n }\n}\n","{\n \"spall\": \"impact_{material}\",\n \"stage\": { \"1\": \"stage_crack\", \"2\": \"breach_dust\", \"3\": \"collapse_dust\" },\n \"expose\": \"rebar_expose\",\n \"collapse\": \"collapse_dust\",\n \"explode\": \"explosion\",\n \"fracture\": { \"glass\": \"glass_burst\" }\n}\n","import eventData from '../data/event-effects.json'\n\n/** What a game event looks like to the map: the kind of event and the little that picks its effect. */\nexport interface GameEvent {\n /** `spall`, `stage`, `expose`, `collapse`, `explode`, `fracture`... (strings, so there is no compile dependency on the destruction package). */\n readonly kind: string\n /** The stage of a `stage` event (1, 2 or 3). */\n readonly stage?: number\n /** The material of a `spall` or `fracture` event. */\n readonly material?: string\n}\n\ntype EventMap = Readonly<Record<string, string | Readonly<Record<string, string>>>>\n\nexport const EVENT_EFFECTS_MAP: EventMap = eventData\n\n/**\n * The effect id that plays for an event (`undefined` when the map has none). A string entry may use `{material}`; an object entry is chosen by the event's\n * stage or material. A game passes its own map to add or change entries.\n */\nexport function effectForEvent(event: GameEvent, map: EventMap = EVENT_EFFECTS_MAP): string | undefined {\n const entry = map[event.kind]\n if (entry === undefined) return undefined\n if (typeof entry === 'string') return event.material || !entry.includes('{material}') ? entry.replace('{material}', event.material ?? '') : undefined\n const key = event.stage !== undefined ? String(event.stage) : event.material\n return key === undefined ? undefined : entry[key]\n}\n","import type { EffectTexts } from '../locale'\n\n/** The Workshop's sheet for each effect, in English (the pt-BR texts are the page's own). */\nexport const en: Readonly<Record<string, EffectTexts>> = {\n blood_hit: {\n name: 'Blood by infection age',\n short: 'A mist on the way in, a jet on the way out, a pool that grows. Age changes colour and amount.',\n cost: '≤ 70 particles',\n costNote: '1 pool · 6 splashes',\n paramLabel: 'Age',\n when: 'Every bullet or blow that hits a zombie. The blood value comes from the bestiary age: 1.0 · 0.7 · 0.35 · 0.1.',\n look: 'Fresh sprays bright red and drips. Recent is burgundy. Old is brown and sparse. Decrepit does not bleed: it gives off black dust and dry flakes. The pool grows for 2 to 4 s and darkens in 60 s.',\n budget: 'Up to 70 particles per hit (Fresh), 1 pool and up to 6 splashes on the ground. Drops that land become decals.',\n network: 'Cosmetic. Goes in the impact event with 2 bits of age and the seed.',\n },\n headshot: {\n name: 'Headshot',\n short: 'The head bursts, the eyes go out on the same frame and the body drops.',\n cost: '≤ 70 particles',\n costNote: '8 debris · 1 pool',\n when: 'A head hit that kills (bestiary rule: a head kills at once).',\n look: 'A red mist around the head, a jet of drops backwards, 6 to 8 pieces of bone and skin, a swap to the burst-neck mesh. The eye glow goes out on the same frame, which is the most readable signal from afar.',\n budget: '20 mist + 35 drops + 8 debris + 1 pool. In the horde only the 12 closest get debris.',\n network: 'Cosmetic. The server already sends \"death by head\".',\n },\n dismember: {\n name: 'Dismemberment',\n short: 'The arm comes off whole with physics and the shoulder gushes for half a second.',\n cost: '≤ 60 particles',\n costNote: '1 limb · 4 splashes',\n when: 'Damage to a limb above the threshold (multipliers by region from the bestiary) or an axe blow.',\n look: 'The limb becomes a loose piece with physics and bounces. A short pulsing jet from the shoulder, drops that form a trail to the pool. The zombie staggers but keeps walking.',\n budget: '1 limb with physics (counts toward the cap of 300), a jet of up to 60 drops in 0.7 s.',\n network: 'The server sends which limb came off. The piece is local; each client simulates its own.',\n },\n bloater_cloud: {\n name: 'Bloater cloud',\n short: 'The belly pulses for 0.5 s, bursts and leaves 4 m of gas for 8 s.',\n cost: '≤ 180 particles',\n costNote: 'server entity',\n when: 'Death of the Bloater, by any cause.',\n look: 'A 0.5 s warning with the belly swelling, a burst with pieces and a greenish jet, and a low moss-green cloud that undulates on the ground for 8 s. Loose green glows inside the cloud mark the area.',\n budget: '40 large sprites at the burst and up to 60 alive in the cloud, at half resolution. Fade near the camera so it does not paint the whole screen.',\n network: 'Server entity (radius 4 m, 8 s): blinds for 5 s, doubles the scent trail and makes people cough. The client only draws.',\n },\n screamer_shriek: {\n name: 'Screamer shriek',\n short: 'A 1.5 s wind-up with steam, a wave on the ground and the horde\\'s eyes light up.',\n cost: '≤ 150 particles',\n costNote: '3 rings · aberration',\n when: 'The Screamer spots the player and starts the shriek (1.5 s wind-up, calls everyone within 150 m).',\n look: 'Head back and steam from the mouth during the wind-up. On the shriek, three pressure rings on the ground, dust raised in the radius, a short chromatic aberration on the screen of whoever is near, and the eyes of nearby zombies flare when they hear.',\n budget: '3 rings, up to 150 dust particles. The screen aberration is a 0.4 s post effect.',\n network: 'The server sends \"shriek at X\". The screen effect depends on local distance.',\n },\n eyes_in_fog: {\n name: 'Eyes in the fog',\n short: 'Eye glow that cuts through the fog; runners leave a red trail.',\n cost: '2 sprites/zombie',\n costNote: '0 lights',\n when: 'Always, on every zombie. It is the bestiary reading system: eye colour tells the category.',\n look: 'Two emissive points with bloom that ignore the fog. The body disappears in the mist at 25 m, the eyes stay readable at 60 m. Runners leave a short trail in the eye colour when they run.',\n budget: '2 sprites per zombie in the same instanced batch as the body. Trail only for runners closer than 30 m. No real light.',\n network: 'Nothing goes over the network: it comes from the zombie category.',\n },\n corpse_decay: {\n name: 'Corpse: darken and dissolve',\n short: '15 minutes in 8 seconds: fall, darken, flies and turn to dust.',\n cost: '≤ 40 particles',\n costNote: 'pool of 400 bodies',\n when: 'Zombie death. Bestiary timeline: ragdoll 3 s, frozen mesh, darken, dissolve between 14 and 15 min.',\n look: 'The skin loses colour until almost black, the pool dries to brown, flies appear from minute 5 and in the last minute the body sinks and gives off dark dust.',\n budget: 'Flies: 6 particles per body, only closer than 10 m. Dust: 40 particles once. The body uses the dissolve shader, no extra particle.',\n network: 'The server sends the time of death; each client works out the state.',\n },\n reanimation: {\n name: 'Reanimation',\n short: 'Veins light up, spasms, the eyes switch on and the body gets up.',\n cost: '≤ 30 particles',\n costNote: 'emissive in the shader',\n when: 'End of infection of a player or NPC: reanimates in 2 to 6 min as Fresh (70% Slow, 30% Runner).',\n look: 'Green pulses through the skin (emissive in the shader, not particles), spasms in the arms and head, the eyes light up from the inside out and the body stands. Breath as steam on the first shriek.',\n budget: 'Only animated emissive and 30 particles. It is rare and important, so it may be expensive.',\n network: 'The server sends \"reanimated\". For the friend who sees a teammate get up, it is the strongest moment of co-op.',\n },\n muzzle_flash: {\n name: 'Muzzle flash',\n short: 'Core, petals and smoke scaled by weapon. The Barrett raises dust.',\n cost: '≤ 60 particles',\n costNote: '1 light · 50 ms',\n paramLabel: 'Weapon',\n when: 'Every shot. In first person it uses the weapon model; from afar it is only the core sprite.',\n look: 'Three layers: a white-yellow core of 1 frame, petals along the barrel and grey smoke that stays 1 to 2 s. The 12-gauge has a wide flash and thick smoke. The Barrett vents two side jets through the muzzle brake and raises a ring of dust from the ground. The light illuminates the room for 50 ms (request from the map brief).',\n budget: 'Up to 60 particles and 1 light per shot. An AK burst uses the same light in a loop. A pistol has no light beyond 15 m.',\n network: 'Cosmetic. It comes from the shot event the server already sends (weapon, origin, direction).',\n },\n impact: {\n name: 'Impact by material',\n short: 'Concrete, metal, wood, glass, flesh and water: each with its own response.',\n cost: '≤ 40 particles',\n costNote: '1 hole',\n paramLabel: 'Material',\n when: 'Every bullet that hits something. The material comes from the mesh surface tag.',\n look: 'Concrete: pale dust in a cone and grey chips. Metal: a shower of bouncing sparks, no dust, with a short light. Wood: thin splinters and brown dust. Glass: glint, shards and a crack. Flesh: blood by age. Water: a crown of drops and two rings.',\n budget: '14 dust + 6 chips + up to 18 sparks + 1 hole. At 40 m only the dust remains.',\n network: '12-byte impact event with the material in 4 bits.',\n },\n penetration: {\n name: 'Bullet through the horde',\n short: 'Barrett tracer passing through 5 zombies, in 10x slow motion.',\n cost: '≤ 250 particles',\n costNote: 'aggregate event',\n when: 'Weapon with penetration: the AK goes through 2, the Remington 700 through 3, the Barrett through 6 and cars.',\n look: 'A tracer with a hot-air trail, blood exiting each body in the order the bullet passes (each in the colour of its age), bodies falling one after another and a final impact on the concrete.',\n budget: 'All hits of a shot go in a single aggregate event with the list of points. 5 bodies = 5 exits + 5 pools.',\n network: 'The server sends the shot and the ordered list of hits.',\n },\n casings_and_magazine: {\n name: 'Casings and magazine',\n short: 'Brass that bounces and stays on the ground, an empty magazine falling, smoke from the hot barrel.',\n cost: '200/player',\n costNote: 'physics 1.5 s',\n when: 'Every shot of a semi-automatic weapon, every reload, and the pump of the 12-gauge or the rifle bolt.',\n look: 'The casing leaves the ejection port spinning, bounces on the asphalt two or three times and stays. The empty magazine drops on reload. After a burst, thin smoke rises from the hot barrel for 2 s.',\n budget: 'Physics only for 1.5 s, then it becomes a static instance. Cap of 200 per player, the oldest goes first.',\n network: 'Cosmetic, each client generates its own. Ground position is not synchronised.',\n },\n molotov: {\n name: 'Molotov cocktail',\n short: 'A lit wick in the arc, glass breaking and a pool of fire that spreads.',\n cost: '≤ 300 particles',\n costNote: '2 lights · entity',\n when: 'A molotov hitting the ground or a zombie.',\n look: 'A bottle in an arc with the wick giving off flame and smoke. On impact, green shards and a splash of fire that opens from 0.5 m to 2.2 m in 0.8 s. Low flames, embers, dark smoke and a flickering light for 6 s, then embers and a burn mark.',\n budget: 'Area fire with up to 300 particles and 1 fire light. Burning zombies use a small emitter attached to the body (40 particles) and only the 8 closest.',\n network: 'Fire is a server entity: area, damage per second, time. The client draws from it.',\n },\n grenade: {\n name: 'Fragmentation grenade',\n short: 'A bounce on the asphalt, flash, shock wave, shrapnel and a ring of dust.',\n cost: '≤ 400 particles',\n costNote: 'aggregate event',\n when: 'A grenade exploding (1.8 s fuse after the throw).',\n look: 'The grenade bounces twice. A layered explosion: a white flash that paints the screen, a short fireball, a shock ring on the ground and another facing the camera, bright shrapnel that bounces, a ring of low dust, grey smoke that rises and a burn mark. Zombies in the radius fall outwards.',\n budget: 'Up to 400 particles, 1 strong light of 0.5 s, 16 debris, 1 mark. The 40 zombies hit go in a single aggregate event.',\n network: 'The server sends the explosion and the list of those hit. Visual is local.',\n },\n explosive_barrel: {\n name: 'Explosive barrel',\n short: 'A mushroom of fire, the lid flying, petrol burning on the ground and a column of smoke.',\n cost: '≤ 450 particles',\n costNote: '2 lights · fire entity',\n when: 'A red barrel with damage above the threshold, a shot or fire touching it.',\n look: 'Everything the grenade does, plus: a fireball that rises into a mushroom, the lid and red pieces flying high, three pools of petrol burning for 5 s and a black smoke column that serves as a landmark from afar.',\n budget: 'Peak of 450 particles. The smoke column stays visible beyond 80 m with 6 large sprites.',\n network: 'Explosion + a fire entity in the pools.',\n },\n shopfront_glass: {\n name: 'Shop window bursting',\n short: 'Two cracks, then the whole pane comes down in shards that stay on the pavement.',\n cost: '≤ 80 debris',\n costNote: '1 crack',\n when: 'Glass with accumulated damage (shot, blow, horde pushing). A 20 m noise that draws the horde.',\n look: 'The first blows open star cracks in the glass texture. At the burst the pane vanishes and becomes 60 to 80 shards that fall spinning, glinting in the light, and stay on the pavement. A veil of fine glass dust in front.',\n budget: 'Shards with physics count toward the cap of 300 and become static in 2 s. 3 crack stages in a single mask.',\n network: 'The server sends the glass state (0 to 3). Shards are local.',\n },\n door_stages: {\n name: 'Door giving way in 3 stages',\n short: 'Blows shake dust from the frame, a plank cracks and the door falls.',\n cost: '≤ 120 particles',\n costNote: '3 stages',\n when: 'The horde hitting a door or barricade (the Brute does 3x damage). Stages: 66%, 33%, 0% health.',\n look: 'Each blow shakes the door and loosens dust from the frame and splinters. At stage 2 the middle plank cracks and falls. At 3 the door topples and raises a cloud of low dust, and the zombie comes out of the dark.',\n budget: 'Dust: 12 particles per blow. Fall: 40 of dust and 6 debris. Blows beyond 15 m have no particles, only sound.',\n network: 'The server sends the door health. The visual stage comes from the value.',\n },\n building_collapse: {\n name: 'Collapse',\n short: 'Dust trickling from the awning, blocks falling and a cloud of dust rolling down the street.',\n cost: '≤ 350 particles',\n costNote: '26 debris',\n when: 'A structure that loses its health: explosion, vehicle impact, a big horde pushing (destruction of the map buildings).',\n look: 'Before: fine dust trickling and pebbles falling for 1 s, as a warning. Then: large and small blocks fall, the camera shakes and a cloud of grey-beige dust rolls along the ground and hangs in the air for 5 s, hiding zombies.',\n budget: '26 debris (6 large), 70 dust sprites at half resolution. A rare event, it fits the budget.',\n network: 'The server sends the collapse and the dust area, which blocks zombie sight for 4 s.',\n },\n block_wall: {\n name: 'Wall brought down with holes',\n short: 'A burst leaves holes and chips; the wall falls in blocks and the holes stay on them.',\n cost: '≤ 300 particles',\n costNote: '30 blocks · attached holes',\n when: 'A concrete wall, low wall or barricade that loses health from shots, explosion, the Brute or a car (map and arsenal destruction).',\n look: 'Each bullet opens a dark hole with a chipped rim, giving off dust and pebbles. When the wall gives way it splits into blocks that topple with physics and a cloud of dust rolls along the ground. The bullet holes are attached to the blocks: they stay on the fallen pieces, turned with them, and do not vanish when the wall breaks.',\n budget: 'Holes are decals that are children of the piece (not of the world), up to 12 per block and 128 in total. Blocks count toward the cap of 300 debris and become static at rest. Dust: 60 sprites at half resolution.',\n network: 'The server sends the wall health and the list of holes (2-byte local position per hole). Whoever joins later sees the same holes on the same pieces. The blocks fall using a common seed.',\n },\n metal_door: {\n name: 'Metal door denting',\n short: 'Each blow sinks the plate, flakes the paint and throws sparks; the hinge bursts.',\n cost: '≤ 160 particles',\n costNote: 'deformable mesh',\n when: 'A door, gate or steel plate taking a zombie blow (Brute 3x), a 12-gauge shot or a car hit.',\n look: 'The plate sinks where it is hit (a deformed mesh, not a model swap), with 3 visible levels. Each blow gives off sparks, flakes of green paint and rust dust. When the health runs out the top hinge bursts in sparks, the door topples and scrapes on the ground giving off more sparks.',\n budget: 'Per-vertex deformation only at the moment of the blow (CPU, 400 vertices). Per blow: 8 sparks, 5 flakes, 6 of rust. Fall: 30 sparks and 40 of dust.',\n network: 'The server keeps 3 bits of dent per door zone (9 zones). Whoever joins later sees the same dented door.',\n },\n iron_railing: {\n name: 'Iron railing bending',\n short: 'The horde pushes, the bars bow, the weld pops in a spark and the railing falls.',\n cost: '≤ 200 particles',\n costNote: '3 deformable bars',\n when: 'A railing, iron gate, handrail or chain-link fence with zombies pushing (horde pressure) or a car hitting it.',\n look: 'The bars bow gradually at the point of pressure and the vertical ones tilt with them. Rust falls in a thread during the strain. At two moments the weld pops with a crackle of sparks. At the end the middle post gives and the railing topples, leaving the bent iron on the ground.',\n budget: 'Curvature worked out only when the pressure changes, up to 180 vertices. Rust: 10 per second during the strain. Weld pop: 20 sparks.',\n network: 'The server sends the pressure (0 to 1) and when it gives. The curve comes from the value, the same on all clients.',\n },\n wooden_fence: {\n name: 'Wooden fence breaking',\n short: 'Planks snap one by one, chips and dust, and the section topples as the horde passes.',\n cost: '≤ 150 particles',\n costNote: '12 planks',\n when: 'A fence, yard gate or plank barricade taking a shot, blow or horde pressure.',\n look: 'Each plank snaps at a random point: the stump stays on the rail and the top piece flies off with chips and wood dust. The zombie appears through the gap. At the end the rail cracks and the whole section topples forward.',\n budget: 'Each plank is a piece: breaking swaps it for a stump + a piece with physics. Per break: 6 chips, 8 of dust. Topple: 35 of dust.',\n network: 'The server sends which planks broke (12 bits) and when the section falls.',\n },\n hit_point_breakage: {\n name: 'Breakage by the point hit',\n short: 'A shot to the leg, a machete on the corner, a bat on the top and a 12-gauge up close: each weapon breaks the part it hit.',\n cost: '≤ 120 particles',\n costNote: 'parts with their own health',\n when: 'Any destructible item. The damage goes to the part that was hit, not to the whole item.',\n look: 'A 9 mm shot to the front leg: the lower part of the leg breaks off and the table tips toward that corner. A machete on the corner of the top: the corner comes off whole in a straight cut, with chips in a line. A bat on the other corner: the top cracks and falls apart in pieces with dust. A 12-gauge up close on what is left: 9 grouped holes that stay in the wood.',\n budget: 'Each part has health and material. A common item: 4 to 12 parts. Particles only from the part hit, by its material.',\n network: 'The server sends which part lost health and how much. A loose part simulates on the client; the final position is sent once.',\n },\n pressure_cylinders: {\n name: 'Extinguisher and oxygen cylinder',\n short: 'A shot to the extinguisher makes a white cloud and a spin; on oxygen the valve breaks and the cylinder becomes a rocket.',\n cost: '≤ 250 particles',\n costNote: 'cloud entity',\n paramLabel: 'Cylinder',\n when: 'A shot, a strong blow or fire on an extinguisher, oxygen cylinder or welding cylinder (extinguisher and oxygen_cylinder keys of the map catalogue).',\n look: 'Extinguisher: the hole gives off a jet of white powder, the extinguisher falls, spins on the ground pushed by the jet and fills a 5 m area with a cloud that blocks sight for 6 s. Oxygen: the valve breaks with a crack, the jet becomes propulsion, the cylinder shoots along the ground scraping and throwing sparks, bounces and stops against a wall. With fire nearby, the oxygen blows up like the barrel.',\n budget: 'Jet: 60 particles per second for 3 s. Extinguisher cloud: up to 40 large sprites. Scraping sparks like the car.',\n network: 'The extinguisher cloud is a server entity (blocks zombie and player sight). The cylinder trajectory uses a common seed and the server corrects the final position.',\n },\n building_fire: {\n name: 'Building on fire',\n short: 'Fire coming out of the windows, licking the facade, embers and smoke with the wind.',\n cost: '≤ 600 particles',\n costNote: '2 lights · reference',\n when: 'A building fire (molotov, explosion, vehicle). It lasts minutes and spreads to the neighbour.',\n look: 'Windows lit orange that flicker, tongues of fire coming out of the top of each window and climbing the facade, embers carried by the wind and a thick slanted column of smoke. The light paints the street.',\n budget: 'Up to 600 particles per building and 2 fire lights. Beyond 80 m only the smoke column and the glow of the windows remain, which become a landmark.',\n network: 'A fire entity on the server per building, with a spread state.',\n },\n rain_and_puddles: {\n name: 'Rain, splashes and puddles',\n short: 'Drops with wind, splashes and rings only near the camera, lightning in the storm.',\n cost: '≤ 4,000 drops',\n costNote: 'splash up to 12 m',\n paramLabel: 'Intensity',\n when: 'The weather of the world. Rain raises the background noise and masks footsteps (it can be used for stealth).',\n look: 'A fine drizzle, almost vertical. Rain with splashes on the asphalt and rings in the puddles, which are dark and mirror-like. A storm with drops slanted by the wind, sheets of water passing and a double flash of lightning that lights the scene for 0.2 s and reveals the horde.',\n budget: 'Up to 4,000 drops tied to the camera in a 30 m box. Splashes and rings only within 12 m and only on what the camera sees.',\n network: 'The server sends the weather and the lightning seed, so everyone sees the same flash.',\n },\n fog_and_ash: {\n name: 'Fog and ash',\n short: 'Low layered mist, ash falling and the lamp post with its cone of light.',\n cost: '≤ 120 sprites',\n costNote: '1 cone of light',\n when: 'Before dawn, near the river and in burnt zones (ash). It cuts the sight of the zombies and of the player.',\n look: 'The engine fog plus a layer of low mist that moves slowly, fine ash falling in a spiral and the beam of the lamp posts in view. Zombie silhouettes show before their details.',\n budget: 'Up to 60 large mist sprites (half resolution) and 60 of ash. The lamp cone is an additive mesh, not real volumetric light.',\n network: 'The weather comes from the server. The density is local, by zone.',\n },\n scraping_sparks: {\n name: 'Scraping sparks',\n short: 'A bumper dragging on the asphalt: a river of bouncing sparks and a mark on the ground.',\n cost: '≤ 200 sparks/s',\n costNote: '1 light',\n when: 'A car part in contact with the ground or a wall (a fallen bumper, a rim without a tyre, a flipped car sliding).',\n look: 'A jet of sparks behind the movement, bouncing on the asphalt and dying from white to orange. A glow and a short light at the contact point. A dark scratched mark on the ground.',\n budget: 'Up to 200 sparks per second, 1 light. Beyond 40 m it is only the glow of the point.',\n network: 'Cosmetic, from the part state and speed that are already replicated.',\n },\n engine_smoke: {\n name: 'Engine smoke',\n short: 'White steam, grey smoke, then black with fire, depending on condition.',\n cost: '≤ 120 particles',\n costNote: 'continuous',\n paramLabel: 'Condition',\n when: 'Whenever the engine is on or hot, from the part condition (0 to 100) in the fleet brief.',\n look: '100: exhaust only. 60: a thread of white steam from the bonnet. 30: steady grey smoke and the odd spark. 10: thick black smoke, flames at the grille and fire light, a warning that it will explode.',\n budget: 'Continuous, so cheap: up to 40 particles per second and 1 light only in the critical state.',\n network: 'Comes from the engine condition, which is already replicated.',\n },\n fuel_leak: {\n name: 'Tank leak and fire',\n short: 'Dripping petrol, a pool, a spark and the fire running to the tank.',\n cost: '≤ 400 particles',\n costNote: 'fire entity',\n when: 'A tank punctured by a shot or a crash. A spark, a shot or nearby fire lights the pool.',\n look: 'Drops of petrol with an oily sheen forming a growing pool. A shot on the metal throws a spark, the fire lights on the pool, runs to the car and, if nobody puts it out, the car explodes.',\n budget: 'Drops: 12 per second. Pool fire like the molotov. The final explosion is the next effect.',\n network: 'The pool and the fire are server entities (they can burn zombies and players).',\n },\n car_explosion: {\n name: 'Car explosion',\n short: 'A fireball, the car jumping, the bonnet and doors flying, the shell burning.',\n cost: '≤ 600 particles',\n costNote: 'rare event',\n when: 'A tank on fire for more than 4 s, or damage above the threshold on a car with fuel.',\n look: 'A large explosion (grenade scale 1.8), the car jumps and lands crooked, the bonnet, doors and a wheel fly off, the glass bursts into shards. The bodywork goes black and the shell burns with black smoke for minutes.',\n budget: 'Peak of 600 particles and 34 debris. It is a rare event and may go over the per-event cap, never the global cap.',\n network: 'Explosion on the server. The car becomes a wreck (another prefab) for everyone.',\n },\n windshield: {\n name: 'Windshield: blood and cracks',\n short: 'View from inside the car: blows, cracks in 4 stages and a bloody hand.',\n cost: '0 particles',\n costNote: '4-stage mask',\n when: 'A zombie hitting the windshield or a side window (fleet brief: the glass takes 8 blows).',\n look: 'Each blow shakes the camera and adds a star crack. Stages 2 and 4 leave a blood stain with a handprint running down. At stage 4 the glass is white with cracks and the next blow breaks it.',\n budget: 'Texture only: a crack mask with 4 stages and a blood decal on the glass. Glass dust: 6 particles per blow.',\n network: 'The server sends the glass health. Blood on the glass is local.',\n },\n headlights_in_rain: {\n name: 'Headlights in the rain',\n short: 'A visible beam, drops lit inside the cone and the horde walking into the light.',\n cost: '≤ 600 lit drops',\n costNote: '2 spots, no shadow',\n when: 'Headlights on at night, with rain or fog.',\n look: 'Two cones of light in the air, rain drops that shine only inside the beam and zombie silhouettes that light up when they enter the light. Outside the cone the rain almost disappears.',\n budget: '2 spotlights without shadow, 2 additive cones and up to 600 extra drops only inside the cone. Headlights of cars beyond 40 m stay as a cone only, with no real light.',\n network: 'The state of the headlights is already replicated with the car.',\n },\n damage_and_bite: {\n name: 'Damage and bite',\n short: 'A red edge on the side of the blow, blood on the lens and the pulse of the bite.',\n cost: 'post-process',\n costNote: 'local only',\n when: 'The player takes a blow (with direction) or a bite (100% infection, characters rule).',\n look: 'Blow: a red edge on the side it came from, a short shake and splashes on the lens. Bite: a red flash over the whole screen, the colour drains for 2 s, a heartbeat pulsing in a dark tunnel and big blood on the lens that runs down.',\n budget: 'A post-process effect and one lens layer. No particles in the world besides the normal blood.',\n network: 'Only on the screen of whoever was hit. The others see the blood in the world.',\n },\n infection_phases: {\n name: 'Infection phases',\n short: 'Malaise, fever with a cold edge, delirium with ghosts and the end.',\n cost: 'post-process',\n costNote: 'local only',\n paramLabel: 'Phase',\n when: 'Phases from the characters brief: 2–12 h malaise, 12–30 h fever, 30–48 h delirium, 48 h death.',\n look: 'Malaise: slightly washed colour and a slowly breathing vignette. Fever: a bluish cold edge only for the infected, waves of blur, a cough that shakes the camera. Delirium: colour aberration, an oscillating hue and fake zombies flickering in the fog. 48 h: almost no colour, a green tone, a dark tunnel and a heartbeat.',\n budget: 'Post-process only. The delirium ghosts are 3 zombie instances without collision, only on the infected client.',\n network: 'Never goes over the network. The other player notices only the cough and the breathing (see \"Cough and breath\").',\n },\n panic: {\n name: 'Panic',\n short: 'The aim shakes, the tunnel closes as the horde arrives and the heart speeds up.',\n cost: 'post-process',\n costNote: 'local only',\n when: 'The characters\\' panic meter (rises with visible zombies, runners and a dead companion). Effects above 60 and above 90.',\n look: 'A fine, constant tremor in the camera (the aim shakes), a dark vignette that closes as the nearest zombie arrives and pulses with the heartbeat, which speeds up from 70 to 150 bpm. Colour loses strength.',\n budget: 'Post-process and camera noise only.',\n network: 'Local. Others\\' panic shows in their aim and in the scream (sound).',\n },\n bloater_blindness: {\n name: 'Bloater blindness',\n short: 'Green gas in the face: a blurred screen for 5 s and mucus on the lens.',\n cost: 'post + 30 particles',\n costNote: 'server entity',\n when: 'A player inside the Bloater cloud (radius 4 m) when it spawns.',\n look: 'A green cloud bursts between the zombie and the camera, the screen goes green and blurred and clears in 5 s, with drops of green mucus on the lens that run down. A cough shakes the camera twice.',\n budget: 'Post-process + 30 gas sprites near the camera, with a fade so particles do not cover the whole screen.',\n network: 'The blindness comes from the server entity; the visual is local.',\n },\n campfire: {\n name: 'Campsite fire',\n short: 'A low flame, embers spiralling up and a flickering warm light.',\n cost: '≤ 120 particles',\n costNote: '1 fire light',\n when: 'A campfire lit by the group or by NPCs. It warms, lights and draws zombies from afar (light).',\n look: 'Small low flames, embers that rise turning and fade, thin smoke and a warm light that flickers and paints the survivor.',\n budget: '120 particles and 1 fire light. Several campfires of the camp share the same light if they are closer than 6 m.',\n network: 'A replicated scene object (lit or out). Visual is local.',\n },\n cough_and_breath: {\n name: 'Cough, breath and flashlight',\n short: 'Breath vapour in the cold, the fever cough visible to others and the flashlight beam.',\n cost: '≤ 40 particles',\n costNote: '1 spot with shadow',\n when: 'A cold night: breath always. Infection fever: a cough every few seconds, audible at 10 m.',\n look: 'A short puff of vapour every 2.4 s. On a cough, three quick bigger puffs and the body jolts. The flashlight shows the beam in the fog and is the only light with a shadow.',\n budget: 'Vapour: 8 particles per puff, only closer than 15 m. Flashlight: 1 spot with a shadow per player, only the local player\\'s casts a shadow.',\n network: 'The cough is the social signal of the infection: the server sends the cough event (without saying it is infection) and the others see the vapour and hear it.',\n },\n}\n","import type { EffectTexts } from '../locale'\n\n/** The Workshop's sheet for each effect (W450-1262), as the page wrote it. */\nexport const ptBR: Readonly<Record<string, EffectTexts>> = {\n blood_hit: {\n name: 'Sangue por idade da infecção',\n short: 'Névoa na entrada, jato na saída, poça que cresce. A idade muda cor e quantidade.',\n cost: '≤ 70 part.',\n costNote: '1 poça · 6 respingos',\n paramLabel: 'Idade',\n when: 'Cada acerto de bala ou golpe em zumbi. O valor de sangue vem da idade do bestiário: 1,0 · 0,7 · 0,35 · 0,1.',\n look: 'Fresco espirra vermelho vivo e pinga. Recente sai bordô. Antigo sai marrom e pouco. Decrépito não sangra: solta pó preto e lascas secas. A poça cresce por 2 a 4 s e escurece em 60 s.',\n budget: 'Até 70 partículas por acerto (Fresco), 1 poça e até 6 respingos no chão. Gotas que caem viram decalque.',\n network: 'Cosmético. Vai no evento de impacto com 2 bits de idade e a semente.',\n },\n headshot: {\n name: 'Headshot',\n short: 'A cabeça estoura, os olhos apagam no mesmo quadro e o corpo cai.',\n cost: '≤ 70 part.',\n costNote: '8 detritos · 1 poça',\n when: 'Acerto na cabeça que mata (regra do bestiário: cabeça mata na hora).',\n look: 'Névoa vermelha em volta da cabeça, jato de gotas para trás, 6 a 8 pedaços de osso e pele, troca pela malha de pescoço estourado. O brilho dos olhos apaga no mesmo quadro, que é o sinal mais legível de longe.',\n budget: '20 de névoa + 35 gotas + 8 detritos + 1 poça. Na horda, só os 12 mais próximos ganham detritos.',\n network: 'Cosmético. O servidor já manda “morte por cabeça”.',\n },\n dismember: {\n name: 'Desmembramento',\n short: 'O braço sai inteiro com física e o ombro jorra por meio segundo.',\n cost: '≤ 60 part.',\n costNote: '1 membro · 4 respingos',\n when: 'Dano no membro acima do limite (multiplicadores por região do bestiário) ou golpe de machado.',\n look: 'O membro vira peça solta com física e quica. Jato curto do ombro em pulsos, gotas que formam um rastro até a poça. O zumbi cambaleia mas continua andando.',\n budget: '1 membro com física (conta no teto de 300), jato de até 60 gotas em 0,7 s.',\n network: 'O servidor manda qual membro saiu. A peça é local; cada cliente simula a sua.',\n },\n bloater_cloud: {\n name: 'Nuvem do Inchado',\n short: 'Barriga pulsa por 0,5 s, estoura e deixa 4 m de gás por 8 s.',\n cost: '≤ 180 part.',\n costNote: 'entidade no servidor',\n when: 'Morte do Inchado, por qualquer causa.',\n look: 'Aviso de 0,5 s com a barriga inchando, estouro com pedaços e jato esverdeado, e uma nuvem baixa verde-musgo que ondula no chão por 8 s. Brilhos verdes soltos dentro da nuvem marcam a área.',\n budget: '40 sprites grandes no estouro e até 60 vivos na nuvem, em meia resolução. Fade perto da câmera para não pintar a tela inteira.',\n network: 'Entidade no servidor (raio 4 m, 8 s): cega por 5 s, dobra o rastro de cheiro e faz tossir. O cliente só desenha.',\n },\n screamer_shriek: {\n name: 'Grito do Gritador',\n short: 'Preparo de 1,5 s com vapor, onda no chão e os olhos da horda acendem.',\n cost: '≤ 150 part.',\n costNote: '3 anéis · aberração',\n when: 'O Gritador avista o jogador e começa o grito (preparo de 1,5 s, chama todos num raio de 150 m).',\n look: 'Cabeça para trás e vapor saindo da boca durante o preparo. No grito, três anéis de pressão no chão, poeira levantada no raio, aberração cromática curta na tela de quem está perto e os olhos dos zumbis próximos piscam forte quando escutam.',\n budget: '3 anéis, até 150 partículas de poeira. A aberração na tela é um efeito de pós de 0,4 s.',\n network: 'O servidor manda “grito em X”. O efeito de tela depende da distância local.',\n },\n eyes_in_fog: {\n name: 'Olhos na neblina',\n short: 'Brilho dos olhos que fura a neblina; os corredores deixam rastro vermelho.',\n cost: '2 sprites/zumbi',\n costNote: '0 luzes',\n when: 'Sempre, em todo zumbi. É o sistema de leitura do bestiário: cor do olho diz a categoria.',\n look: 'Dois pontos emissivos com bloom que ignoram a neblina. O corpo some na névoa a 25 m, os olhos continuam legíveis a 60 m. Corredores deixam um rastro curto da cor do olho quando correm.',\n budget: '2 sprites por zumbi no mesmo lote instanciado do corpo. Rastro só para corredores a menos de 30 m. Nenhuma luz real.',\n network: 'Nada vai pela rede: sai da categoria do zumbi.',\n },\n corpse_decay: {\n name: 'Corpo: escurecer e dissolver',\n short: '15 minutos em 8 segundos: queda, escurece, moscas e vira pó.',\n cost: '≤ 40 part.',\n costNote: 'pool de 400 corpos',\n when: 'Morte do zumbi. Linha do tempo do bestiário: ragdoll 3 s, malha congelada, escurece, dissolve entre 14 e 15 min.',\n look: 'A pele perde a cor até ficar quase preta, a poça seca para marrom, aparecem moscas a partir do minuto 5 e no último minuto o corpo afunda e solta pó escuro.',\n budget: 'Moscas: 6 partículas por corpo, só a menos de 10 m. Pó: 40 partículas uma vez. O corpo usa o shader de dissolve, sem partícula extra.',\n network: 'O servidor manda a hora da morte; cada cliente calcula o estado.',\n },\n reanimation: {\n name: 'Reanimação',\n short: 'Veias acendem, espasmos, os olhos ligam e o corpo levanta.',\n cost: '≤ 30 part.',\n costNote: 'emissivo no shader',\n when: 'Fim da infecção de jogador ou NPC: reanima em 2 a 6 min como Fresco (70% Lento, 30% Corredor).',\n look: 'Pulsos verdes pela pele (emissivo no shader, não partícula), espasmos nos braços e na cabeça, os olhos acendem de dentro para fora e o corpo se levanta. Respiração em vapor no primeiro grito.',\n budget: 'Só emissivo animado e 30 partículas. É raro e importante, então pode ser caro.',\n network: 'O servidor manda “reanimou”. Para o amigo que vê o colega levantar, é o momento mais forte do coop.',\n },\n muzzle_flash: {\n name: 'Clarão de disparo',\n short: 'Núcleo, pétalas e fumaça com escala por arma. A Barrett levanta poeira.',\n cost: '≤ 60 part.',\n costNote: '1 luz · 50 ms',\n paramLabel: 'Arma',\n when: 'Cada disparo. Em primeira pessoa usa o modelo da arma; de longe vira só o sprite do núcleo.',\n look: 'Três camadas: núcleo branco-amarelo de 1 quadro, pétalas na direção do cano e fumaça cinza que fica 1 a 2 s. A 12 tem clarão largo e fumaça grossa. A Barrett solta dois jatos laterais pelo freio de boca e levanta um anel de poeira do chão. A luz ilumina o cômodo por 50 ms (pedido do brief do mapa).',\n budget: 'Até 60 partículas e 1 luz por tiro. Rajada de AK usa a mesma luz em loop. Pistola sem luz a mais de 15 m.',\n network: 'Cosmético. Sai do evento de tiro que o servidor já manda (arma, origem, direção).',\n },\n impact: {\n name: 'Impacto por material',\n short: 'Concreto, metal, madeira, vidro, carne e água: cada um com sua resposta.',\n cost: '≤ 40 part.',\n costNote: '1 furo',\n paramLabel: 'Material',\n when: 'Toda bala que acerta algo. O material vem da tag de superfície da malha.',\n look: 'Concreto: pó claro em cone e lascas cinza. Metal: chuva de faíscas que quicam, sem pó, com luz curta. Madeira: lascas finas e pó marrom. Vidro: brilho, cacos e rachadura. Carne: sangue pela idade. Água: coroa de gotas e dois anéis.',\n budget: '14 de pó + 6 lascas + até 18 faíscas + 1 furo. A 40 m sobra só o pó.',\n network: 'Evento de impacto de 12 bytes com material em 4 bits.',\n },\n penetration: {\n name: 'Bala que atravessa a horda',\n short: 'Traçante da Barrett passando por 5 zumbis, em câmera lenta 10×.',\n cost: '≤ 250 part.',\n costNote: 'evento agregado',\n when: 'Arma com penetração: AK atravessa 2, Remington 700 atravessa 3, Barrett atravessa 6 e carros.',\n look: 'Traçante com rastro de ar quente, saída de sangue em cada corpo na ordem em que a bala passa (cada um com a cor da sua idade), corpos caindo um atrás do outro e impacto final no concreto.',\n budget: 'Todos os acertos de um tiro saem num único evento agregado com a lista de pontos. 5 corpos = 5 saídas + 5 poças.',\n network: 'O servidor manda o tiro e a lista ordenada de acertos.',\n },\n casings_and_magazine: {\n name: 'Cápsulas e carregador',\n short: 'Latão que quica e fica no chão, carregador vazio caindo, fumaça do cano quente.',\n cost: '200/jogador',\n costNote: 'física 1,5 s',\n when: 'Cada disparo de arma semiautomática, recarga, e bombeada da 12 ou ferrolho do rifle.',\n look: 'Cápsula sai pela janela de ejeção girando, quica no asfalto duas ou três vezes e fica. O carregador vazio cai na recarga. Depois da rajada, fumaça fina sobe do cano quente por 2 s.',\n budget: 'Física só por 1,5 s, depois vira instância estática. Teto de 200 por jogador, o mais antigo some primeiro.',\n network: 'Cosmético, cada cliente gera as suas. Não sincroniza posição no chão.',\n },\n molotov: {\n name: 'Coquetel molotov',\n short: 'Pavio aceso no arco, vidro quebrando e poça de fogo que espalha.',\n cost: '≤ 300 part.',\n costNote: '2 luzes · entidade',\n when: 'Molotov atingindo o chão ou um zumbi.',\n look: 'Garrafa em arco com pavio soltando chama e fumaça. Ao bater, cacos verdes e um respingo de fogo que abre de 0,5 m para 2,2 m em 0,8 s. Chamas baixas, brasas, fumaça escura e luz tremendo por 6 s, depois brasa e mancha queimada.',\n budget: 'Fogo de área com até 300 partículas e 1 luz de fogo. Zumbis em chamas usam um emissor pequeno preso ao corpo (40 partículas) e só os 8 mais próximos.',\n network: 'Fogo é entidade no servidor: área, dano por segundo, tempo. O cliente desenha a partir dela.',\n },\n grenade: {\n name: 'Granada de fragmentação',\n short: 'Quique no asfalto, clarão, onda de choque, estilhaços e anel de poeira.',\n cost: '≤ 400 part.',\n costNote: 'evento agregado',\n when: 'Granada explodindo (pavio de 1,8 s após o arremesso).',\n look: 'Granada quica duas vezes. Explosão em camadas: clarão branco que pinta a tela, bola de fogo curta, anel de choque no chão e outro de frente para a câmera, estilhaços brilhantes que quicam, anel de poeira rasteira, fumaça cinza que sobe e marca queimada. Zumbis no raio caem para fora.',\n budget: 'Até 400 partículas, 1 luz forte de 0,5 s, 16 detritos, 1 marca. Os 40 zumbis atingidos entram num único evento agregado.',\n network: 'O servidor manda a explosão e a lista de atingidos. Visual local.',\n },\n explosive_barrel: {\n name: 'Barril explosivo',\n short: 'Cogumelo de fogo, tampa voando, gasolina queimando no chão e coluna de fumaça.',\n cost: '≤ 450 part.',\n costNote: '2 luzes · entidade de fogo',\n when: 'Barril vermelho com dano acima do limite, tiro ou fogo encostado.',\n look: 'Tudo da granada, mais: bola de fogo que sobe em cogumelo, tampa e pedaços vermelhos voando alto, três poças de gasolina queimando por 5 s e coluna de fumaça preta que serve de referência de longe.',\n budget: 'Pico de 450 partículas. A coluna de fumaça fica visível além de 80 m com 6 sprites grandes.',\n network: 'Explosão + entidade de fogo nas poças.',\n },\n shopfront_glass: {\n name: 'Vitrine estourando',\n short: 'Duas trincas, depois a placa inteira desce em cacos que ficam na calçada.',\n cost: '≤ 80 detritos',\n costNote: '1 rachadura',\n when: 'Vidro com dano acumulado (tiro, golpe, horda empurrando). Barulho de 20 m que atrai a horda.',\n look: 'Primeiros golpes abrem trincas em estrela na textura do vidro. No estouro a placa some e vira 60 a 80 cacos que caem girando, brilhando na luz, e ficam na calçada. Um véu de pó de vidro fino na frente.',\n budget: 'Cacos com física contam no teto de 300 e viram estáticos em 2 s. 3 estágios de rachadura numa máscara só.',\n network: 'O servidor manda o estado do vidro (0 a 3). Cacos são locais.',\n },\n door_stages: {\n name: 'Porta cedendo em 3 estágios',\n short: 'Pancadas soltam pó do batente, uma tábua racha e a porta cai.',\n cost: '≤ 120 part.',\n costNote: '3 estágios',\n when: 'Horda batendo em porta ou barricada (o Brutamonte faz 3× dano). Estágios: 66%, 33%, 0% de vida.',\n look: 'Cada pancada sacode a porta e solta pó do batente e lascas. No estágio 2 a tábua do meio racha e cai. No 3 a porta tomba e levanta uma nuvem de poeira rasteira, e o zumbi sai do escuro.',\n budget: 'Pó: 12 partículas por pancada. Queda: 40 de poeira e 6 detritos. Pancadas a mais de 15 m sem partícula, só som.',\n network: 'O servidor manda a vida da porta. Estágio visual sai do valor.',\n },\n building_collapse: {\n name: 'Desabamento',\n short: 'Pó escorrendo da marquise, blocos caindo e nuvem de poeira que rola pela rua.',\n cost: '≤ 350 part.',\n costNote: '26 detritos',\n when: 'Estrutura que perde a vida: explosão, veículo batendo, horda grande empurrando (destruição de prédios do mapa).',\n look: 'Antes: pó fino escorrendo e pedrinhas caindo por 1 s, como aviso. Depois: blocos grandes e pequenos caem, a câmera treme e uma nuvem de poeira cinza-bege rola pelo chão e fica no ar por 5 s, escondendo zumbis.',\n budget: '26 detritos (6 grandes), 70 sprites de poeira em meia resolução. É evento raro, cabe no orçamento.',\n network: 'O servidor manda o colapso e a área de poeira, que bloqueia visão de zumbi por 4 s.',\n },\n block_wall: {\n name: 'Parede derrubada com furos',\n short: 'Rajada deixa furos e lascas; a parede cai em blocos e os furos continuam neles.',\n cost: '≤ 300 part.',\n costNote: '30 blocos · furos presos',\n when: 'Parede, mureta ou barricada de concreto que perde a vida por tiros, explosão, Brutamonte ou carro (destruição do mapa e do arsenal).',\n look: 'Cada bala abre um furo escuro com borda lascada, solta pó e pedrinhas. Quando a parede cede, ela se parte em blocos que tombam com física e uma nuvem de poeira rola pelo chão. Os furos de bala estão presos aos blocos: ficam nos pedaços caídos, girados junto com eles, e não somem quando a parede quebra.',\n budget: 'Furos são decalques filhos do pedaço (não do mundo), até 12 por bloco e 128 no total. Blocos contam no teto de 300 detritos e viram estáticos em repouso. Poeira: 60 sprites em meia resolução.',\n network: 'O servidor manda vida da parede e a lista de furos (posição local de 2 bytes por furo). Quem entra depois vê os mesmos furos nos mesmos pedaços. Queda dos blocos usa semente comum.',\n },\n metal_door: {\n name: 'Porta de metal amassando',\n short: 'Cada pancada afunda a chapa, descasca a tinta e solta faísca; a dobradiça arrebenta.',\n cost: '≤ 160 part.',\n costNote: 'malha deformável',\n when: 'Porta, portão ou chapa de aço levando golpe de zumbi (Brutamonte 3×), tiro de 12 ou batida de carro.',\n look: 'A chapa afunda onde apanha (malha deformada, não troca de modelo), com 3 níveis visíveis. Cada pancada solta faísca, lasca de tinta verde e pó de ferrugem. Quando a vida acaba, a dobradiça de cima estoura em faíscas, a porta tomba e raspa no chão soltando mais faísca.',\n budget: 'Deformação por vértice só na hora do golpe (CPU, 400 vértices). Por golpe: 8 faíscas, 5 lascas, 6 de ferrugem. Queda: 30 faíscas e 40 de poeira.',\n network: 'O servidor guarda 3 bits de amassado por zona da porta (9 zonas). Quem entra depois vê a mesma porta amassada.',\n },\n iron_railing: {\n name: 'Grade de ferro entortando',\n short: 'A horda empurra, as barras envergam, a solda estoura em faísca e a grade tomba.',\n cost: '≤ 200 part.',\n costNote: '3 barras deformáveis',\n when: 'Grade, portão de ferro, corrimão ou alambrado com zumbis empurrando (pressão da horda) ou carro batendo.',\n look: 'As barras envergam aos poucos no ponto de pressão e as verticais inclinam junto. Ferrugem cai em fio durante o esforço. Em dois momentos a solda estoura com um estalo de faíscas. No fim o poste do meio cede e a grade tomba, deixando o ferro torto no chão.',\n budget: 'Curvatura calculada só quando a pressão muda, até 180 vértices. Ferrugem: 10 por segundo durante o esforço. Estouro de solda: 20 faíscas.',\n network: 'O servidor manda a pressão (0 a 1) e quando cede. A curva sai do valor, igual em todos os clientes.',\n },\n wooden_fence: {\n name: 'Cerca de madeira quebrando',\n short: 'Tábuas partem uma a uma, lascas e pó, e a seção tomba com a horda passando.',\n cost: '≤ 150 part.',\n costNote: '12 tábuas',\n when: 'Cerca, portão de quintal ou barricada de tábuas recebendo tiro, golpe ou pressão da horda.',\n look: 'Cada tábua parte num ponto aleatório: o toco fica preso no trilho e o pedaço de cima voa com lascas e pó de madeira. O zumbi aparece pelo vão. No fim o trilho racha e a seção inteira tomba para frente.',\n budget: 'Cada tábua é uma peça: quebra troca para toco + pedaço com física. Por quebra: 6 lascas, 8 de pó. Tombo: 35 de poeira.',\n network: 'O servidor manda quais tábuas quebraram (12 bits) e quando a seção cai.',\n },\n hit_point_breakage: {\n name: 'Quebra pelo ponto atingido',\n short: 'Tiro na perna, facão na quina, taco no tampo e 12 de perto: cada arma quebra a parte que pegou.',\n cost: '≤ 120 part.',\n costNote: 'peças com vida própria',\n when: 'Qualquer item destrutível. O dano vai para a peça que foi atingida, não para o item inteiro.',\n look: 'Tiro de 9 mm na perna da frente: a parte de baixo da perna parte e a mesa tomba para aquele canto. Facão na quina do tampo: a quina sai inteira num corte reto, com lascas em linha. Taco no outro canto: o tampo racha e se desfaz em pedaços com pó. 12 de perto no que sobrou: 9 furos agrupados que ficam na madeira.',\n budget: 'Cada peça tem vida e material. Item comum: 4 a 12 peças. Partículas só da peça atingida, pelo material dela.',\n network: 'O servidor manda qual peça perdeu vida e quanto. Peça solta simula no cliente; a posição final é mandada uma vez.',\n },\n pressure_cylinders: {\n name: 'Extintor e cilindro de oxigênio',\n short: 'Tiro no extintor faz nuvem branca e giro; no oxigênio a válvula quebra e o cilindro vira foguete.',\n cost: '≤ 250 part.',\n costNote: 'entidade de nuvem',\n paramLabel: 'Cilindro',\n when: 'Tiro, golpe forte ou fogo em extintor, cilindro de oxigênio ou de solda (chaves extintor e cilindro_oxigenio do catálogo do mapa).',\n look: 'Extintor: o furo solta um jato de pó branco, o extintor cai, gira no chão empurrado pelo jato e enche uma área de 5 m de nuvem que tapa a visão por 6 s. Oxigênio: a válvula quebra com estalo, o jato vira propulsão, o cilindro dispara rente ao chão raspando e soltando faísca, quica e para numa parede. Com fogo por perto, o oxigênio estoura como o barril.',\n budget: 'Jato: 60 partículas por segundo por 3 s. Nuvem do extintor: até 40 sprites grandes. Faísca de raspagem como a do carro.',\n network: 'Nuvem do extintor é entidade no servidor (bloqueia visão de zumbi e de jogador). A trajetória do cilindro usa semente comum e o servidor corrige a posição final.',\n },\n building_fire: {\n name: 'Prédio em chamas',\n short: 'Fogo saindo pelas janelas, lambendo a fachada, brasas e fumaça com vento.',\n cost: '≤ 600 part.',\n costNote: '2 luzes · referência',\n when: 'Incêndio de prédio (molotov, explosão, veículo). Dura minutos e espalha para o vizinho.',\n look: 'Janelas acesas em laranja que tremem, línguas de fogo saindo pela parte de cima de cada janela e subindo pela fachada, brasas levadas pelo vento e uma coluna de fumaça grossa inclinada. A luz pinta a rua.',\n budget: 'Até 600 partículas por prédio e 2 luzes de fogo. Além de 80 m fica só a coluna de fumaça e o brilho das janelas, que viram ponto de referência.',\n network: 'Entidade de fogo no servidor por prédio, com estado de espalhar.',\n },\n rain_and_puddles: {\n name: 'Chuva, respingos e poças',\n short: 'Gotas com vento, respingos e anéis só perto da câmera, relâmpago no temporal.',\n cost: '≤ 4.000 gotas',\n costNote: 'respingo até 12 m',\n paramLabel: 'Intensidade',\n when: 'Clima do mundo. Chuva aumenta o ruído de fundo e mascara passos (dá para usar na furtividade).',\n look: 'Garoa fina quase vertical. Chuva com respingos no asfalto e anéis nas poças, que ficam escuras e espelhadas. Temporal com gotas inclinadas pelo vento, cortinas de água passando e relâmpago duplo que clareia a cena por 0,2 s e revela a horda.',\n budget: 'Até 4.000 gotas presas à câmera numa caixa de 30 m. Respingos e anéis só a menos de 12 m e só em cima do que a câmera vê.',\n network: 'O servidor manda o clima e a semente do relâmpago, para todos verem o mesmo clarão.',\n },\n fog_and_ash: {\n name: 'Neblina e cinzas',\n short: 'Névoa rasteira em camadas, cinza caindo e o poste com cone de luz.',\n cost: '≤ 120 sprites',\n costNote: '1 cone de luz',\n when: 'Madrugada, perto de rio e em zonas queimadas (cinza). Reduz a visão dos zumbis e do jogador.',\n look: 'Neblina do motor mais uma camada de névoa baixa que se move devagar, cinza fina caindo em espiral e o feixe dos postes visível. Silhuetas de zumbis aparecem antes dos detalhes.',\n budget: 'Até 60 sprites grandes de névoa (meia resolução) e 60 de cinza. O cone do poste é uma malha aditiva, não luz volumétrica real.',\n network: 'Clima vem do servidor. Densidade é local por zona.',\n },\n scraping_sparks: {\n name: 'Faíscas de raspagem',\n short: 'Para-choque arrastando no asfalto: rio de faíscas que quicam e marca no chão.',\n cost: '≤ 200 faíscas/s',\n costNote: '1 luz',\n when: 'Peça de carro em contato com o chão ou com parede (para-choque caído, roda sem pneu, carro capotado deslizando).',\n look: 'Jato de faíscas para trás do movimento, que quicam no asfalto e apagam do branco para o laranja. Brilho e luz curta no ponto de contato. Marca escura riscada no chão.',\n budget: 'Até 200 faíscas por segundo, 1 luz. A mais de 40 m vira só o brilho do ponto.',\n network: 'Cosmético, a partir do estado da peça e da velocidade que já são replicados.',\n },\n engine_smoke: {\n name: 'Fumaça do motor',\n short: 'Vapor branco, fumaça cinza, depois preta com fogo, conforme a condição.',\n cost: '≤ 120 part.',\n costNote: 'contínuo',\n paramLabel: 'Condição',\n when: 'Sempre que o motor está ligado ou quente, a partir da condição da peça (0 a 100) do brief da frota.',\n look: '100: só escapamento. 60: fio de vapor branco saindo pelo capô. 30: fumaça cinza constante e faísca de vez em quando. 10: fumaça preta grossa, chamas na grade e luz de fogo, aviso de que vai explodir.',\n budget: 'Contínuo, então barato: até 40 partículas por segundo e 1 luz só no estado crítico.',\n network: 'Sai da condição do motor, que já é replicada.',\n },\n fuel_leak: {\n name: 'Vazamento e incêndio do tanque',\n short: 'Gasolina pingando, poça, uma faísca e o fogo correndo até o tanque.',\n cost: '≤ 400 part.',\n costNote: 'entidade de fogo',\n when: 'Tanque furado por tiro ou batida. Faísca, tiro ou fogo próximo acende a poça.',\n look: 'Pingos de gasolina com brilho oleoso formando uma poça que cresce. Um tiro no metal solta faísca, o fogo acende na poça, corre até o carro e, se ninguém apagar, o carro explode.',\n budget: 'Pingos: 12 por segundo. Fogo de poça como o do molotov. A explosão final é o efeito seguinte.',\n network: 'Poça e fogo são entidades no servidor (podem queimar zumbis e jogadores).',\n },\n car_explosion: {\n name: 'Explosão do carro',\n short: 'Bola de fogo, carro pulando, capô e portas voando, carcaça queimando.',\n cost: '≤ 600 part.',\n costNote: 'evento raro',\n when: 'Tanque em chamas por mais de 4 s, ou dano acima do limite em carro com combustível.',\n look: 'Explosão grande (escala 1,8 da granada), o carro pula e cai torto, capô, portas e uma roda saem voando, vidro estoura em cacos. A lataria fica preta e a carcaça queima com fumaça preta por minutos.',\n budget: 'Pico de 600 partículas e 34 detritos. É evento raro e pode passar do teto por evento, nunca do teto global.',\n network: 'Explosão no servidor. O carro vira carcaça (outro prefab) para todos.',\n },\n windshield: {\n name: 'Para-brisa: sangue e rachadura',\n short: 'Visão de dentro do carro: pancadas, trincas em 4 estágios e mão de sangue.',\n cost: '0 partículas',\n costNote: 'máscara 4 estágios',\n when: 'Zumbi batendo no para-brisa ou no vidro lateral (brief da frota: vidro aguenta 8 golpes).',\n look: 'Cada golpe treme a câmera e soma uma trinca em estrela. Estágio 2 e 4 deixam mancha de sangue com marca de mão escorrendo. No estágio 4 o vidro está branco de rachadura e o próximo golpe quebra.',\n budget: 'Só textura: uma máscara de rachadura com 4 estágios e um decal de sangue no vidro. Pó de vidro: 6 partículas por golpe.',\n network: 'O servidor manda a vida do vidro. Sangue no vidro é local.',\n },\n headlights_in_rain: {\n name: 'Faróis na chuva',\n short: 'Feixe visível, gotas acesas dentro do cone e a horda entrando na luz.',\n cost: '≤ 600 gotas acesas',\n costNote: '2 spots sem sombra',\n when: 'Faróis ligados à noite, com chuva ou neblina.',\n look: 'Dois cones de luz visíveis no ar, gotas de chuva que brilham só dentro do feixe e silhuetas de zumbi que acendem quando entram na luz. Fora do cone, a chuva quase some.',\n budget: '2 spotlights sem sombra, 2 cones aditivos e até 600 gotas extras só dentro do cone. Faróis de carros a mais de 40 m ficam só como cone, sem luz real.',\n network: 'Estado do farol já é replicado com o carro.',\n },\n damage_and_bite: {\n name: 'Dano e mordida',\n short: 'Borda vermelha do lado do golpe, sangue na lente e o pulso da mordida.',\n cost: 'pós-processo',\n costNote: 'só local',\n when: 'Jogador recebe golpe (com direção) ou mordida (infecção 100%, regra dos personagens).',\n look: 'Golpe: borda vermelha do lado de onde veio, tremor curto e respingos na lente. Mordida: clarão vermelho na tela inteira, a cor some por 2 s, batimento pulsando no túnel escuro e sangue grande na lente que escorre.',\n budget: 'Efeito de pós-processo e uma camada de lente. Nenhuma partícula no mundo além do sangue normal.',\n network: 'Só na tela de quem levou. Os outros veem o sangue no mundo.',\n },\n infection_phases: {\n name: 'Fases da infecção',\n short: 'Mal-estar, febre com borda fria, delírio com fantasmas e o fim.',\n cost: 'pós-processo',\n costNote: 'só local',\n paramLabel: 'Fase',\n when: 'Fases do brief de personagens: 2–12 h mal-estar, 12–30 h febre, 30–48 h delírio, 48 h morte.',\n look: 'Mal-estar: cor um pouco lavada e vinheta que respira devagar. Febre: borda fria azulada só para o infectado, desfoque em ondas, tosse que treme a câmera. Delírio: aberração de cor, matiz que oscila e zumbis falsos que piscam na neblina. 48 h: quase sem cor, tom verde, túnel escuro e batimento.',\n budget: 'Só pós-processo. Os fantasmas do delírio são 3 instâncias do zumbi sem colisão, só no cliente infectado.',\n network: 'Nunca vai pela rede. O outro jogador percebe só a tosse e a respiração (ver “Tosse e respiração”).',\n },\n panic: {\n name: 'Pânico',\n short: 'A mira treme, o túnel fecha conforme a horda chega e o coração acelera.',\n cost: 'pós-processo',\n costNote: 'só local',\n when: 'Medidor de pânico dos personagens (sobe com zumbis visíveis, corredores e companheiro morto). Efeitos acima de 60 e de 90.',\n look: 'Tremor fino e constante na câmera (a mira treme), vinheta escura que fecha conforme o zumbi mais próximo chega e pulsa com o batimento, que acelera de 70 para 150 bpm. A cor perde força.',\n budget: 'Só pós-processo e ruído de câmera.',\n network: 'Local. O pânico dos outros aparece na mira deles e no grito (som).',\n },\n bloater_blindness: {\n name: 'Cegueira do Inchado',\n short: 'Gás verde na cara: tela turva por 5 s e muco na lente.',\n cost: 'pós + 30 part.',\n costNote: 'entidade do servidor',\n when: 'Jogador dentro da nuvem do Inchado (raio 4 m) quando ela nasce.',\n look: 'Nuvem verde explode entre o zumbi e a câmera, a tela fica verde e desfocada e clareia em 5 s, com gotas de muco verde na lente que escorrem. Tosse treme a câmera duas vezes.',\n budget: 'Pós-processo + 30 sprites de gás perto da câmera, com fade para não cobrir a tela inteira de partícula.',\n network: 'A cegueira vem da entidade no servidor; o visual é local.',\n },\n campfire: {\n name: 'Fogueira do acampamento',\n short: 'Chama baixa, brasas subindo em espiral e luz quente tremendo.',\n cost: '≤ 120 part.',\n costNote: '1 luz de fogo',\n when: 'Fogueira acesa pelo grupo ou por NPCs. Esquenta, ilumina e atrai zumbis de longe (luz).',\n look: 'Chamas pequenas e baixas, brasas que sobem girando e somem, fumaça fina e uma luz quente que treme e pinta o sobrevivente.',\n budget: '120 partículas e 1 luz de fogo. Várias fogueiras do acampamento dividem a mesma luz se estiverem a menos de 6 m.',\n network: 'Objeto de cena replicado (aceso ou apagado). Visual local.',\n },\n cough_and_breath: {\n name: 'Tosse, respiração e lanterna',\n short: 'Vapor da respiração no frio, tosse da febre visível para os outros e feixe da lanterna.',\n cost: '≤ 40 part.',\n costNote: '1 spot com sombra',\n when: 'Noite fria: respiração sempre. Febre da infecção: tosse a cada poucos segundos, audível a 10 m.',\n look: 'Sopro curto de vapor a cada 2,4 s. Na tosse, três sopros rápidos e maiores e o corpo dá um tranco. A lanterna mostra o feixe na neblina e é a única luz com sombra.',\n budget: 'Vapor: 8 partículas por sopro, só a menos de 15 m. Lanterna: 1 spot com sombra por jogador, só a do jogador local projeta sombra.',\n network: 'A tosse é o sinal social da infecção: o servidor manda o evento de tosse (sem dizer que é infecção) e os outros veem o vapor e ouvem.',\n },\n}\n","import { en } from './locales/en'\nimport { ptBR } from './locales/pt-BR'\n\n/** Locales shipped with the package. Unknown locales fall back to `en`. */\nexport type VfxLocale = 'en' | 'pt-BR'\n\n/** The texts of one Workshop effect: its card in the list and its sheet (when, how it looks, what it costs, what goes over the network). */\nexport interface EffectTexts {\n readonly name: string\n readonly short: string\n /** The cost shown on the card, and what comes with it. */\n readonly cost: string\n readonly costNote: string\n /** The label of the effect's parameter, when it has one. */\n readonly paramLabel?: string\n readonly when: string\n readonly look: string\n readonly budget: string\n readonly network: string\n}\n\n/** The words around the sheet (`renderSheet`, W1266). */\nexport interface SheetLabels {\n readonly when: string\n readonly look: string\n readonly budget: string\n readonly network: string\n readonly quality: string\n readonly replay: string\n readonly auto: string\n}\n\nexport interface VfxStrings {\n readonly effects: Readonly<Record<string, EffectTexts>>\n readonly sheet: SheetLabels\n readonly groups: Readonly<Record<string, string>>\n}\n\nexport const locales: Record<VfxLocale, VfxStrings> = {\n en: {\n effects: en,\n sheet: { when: 'When', look: 'Look', budget: 'Budget', network: 'Network', quality: 'Quality', replay: 'Replay', auto: 'Auto replay' },\n groups: { explosions: 'Explosions', creatures: 'Zombies', weapons: 'Weapons and explosives', world: 'Map, weather and destruction', destruction: 'Scenery destruction', vehicles: 'Vehicles', screen: 'Characters and screen effects' },\n },\n 'pt-BR': {\n effects: ptBR,\n sheet: { when: 'Quando', look: 'Visual', budget: 'Orçamento', network: 'Rede', quality: 'Qualidade', replay: 'Repetir', auto: 'Repetição automática' },\n groups: { explosions: 'Explosões', creatures: 'Zumbis', weapons: 'Armas e explosivos', world: 'Mapa, clima e destruição', destruction: 'Destruição de cenário', vehicles: 'Veículos', screen: 'Personagens e efeitos de tela' },\n },\n}\n\nexport const resolveStrings = (locale: string | undefined): VfxStrings => locales[(locale as VfxLocale) ?? 'en'] ?? locales.en\n","import type { Vec3Tuple } from '../workshop/types'\n\nconst ANIMATION_SPEED = 1.4\nconst START_POSITION: Vec3Tuple = [4, 2.4, 7]\nconst START_TARGET: Vec3Tuple = [0, 1, 0]\n\ntype Vector = [number, number, number]\n\nfunction subtract(a: Vec3Tuple, b: Vec3Tuple): Vector {\n return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]\n}\n\nfunction unit(vector: Vector): Vector {\n const length = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]) || 1\n const inverse = 1 / length\n return [vector[0] * inverse, vector[1] * inverse, vector[2] * inverse]\n}\n\nfunction crossProduct(a: Vector, b: Vector): Vector {\n return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]\n}\n\nfunction ease(value: number): number {\n return value < 0 ? 0 : value > 1 ? 1 : value * value * (3 - 2 * value)\n}\n\nfunction mix(from: Vec3Tuple, to: Vec3Tuple, amount: number): Vec3Tuple {\n return [from[0] + (to[0] - from[0]) * amount, from[1] + (to[1] - from[1]) * amount, from[2] + (to[2] - from[2]) * amount]\n}\n\n/** Rotates a vector by the quaternion `(x, y, z, w)` as three.js `applyQuaternion` does. */\nfunction rotate(local: readonly number[], x: number, y: number, z: number, w: number): Vec3Tuple {\n const [vx, vy, vz] = local as Vec3Tuple\n const ix = w * vx + y * vz - z * vy\n const iy = w * vy + z * vx - x * vz\n const iz = w * vz + x * vy - y * vx\n const iw = -x * vx - y * vy - z * vz\n return [ix * w + iw * -x + iy * -z - iz * -y, iy * w + iw * -y + iz * -x - ix * -z, iz * w + iw * -z + ix * -y - iy * -x]\n}\n\n/** The quaternion three.js builds from `lookAt(eye, target)` with the y axis up (`setFromRotationMatrix`). */\nfunction lookAtQuaternion(eye: Vec3Tuple, target: Vec3Tuple): readonly [number, number, number, number] {\n const z = unit(subtract(eye, target))\n const x = unit(crossProduct([0, 1, 0], z))\n const y = crossProduct(z, x)\n const [m11, m21, m31, m12, m22, m32, m13, m23, m33] = [x[0], x[1], x[2], y[0], y[1], y[2], z[0], z[1], z[2]]\n const trace = m11 + m22 + m33\n if (trace > 0) {\n const scale = 0.5 / Math.sqrt(trace + 1)\n return [(m32 - m23) * scale, (m13 - m31) * scale, (m21 - m12) * scale, 0.25 / scale]\n }\n if (m11 > m22 && m11 > m33) {\n const scale = 2 * Math.sqrt(1 + m11 - m22 - m33)\n return [0.25 * scale, (m12 + m21) / scale, (m13 + m31) / scale, (m32 - m23) / scale]\n }\n if (m22 > m33) {\n const scale = 2 * Math.sqrt(1 + m22 - m11 - m33)\n return [(m12 + m21) / scale, 0.25 * scale, (m23 + m32) / scale, (m13 - m31) / scale]\n }\n const scale = 2 * Math.sqrt(1 + m33 - m11 - m22)\n return [(m13 + m31) / scale, (m23 + m32) / scale, 0.25 * scale, (m21 - m12) / scale]\n}\n\n/**\n * The Workshop's view (W1259-1262): it glides to each effect's `view` (`camTo`) in about 0.7 s and persists between effects, so an effect that reads the\n * camera (a ring that faces the viewer, a mist at the lens) sees where the previous one left it.\n */\nexport class EffectCamera {\n position: Vec3Tuple = START_POSITION\n target: Vec3Tuple = START_TARGET\n private animation: { from: Vec3Tuple; fromTarget: Vec3Tuple; to: Vec3Tuple; toTarget: Vec3Tuple; progress: number } | undefined\n\n goTo(eye: Vec3Tuple, target: Vec3Tuple): void {\n this.animation = { from: this.position, fromTarget: this.target, to: eye, toTarget: target, progress: 0 }\n }\n\n place(eye: Vec3Tuple, target: Vec3Tuple): void {\n this.position = eye\n this.target = target\n this.animation = undefined\n }\n\n hold(): void {\n this.animation = undefined\n }\n\n update(dt: number): void {\n const animation = this.animation\n if (!animation) return\n animation.progress = Math.min(1, animation.progress + dt * ANIMATION_SPEED)\n const eased = ease(animation.progress)\n this.position = mix(animation.from, animation.to, eased)\n this.target = mix(animation.fromTarget, animation.toTarget, eased)\n if (animation.progress >= 1) this.animation = undefined\n }\n\n /** A direction given in the camera's own axes, in world axes. */\n forward(local: readonly number[]): Vec3Tuple {\n const [x, y, z, w] = lookAtQuaternion(this.position, this.target)\n return rotate(local, x, y, z, w)\n }\n}\n","import type { RandomStream } from '../random'\nimport type { WorkshopEngine } from '../workshop/engine'\nimport type { Vec3Tuple } from '../workshop/types'\nimport type { Behavior } from './behavior'\nimport { EffectCamera } from './camera'\nimport { EffectInstance } from './instance'\nimport type { CueRequest, EffectDef, EffectStep } from './types'\n\nconst SHAKE_SCALE = 0.06\nconst SHAKE_DEPTH = 0.5\n\nexport interface EffectRunnerOptions {\n readonly engine: WorkshopEngine\n readonly effects: Readonly<Record<string, EffectDef>>\n readonly behaviors?: Readonly<Record<string, Behavior>>\n readonly onCue?: (cue: CueRequest) => void\n}\n\n/**\n * The Workshop's one-effect-at-a-time view (W1253-1295): choosing an effect clears the set and runs it; each frame fires the timers due, runs the tick, ages\n * the pools, glides the camera and draws the camera shake. This is what the bench shows and what the golden replays; `createEffectPlayer` plays many at once.\n */\nexport class EffectRunner {\n readonly camera = new EffectCamera()\n readonly instance: EffectInstance\n /** The camera shake of the last frame, three draws per frame as in the page. */\n shake: Vec3Tuple = [0, 0, 0]\n\n constructor(private readonly options: EffectRunnerOptions) {\n const behaviors = options.behaviors ?? {}\n this.instance = new EffectInstance({\n engine: options.engine,\n view: this.camera,\n behavior: (id) => behaviors[id]!,\n behaviors: () => Object.values(behaviors),\n ...(options.onCue ? { onCue: options.onCue } : {}),\n })\n }\n\n get current(): EffectDef | undefined {\n return this.instance.def\n }\n\n get variables(): Map<string, unknown> {\n return this.instance.variables\n }\n\n get param(): number {\n return this.instance.param\n }\n\n get fresh(): boolean {\n return this.instance.fresh\n }\n\n get time(): number {\n return this.instance.time\n }\n\n /** Seconds since the effect was chosen: unlike `time`, a restart of the loop does not reset it (`el`). */\n get elapsed(): number {\n return this.instance.elapsed\n }\n\n get random(): RandomStream {\n return this.options.engine.random\n }\n\n /** `select` + `startLoop` (W1253-1257): clears everything, then runs the effect's setup and timeline. */\n start(id: string, param?: number): boolean {\n const def = this.options.effects[id]\n if (!def) return false\n if (def.view) this.camera.goTo(def.view.eye, def.view.target)\n else this.camera.hold()\n this.options.engine.clear()\n this.instance.begin(def, param)\n return true\n }\n\n /** `startLoop` again: the same effect from its start (the replay button and the end of the loop). */\n restart(): void {\n if (!this.instance.def) return\n this.options.engine.clear()\n this.instance.restart()\n }\n\n /** One frame of the page (W1280-1295). */\n update(dt: number): void {\n const engine = this.options.engine\n engine.post = {}\n this.instance.step(dt)\n engine.update(dt)\n this.camera.update(dt)\n const amplitude = engine.screen.shake * SHAKE_SCALE + this.instance.tremor\n const random = this.random\n this.shake = [random.between(-1, 1) * amplitude, random.between(-1, 1) * amplitude, random.between(-1, 1) * amplitude * SHAKE_DEPTH]\n }\n\n execute(steps: readonly EffectStep[]): void {\n this.instance.execute(steps)\n }\n}\n","import type { RandomStream } from './random'\n\nexport interface GroundFieldOptions {\n /** Side of the deformable patch in metres (the Lab's 60). */\n readonly size?: number\n /** Cells per side (the Lab's 180: 33 cm cells). */\n readonly segments?: number\n}\n\n/** The deformable ground the Lab's craters dig into (L60-83): a height field, in float32 like the page, centred on the origin. */\nexport interface GroundField {\n readonly size: number\n readonly segments: number\n /** Vertices per side. */\n readonly stride: number\n readonly cellSize: number\n /** World coordinate of the first row and column. */\n readonly origin: number\n readonly heights: Float32Array\n /** Goes up on every dent or reset: a renderer redraws its mesh when it changes. */\n readonly version: number\n heightAt(x: number, z: number): number\n /** A bowl with an uneven floor and a raised rim, drawing from `random` like the Lab's `dent`. */\n dent(random: RandomStream, x: number, z: number, radius: number, depth: number): void\n reset(): void\n}\n\nconst DEFAULT_SIZE = 60\nconst DEFAULT_SEGMENTS = 180\nconst MAX_DEPTH = 3\nconst RIM_REACH = 1.45\nconst REACH_FACTOR = 1.5\n\nexport function createGroundField(options: GroundFieldOptions = {}): GroundField {\n const size = options.size ?? DEFAULT_SIZE\n const segments = options.segments ?? DEFAULT_SEGMENTS\n const stride = segments + 1\n const cellSize = size / segments\n const origin = -size / 2\n const heights = new Float32Array(stride * stride)\n let version = 0\n\n function heightAt(x: number, z: number): number {\n const fx = (x - origin) / cellSize\n const fz = (z - origin) / cellSize\n if (fx < 0 || fz < 0 || fx >= segments || fz >= segments) return 0\n const column = fx | 0\n const row = fz | 0\n const tx = fx - column\n const tz = fz - row\n const index = row * stride + column\n return (heights[index]! * (1 - tx) + heights[index + 1]! * tx) * (1 - tz) + (heights[index + stride]! * (1 - tx) + heights[index + stride + 1]! * tx) * tz\n }\n\n function dent(random: RandomStream, x: number, z: number, radius: number, depth: number): void {\n const reach = radius * REACH_FACTOR\n const column0 = Math.max(0, Math.floor((x - reach - origin) / cellSize))\n const column1 = Math.min(segments, Math.ceil((x + reach - origin) / cellSize))\n const row0 = Math.max(0, Math.floor((z - reach - origin) / cellSize))\n const row1 = Math.min(segments, Math.ceil((z + reach - origin) / cellSize))\n const phaseA = random.next() * 6\n const phaseB = random.next() * 6\n for (let row = row0; row <= row1; row++) {\n for (let column = column0; column <= column1; column++) {\n const px = origin + column * cellSize\n const pz = origin + row * cellSize\n const distance = Math.hypot(px - x, pz - z)\n const angle = Math.atan2(pz - z, px - x)\n const q = distance / (radius * (1 + 0.13 * Math.sin(angle * 5 + phaseA) + 0.07 * Math.sin(angle * 11 + phaseB)))\n let change = 0\n if (q < 1) change = -depth * (1 - q * q) * (0.85 + random.next() * 0.3)\n else if (q < RIM_REACH) {\n const k = (q - 1) / 0.45\n change = depth * 0.3 * Math.sin(k * Math.PI) * (1 - k * 0.3) * (0.6 + random.next() * 0.8)\n }\n if (!change) continue\n const index = row * stride + column\n heights[index] = Math.max(-MAX_DEPTH, heights[index]! + change)\n }\n }\n version++\n }\n\n function reset(): void {\n heights.fill(0)\n version++\n }\n\n return {\n size,\n segments,\n stride,\n cellSize,\n origin,\n heights,\n get version() {\n return version\n },\n heightAt,\n dent,\n reset,\n }\n}\n","import type { Puddle, VfxWorld } from '@adatechnology/game-vfx'\nimport { inPuddle } from '@adatechnology/game-vfx'\nimport type { WeatherSample } from '@adatechnology/game-weather'\nimport type { Vec3 } from './types'\n\n/** The rain (mm/h) from which a drop that lands splashes (the Workshop's garoa, 2.5, does not; its chuva, 15, does). */\nexport const SPLASH_FROM_MM = 5\n\nexport type NearRainOptions = {\n /** Ground ellipses where a drop leaves a ring (the Workshop's four dark puddles; a game has its own). None by default. */\n readonly puddles?: readonly Puddle[]\n /** Replaces the puddle test. */\n readonly puddleAt?: (x: number, z: number) => boolean\n readonly splashFrom?: number\n}\n\nexport type NearRain = {\n update(sample: WeatherSample, dt: number, camera: Vec3): void\n /** Streaks alive. */\n count(): number\n clear(): void\n}\n\n/**\n * The Workshop's near rain (W313-331) driven by the weather: the streaks live in the world's Workshop engine (`engine.rain`), game-vfx-three draws them, and the\n * splashes and rings they leave go to the `normal` sprite pool and the rings. `drops` (900 garoa, 2 200 chuva, 4 000 temporal) and the wind along x are the Workshop's own\n * numbers for its three intensities.\n */\nexport function createNearRain(vfx: VfxWorld | undefined, options: NearRainOptions = {}): NearRain {\n const puddles = options.puddles ?? []\n return {\n update(sample, dt, camera) {\n const raining = sample.rain > 0\n const engine = raining ? vfx?.workshop() : vfx?.peekWorkshop()\n if (!engine) return\n const field = engine.rain\n if (!raining) {\n if (field.on) field.clear()\n return\n }\n field.puddleAt = options.puddleAt ?? ((x, z) => inPuddle(puddles, x, z))\n field.drive(sample.drops, sample.windX, camera[0], camera[2])\n field.update(dt, sample.rain >= (options.splashFrom ?? SPLASH_FROM_MM), camera)\n },\n count: () => vfx?.peekWorkshop()?.rain.count ?? 0,\n clear() {\n vfx?.peekWorkshop()?.rain.clear()\n },\n }\n}\n","import type * as THREE_NS from 'three'\nimport type { WeatherSample } from '@adatechnology/game-weather'\nimport type { ThreeLib, WeatherProfileName } from './types'\n\n/**\n * The game's rain curtain (jogo.js `const RAIN = 14000`): a box of points that follows the camera, 70 m wide and 25 m high, falling 13 m/s. The shader texts and the\n * numbers are the page's, character for character (a golden cuts them from the page and compares).\n */\nexport const RAIN_CURTAIN = { count: 14000, width: 70, height: 25, speed: 13, pointSize: 1.6 } as const\n\nexport const RAIN_CURTAIN_VERTEX =\n 'uniform float uTime; uniform vec3 uCam; void main(){ vec3 p = position; p.y = mod(p.y - uTime*13., 25.); p.xz = uCam.xz + mod(p.xz - uCam.xz + 35., 70.) - 35.; gl_Position = projectionMatrix*viewMatrix*vec4(p,1.); gl_PointSize = 1.6; }'\nexport const RAIN_CURTAIN_FRAGMENT = 'uniform float uDay; void main(){ gl_FragColor = vec4(vec3(.68,.72,.78)*(0.25+0.75*uDay), .22+.1*uDay); }'\n\n/** The share of the 14 000 points each quality draws. */\nexport const RAIN_CURTAIN_PROFILES: Readonly<Record<WeatherProfileName, number>> = { low: 0.35, medium: 0.7, high: 1 }\n/** The weather's `drops` at which the whole curtain is drawn (the storm's 4 000). */\nexport const RAIN_CURTAIN_FULL_AT = 4000\n\n/** The page's loop: x and z in -35..35, y in 0..25, three draws a point in that order, stored as Float32. */\nexport function rainCurtainPositions(random: () => number = Math.random, count: number = RAIN_CURTAIN.count): Float32Array {\n const positions = new Float32Array(count * 3)\n for (let index = 0; index < count; index++) {\n positions[index * 3] = (random() - 0.5) * RAIN_CURTAIN.width\n positions[index * 3 + 1] = random() * RAIN_CURTAIN.height\n positions[index * 3 + 2] = (random() - 0.5) * RAIN_CURTAIN.width\n }\n return positions\n}\n\nexport type RainCurtainOptions = {\n readonly random?: () => number\n readonly profile?: WeatherProfileName\n /** Drops (`WeatherSample.drops`) that draw the whole curtain. */\n readonly fullAt?: number\n readonly count?: number\n}\n\nexport type RainCurtain = {\n readonly object: THREE_NS.Points\n /** Draws `sample.drops` of the curtain around `camera`; `daylight` is the game's `s` (0 night, 1 day). */\n update(sample: WeatherSample, camera: { readonly x: number; readonly y: number; readonly z: number }, time: number, daylight: number): void\n setProfile(profile: WeatherProfileName): void\n /** How many points are drawn now. */\n drawn(): number\n dispose(): void\n}\n\nexport function createRainCurtain(THREE: ThreeLib, options: RainCurtainOptions = {}): RainCurtain {\n const count = options.count ?? RAIN_CURTAIN.count\n const geometry = new THREE.BufferGeometry()\n geometry.setAttribute('position', new THREE.BufferAttribute(rainCurtainPositions(options.random, count), 3))\n const uniforms = { uTime: { value: 0 }, uCam: { value: new THREE.Vector3() }, uDay: { value: 1 } }\n const material = new THREE.ShaderMaterial({ uniforms, transparent: true, depthWrite: false, vertexShader: RAIN_CURTAIN_VERTEX, fragmentShader: RAIN_CURTAIN_FRAGMENT })\n const object = new THREE.Points(geometry, material)\n object.frustumCulled = false\n object.name = 'weather-rain-curtain'\n object.visible = false\n let share = RAIN_CURTAIN_PROFILES[options.profile ?? 'high']\n let drawn = 0\n return {\n object,\n update(sample, camera, time, daylight) {\n const fullAt = options.fullAt ?? RAIN_CURTAIN_FULL_AT\n drawn = sample.rain > 0 ? Math.round(count * share * Math.min(1, sample.drops / fullAt)) : 0\n object.visible = drawn > 0\n geometry.setDrawRange(0, drawn)\n uniforms.uTime.value = time\n uniforms.uDay.value = daylight\n uniforms.uCam.value.set(camera.x, camera.y, camera.z)\n },\n setProfile(profile) {\n share = RAIN_CURTAIN_PROFILES[profile]\n },\n drawn: () => drawn,\n dispose() {\n geometry.dispose()\n material.dispose()\n },\n }\n}\n","import type * as THREE_NS from 'three'\nimport type { VfxWorld } from '@adatechnology/game-vfx'\nimport type { Puddle } from '@adatechnology/game-vfx'\nimport { createFog, emitAirborne } from './fog'\nimport type { Fog } from './fog'\nimport { createHeadlights } from './headlights'\nimport type { HeadlightRules, Headlights } from './headlights'\nimport { createHeatHaze } from './heat-haze'\nimport type { HeatHaze } from './heat-haze'\nimport { createLightning } from './lightning'\nimport type { Lightning } from './lightning'\nimport { createNearRain } from './rain-near'\nimport type { NearRain } from './rain-near'\nimport { createRainCurtain } from './rain-curtain'\nimport type { RainCurtain } from './rain-curtain'\nimport { createSky } from './sky'\nimport type { Sky } from './sky'\nimport type { ThreeLib, Vec3, WeatherProfileName, WeatherSource, ZoneFog } from './types'\n\n/** The parts the renderer draws, each of which a layer switches. */\nexport const WEATHER_LAYERS = ['far_rain', 'near_rain', 'mist', 'fog', 'sky', 'lightning', 'headlights', 'haze'] as const\nexport type WeatherLayerId = (typeof WEATHER_LAYERS)[number]\n\nexport type WeatherRendererOptions = {\n readonly scene: THREE_NS.Scene\n readonly camera: THREE_NS.Camera\n readonly weather: WeatherSource\n /** The effects world: near rain, splashes, mist, ash, the white of lightning and the lit drops of headlights live in its Workshop pools (game-vfx-three draws them). */\n readonly vfx?: VfxWorld\n readonly profile?: WeatherProfileName\n /** Seeds the rain curtain (default `Math.random`). */\n readonly random?: () => number\n readonly zone?: ZoneFog\n readonly puddles?: readonly Puddle[]\n readonly puddleAt?: (x: number, z: number) => boolean\n readonly headlights?: HeadlightRules\n readonly layers?: Partial<Record<WeatherLayerId, boolean>>\n}\n\nexport type WeatherRenderer = {\n readonly curtain: RainCurtain\n readonly nearRain: NearRain\n readonly fog: Fog\n readonly sky: Sky\n readonly lightning: Lightning\n readonly headlights: Headlights\n readonly haze: HeatHaze\n /** Draws the weather of the source for a frame of `dt` seconds. Call once per frame, after the weather advanced. */\n update(dt: number): void\n setLayer(id: WeatherLayerId, on: boolean): void\n layer(id: WeatherLayerId): boolean\n setZoneFog(zone: ZoneFog | undefined): void\n setProfile(profile: WeatherProfileName): void\n stats(): { readonly rainCurtain: number; readonly rainNear: number; readonly cars: number }\n dispose(): void\n}\n\n/** The whole weather picture: rain curtain, near rain with splashes, mist and ash, fog, sky, lightning, headlights and heat haze, from one `WeatherSource`. */\nexport function createWeatherRenderer(THREE: ThreeLib, options: WeatherRendererOptions): WeatherRenderer {\n const { scene, camera, weather, vfx } = options\n const group = new THREE.Group()\n group.name = 'weather'\n scene.add(group)\n const curtain = createRainCurtain(THREE, { ...(options.random ? { random: options.random } : {}), ...(options.profile ? { profile: options.profile } : {}) })\n const haze = createHeatHaze(THREE)\n group.add(curtain.object, haze.object)\n const sky = createSky(THREE, scene)\n const fog = createFog(THREE, scene)\n const lightning = createLightning(THREE, scene, vfx)\n const headlights = createHeadlights(THREE, options.headlights)\n const nearRain = createNearRain(vfx, { ...(options.puddles ? { puddles: options.puddles } : {}), ...(options.puddleAt ? { puddleAt: options.puddleAt } : {}) })\n const on = new Map<WeatherLayerId, boolean>(WEATHER_LAYERS.map((id) => [id, options.layers?.[id] ?? true]))\n let zone = options.zone\n let time = 0\n const eye: { x: number; y: number; z: number } = { x: 0, y: 0, z: 0 }\n\n const switchedOff = (id: WeatherLayerId): void => {\n if (id === 'far_rain') curtain.object.visible = false\n else if (id === 'near_rain') nearRain.clear()\n else if (id === 'fog') fog.clear()\n else if (id === 'lightning') lightning.light.intensity = 0\n else if (id === 'haze') haze.object.visible = false\n }\n\n return {\n curtain,\n nearRain,\n fog,\n sky,\n lightning,\n headlights,\n haze,\n update(dt) {\n time += dt\n const sample = weather.sample()\n const position = camera.position\n eye.x = position.x\n eye.y = position.y\n eye.z = position.z\n const where: Vec3 = [eye.x, eye.y, eye.z]\n const background = (scene.background as THREE_NS.Color | null) ?? undefined\n const ownSky = on.get('sky') === true\n const color = ownSky ? sky.update(sample, zone) : background && (background as { isColor?: boolean }).isColor ? background : sky.color\n if (on.get('fog')) fog.update(sample, color, zone)\n if (on.get('lightning')) lightning.update(sample, ownSky ? (scene.background as THREE_NS.Color) : undefined)\n if (on.get('far_rain')) curtain.update(sample, eye, time, sample.sun)\n if (on.get('haze')) haze.update(sample, time)\n if (vfx) {\n if (on.get('near_rain')) nearRain.update(sample, dt, where)\n if (on.get('mist')) emitAirborne(vfx.workshop(), sample, dt, where)\n }\n headlights.update(sample, dt, where, vfx && sample.rain > 0 ? vfx.workshop() : undefined, on.get('headlights'))\n },\n setLayer(id, value) {\n on.set(id, value)\n if (!value) switchedOff(id)\n },\n layer: (id) => on.get(id) === true,\n setZoneFog(next) {\n zone = next\n },\n setProfile(profile) {\n curtain.setProfile(profile)\n },\n stats: () => ({ rainCurtain: curtain.drawn(), rainNear: nearRain.count(), cars: headlights.count() }),\n dispose() {\n scene.remove(group)\n curtain.dispose()\n haze.dispose()\n lightning.dispose()\n headlights.dispose()\n nearRain.clear()\n },\n }\n}\n","import type { Vec3 } from './destruction'\n\n/**\n * The numbers of the shove a blast gives to a body that moves (a loose item, a piece of furniture, a person): all of them a game value, with a default that reads well in a room,\n * not a physical table. A host reads them once and lends them to the modules that move their own bodies (`ModuleHost.blastShove`).\n */\nexport interface BlastShoveOptions {\n /**\n * The share of the blast energy that reaches a body as motion: the speed at the centre is `sqrt(2 * energy * coupling / mass)`, before the falloff (default 0.02).\n */\n readonly coupling?: number\n /** Fastest a body is thrown, in m/s, at the centre of the blast (default 20). */\n readonly maxSpeed?: number\n /** How much of the push goes up: the direction is `(away, lift)` normalised from the horizontal outward direction (default 0.3). */\n readonly lift?: number\n /** Below this speed (m/s) nothing moves: a heavy thing far from a small blast stays where it is (default 0.3). */\n readonly minSpeed?: number\n /** Exponent of the falloff `(1 - distance / radius) ^ power`, the same as the damage of the destruction world (default 2). */\n readonly falloffPower?: number\n}\n\n/** The defaults of `BlastShoveOptions`. */\nexport const DEFAULT_BLAST_SHOVE: Readonly<Required<BlastShoveOptions>> = { coupling: 0.02, maxSpeed: 20, lift: 0.3, minSpeed: 0.3, falloffPower: 2 }\n\n/** A blast as the shove reads it: where, how much energy (J) and how far it reaches (m). `reach` (0..1) is the share that got through what stood in the way. */\nexport interface BlastSource {\n readonly point: Vec3\n readonly energy: number\n readonly radius: number\n readonly reach?: number\n}\n\n/** What a blast does to one body: how fast it leaves (m/s), the unit direction it leaves in, and how far from the centre it was. */\nexport interface BlastShove {\n readonly speed: number\n readonly direction: Vec3\n readonly distance: number\n /** The falloff applied, 0..1. */\n readonly falloff: number\n}\n\n/**\n * The shove a blast gives to a body of `mass` kg whose centre is at `position`: away from the centre, lifted a little, with the speed\n * `min(maxSpeed, sqrt(2 * energy * reach * coupling / mass)) * (1 - distance / radius) ^ 2`. Undefined when the body is outside the radius or the speed is under `minSpeed`.\n * The same rule moves the loose items and furniture of the test room and the people of the characters module, so a game that tunes the options tunes all of them.\n */\nexport function blastShove(blast: BlastSource, target: { readonly position: Vec3; readonly mass: number }, options: BlastShoveOptions = {}): BlastShove | undefined {\n const o = { ...DEFAULT_BLAST_SHOVE, ...options }\n const dx = target.position[0] - blast.point[0]\n const dy = target.position[1] - blast.point[1]\n const dz = target.position[2] - blast.point[2]\n const distance = Math.hypot(dx, dy, dz)\n if (!(blast.radius > 0) || distance > blast.radius) return undefined\n const falloff = (1 - distance / blast.radius) ** o.falloffPower\n const mass = Math.max(target.mass, 0.05)\n const speed = Math.min(o.maxSpeed, Math.sqrt((2 * blast.energy * (blast.reach ?? 1) * o.coupling) / mass)) * falloff\n if (!(speed >= o.minSpeed)) return undefined\n // straight above or below the centre there is no \"away\": the push goes up\n const direction: Vec3 = distance > 1e-4 ? [dx / distance, dy / distance + o.lift, dz / distance] : [0, 1, 0]\n const length = Math.hypot(direction[0], direction[1], direction[2])\n return { speed, direction: [direction[0] / length, direction[1] / length, direction[2] / length], distance, falloff }\n}\n","import type { DamageType, DestructibleSpec, DestructionEvent, Hit, JointSpec, ObjectState, ToolKind, Vec3 } from './destruction'\n\n/**\n * What a game module implements for an object whose behaviour it owns (a vehicle with its own panel deformation, a creature, a machine). The\n * world (`DestructionWorld.register`) hosts it next to its own objects: raycast, shoot, explode, hit, disassemble, extinguish, update, state,\n * reset and `on` all route to it.\n *\n * Space: the impl receives hits in WORLD space and returns events in WORLD space (it owns its transform; the transform given to `register` or\n * `setTransform` is only used by the world for raycasts and blast range). Every event must carry `object === id` and name parts listed by\n * `describe()`.\n */\nexport interface DestructibleImpl {\n readonly id: string\n readonly name?: string\n /** Parts, joints and structure in the impl's local space, for raycast, inspector and log. Called often: keep it cheap. */\n describe(): DestructibleSpec\n /** Apply one hit (see `IMPACT_CONTRACT` for the fields per type) and return the events it caused. */\n hit(hit: Hit): DestructionEvent[]\n /** Advance fire, melting, collapse and other timed behaviour. */\n update?(dt: number): DestructionEvent[]\n state(): ObjectState\n reset?(): void\n /** Same contract as `DestructionWorld.disassemble`: progress 0..1, or -1 when the tool cannot undo the joint. */\n disassemble?(jointId: string, tool: ToolKind, dt: number): number\n extinguish?(partId?: string): void\n /** Optional: an animation moved a part; `position` / `rotation` (Euler XYZ, radians) are object-local. Keep raycast and hit shapes in sync. */\n setPartPose?(partId: string, position: Vec3, rotation?: Vec3): void\n /** Plain JSON of the impl's own state, stored in `DestructionSnapshot.impls`. */\n serialize?(): unknown\n load?(data: unknown): void\n}\n\n/** The fields a weapon or hazard must put in a `Hit`, per damage type. THE impact contract. */\nexport interface ImpactContract {\n required: string[]\n optional: string[]\n note: string\n}\n\nexport const IMPACT_CONTRACT: Record<DamageType, ImpactContract> = {\n ballistic: { required: ['type', 'point', 'direction', 'energy', 'caliber'], optional: ['space', 'source'], note: 'bullets and pellets: joules and diameter in millimetres' },\n blunt: { required: ['type', 'point', 'energy'], optional: ['direction', 'radius', 'space', 'source'], note: 'contact radius in metres (default 3 cm)' },\n slash: { required: ['type', 'point', 'energy'], optional: ['direction', 'radius', 'space', 'source'], note: 'axes, knives, machetes' },\n explosive: { required: ['type', 'point', 'energy', 'radius'], optional: ['lethalRadius', 'space', 'source'], note: 'point is the centre of the blast, radius the range in metres' },\n fire: { required: ['type', 'point', 'temperature', 'radius'], optional: ['duration', 'space', 'source'], note: 'flame temperature in °C; duration in seconds (call every frame with `dt`)' },\n heat: { required: ['type', 'point', 'temperature', 'radius'], optional: ['duration', 'space', 'source'], note: 'heat without flame (cutting torch, hot plate)' },\n}\n\n/** Problems with a hit according to the impact contract (empty when it is valid). */\nexport const checkHit = (hit: Hit): string[] => {\n const c = IMPACT_CONTRACT[hit.type]\n if (!c) return [`unknown damage type \"${String(hit.type)}\"`]\n const out: string[] = []\n for (const f of c.required) {\n const v = (hit as unknown as Record<string, unknown>)[f]\n if (v === undefined || (Array.isArray(v) ? v.some((n) => !Number.isFinite(n)) : typeof v === 'number' && !Number.isFinite(v))) out.push(`${hit.type} hit needs \"${f}\"`)\n }\n return out\n}\n\nexport interface CheckOptions {\n /**\n * Ids of every material the impl may use (the contracts package does not know the materials of your world: pass the ids of\n * `world.materials`, or the built-in plus your own). When omitted, the material check is skipped.\n */\n materials?: Iterable<string>\n}\n\nconst finiteJson = (v: unknown, path: string, out: string[]) => {\n if (typeof v === 'number') {\n if (!Number.isFinite(v)) out.push(`${path} is not a finite number`)\n } else if (Array.isArray(v)) v.forEach((x, i) => finiteJson(x, `${path}[${i}]`, out))\n else if (v && typeof v === 'object') for (const [k, x] of Object.entries(v)) finiteJson(x, `${path}.${k}`, out)\n else if (typeof v === 'function' || typeof v === 'symbol' || typeof v === 'bigint') out.push(`${path} is not JSON`)\n}\n\n/**\n * Contract checker for the test room (it needs no world): `describe()` is valid, a 500 J ballistic hit at a part centre returns well-formed events of this impl, `state()`\n * matches `describe()`, hit points never go negative, `update(0.016)` returns an array and every event is JSON-serializable. It hits the impl\n * (and calls `reset()` afterwards when there is one).\n */\nexport const checkDestructible = (impl: DestructibleImpl, options: CheckOptions = {}): { ok: boolean; problems: string[] } => {\n const problems: string[] = []\n const known = options.materials ? new Set(options.materials) : null\n let spec: DestructibleSpec | undefined\n try {\n spec = impl.describe()\n } catch (e) {\n return { ok: false, problems: [`describe() threw: ${String(e)}`] }\n }\n if (!spec || !Array.isArray(spec.parts) || spec.parts.length === 0) return { ok: false, problems: ['describe() must return at least one part'] }\n const ids = new Set<string>()\n for (const p of spec.parts) {\n if (ids.has(p.id)) problems.push(`duplicate part id \"${p.id}\"`)\n ids.add(p.id)\n if (known && !known.has(p.material)) problems.push(`part \"${p.id}\" has unknown material \"${p.material}\"`)\n if (!p.shape || !['box', 'cylinder', 'sphere'].includes(p.shape.kind)) problems.push(`part \"${p.id}\" has no valid shape`)\n if (!Array.isArray(p.position) || p.position.length !== 3) problems.push(`part \"${p.id}\" has no position`)\n }\n const jointIds = new Set<string>()\n for (const j of (spec.joints ?? []) as JointSpec[]) {\n if (!ids.has(j.a) || !ids.has(j.b)) problems.push(`joint \"${j.id ?? `${j.a}~${j.b}`}\" references an unknown part`)\n const jid = j.id ?? `${j.a}~${j.b}`\n if (jointIds.has(jid)) problems.push(`duplicate joint id \"${jid}\"`)\n jointIds.add(jid)\n }\n const checkState = (label: string) => {\n let st: ObjectState\n try {\n st = impl.state()\n } catch (e) {\n problems.push(`state() threw ${label}: ${String(e)}`)\n return\n }\n const sids = st.parts.map((p) => p.id).sort().join(',')\n if (sids !== [...ids].sort().join(',')) problems.push(`state() part ids do not match describe() ${label}`)\n for (const p of st.parts) if (!(p.hp >= 0)) problems.push(`part \"${p.id}\" has negative or invalid hp ${label}`)\n }\n const checkEvents = (events: unknown, label: string) => {\n if (!Array.isArray(events)) {\n problems.push(`${label} must return an array`)\n return\n }\n const evs = events as DestructionEvent[]\n evs.forEach((e, i) => {\n if (e.object !== impl.id) problems.push(`${label} event ${i} (${e.type}) has object \"${String(e.object)}\", expected \"${impl.id}\"`)\n const part = (e as { part?: string }).part\n if (part !== undefined && !ids.has(part)) problems.push(`${label} event ${i} (${e.type}) names unknown part \"${part}\"`)\n })\n const bad: string[] = []\n finiteJson(evs, 'events', bad)\n problems.push(...bad.map((b) => `${label}: ${b}`))\n try {\n const s = JSON.stringify(evs)\n if (JSON.stringify(JSON.parse(s)) !== s) problems.push(`${label} events do not survive a JSON round trip`)\n } catch (e) {\n problems.push(`${label} events are not JSON-serializable: ${String(e)}`)\n }\n }\n checkState('before the hit')\n const target = spec.parts[0]\n try {\n const hit: Hit = { type: 'ballistic', point: [target.position[0], target.position[1], target.position[2]], direction: [0, 0, -1], energy: 500, caliber: 9 }\n checkEvents(impl.hit(hit), 'hit()')\n } catch (e) {\n problems.push(`hit() threw: ${String(e)}`)\n }\n checkState('after the hit')\n if (impl.update) {\n try {\n checkEvents(impl.update(0.016), 'update()')\n } catch (e) {\n problems.push(`update() threw: ${String(e)}`)\n }\n checkState('after update')\n }\n try {\n impl.reset?.()\n } catch (e) {\n problems.push(`reset() threw: ${String(e)}`)\n }\n return { ok: problems.length === 0, problems }\n}\n","/** The slots of a body an item can cover (the item slot vocabulary: game-items imports it from here). */\nexport type BodySlot = 'head' | 'eyes' | 'torso' | 'arms' | 'hands' | 'legs' | 'feet' | 'over'\n\n/** The 11 segments of the detailed body model (the Arsenal ids). */\nexport type SegmentKind = 'head' | 'jaw' | 'neck' | 'torso' | 'pelvis' | 'upperArm' | 'forearm' | 'hand' | 'thigh' | 'shin' | 'foot'\n\n/** The 6 regions of the crowd model (the game regions 0..5). */\nexport type BodyRegion = 'head' | 'torso' | 'armL' | 'armR' | 'legL' | 'legR'\n\n/** Bits of the `missing` mask of a body. The instanced body shader reads them, so the values are part of the contract. */\nexport const MISSING_BITS = {\n head: 1,\n armL: 2,\n armR: 4,\n legL: 8,\n legR: 16,\n armLBroken: 32,\n armRBroken: 64,\n legLBroken: 128,\n legRBroken: 256,\n jaw: 512,\n} as const\n","import type { DestructibleSpec, PartSpec } from './destruction'\n\nexport interface CheckSpecOptions {\n /** Known material ids. When given, every part's material must be one of them. */\n materials?: Iterable<string>\n}\n\nexport interface SpecReport {\n /** False when there is at least one problem (warnings do not count). */\n ok: boolean\n problems: string[]\n /** Things that are legal but probably wrong, e.g. structural parts that cannot reach an anchored part. */\n warnings: string[]\n}\n\nconst STRUCTURE_ROLES = ['foundation', 'column', 'beam', 'slab', 'wall', 'partition', 'roof', 'stair', 'none']\nconst BEARS = [true, false, 'a_on_b', 'b_on_a']\n\nconst finite = (n: unknown): n is number => typeof n === 'number' && Number.isFinite(n)\nconst positive = (n: unknown): boolean => finite(n) && n > 0\nconst vec3 = (v: unknown): boolean => Array.isArray(v) && v.length === 3 && v.every(finite)\nconst nonNegative = (n: unknown): boolean => n === undefined || (finite(n) && n >= 0)\n\nconst FACES = ['both', '+', '-']\nconst BONDS = ['running', 'stack']\n\n/** Layers, reinforcement, units and breach: known materials, finite positive sizes. */\nconst checkStagedFields = (p: PartSpec | undefined, label: string, materials: Set<string> | null, problems: string[]): void => {\n const known = (m: unknown, what: string): void => {\n if (materials && !materials.has(m as string)) problems.push(`${label} ${what} has unknown material \"${String(m)}\"`)\n }\n ;(p?.layers ?? []).forEach((l, i) => {\n known(l?.material, `layer ${i}`)\n if (!positive(l?.thickness)) problems.push(`${label} layer ${i} thickness must be finite and positive`)\n if (!FACES.includes(l?.faces)) problems.push(`${label} layer ${i} has an invalid faces \"${String(l?.faces)}\"`)\n })\n const r = p?.reinforcement\n if (r) {\n known(r.material, 'reinforcement')\n if (!positive(r.spacing) || !positive(r.diameter)) problems.push(`${label} reinforcement spacing and diameter must be finite and positive`)\n if (r.residualCapacity !== undefined && !(finite(r.residualCapacity) && r.residualCapacity >= 0 && r.residualCapacity <= 1)) problems.push(`${label} reinforcement residualCapacity must be between 0 and 1`)\n }\n const u = p?.units\n if (u) {\n known(u.mortar, 'units mortar')\n if (!vec3(u.size) || !u.size.every(positive)) problems.push(`${label} units size must be three finite positive numbers`)\n if (!BONDS.includes(u.bond)) problems.push(`${label} units has an invalid bond \"${String(u.bond)}\"`)\n }\n if (p?.breach && p.breach.area !== undefined && !positive(p.breach.area)) problems.push(`${label} breach area must be finite and positive`)\n}\n\n/**\n * Static checks on a `DestructibleSpec` (no world needed), for items that give a `spec` instead of an `impl`: unique part ids, known materials\n * (when `options.materials` is given), joints that reference existing, different parts, finite positive sizes, non-negative mass / hp / capacity,\n * valid structure roles and `bears` directions. A structural part with no path of joints to an anchored part is a warning, not a problem: it\n * is allowed, it just falls.\n */\nexport const checkSpec = (spec: DestructibleSpec, options: CheckSpecOptions = {}): SpecReport => {\n const problems: string[] = []\n const warnings: string[] = []\n const materials = options.materials ? new Set(options.materials) : null\n const parts = Array.isArray(spec?.parts) ? spec.parts : []\n if (!Array.isArray(spec?.parts) || parts.length === 0) problems.push('the spec needs at least one part')\n const ids = new Set<string>()\n for (const p of parts) {\n const id = String(p?.id ?? '')\n const label = `part \"${id}\"`\n if (id === '') problems.push('a part has no id')\n else if (ids.has(id)) problems.push(`duplicate part id \"${id}\"`)\n ids.add(id)\n if (materials && !materials.has(p?.material)) problems.push(`${label} has unknown material \"${String(p?.material)}\"`)\n else if (typeof p?.material !== 'string' || p.material === '') problems.push(`${label} needs a material`)\n const s = p?.shape\n if (s?.kind === 'box') {\n if (!Array.isArray(s.size) || s.size.length !== 3 || !s.size.every(positive)) problems.push(`${label} box size must be three finite positive numbers`)\n } else if (s?.kind === 'cylinder') {\n if (!positive(s.radius) || !positive(s.height)) problems.push(`${label} cylinder radius and height must be finite and positive`)\n } else if (s?.kind === 'sphere') {\n if (!positive(s.radius)) problems.push(`${label} sphere radius must be finite and positive`)\n } else problems.push(`${label} has an unknown shape`)\n if (!vec3(p?.position)) problems.push(`${label} needs a finite position [x, y, z]`)\n if (p?.rotation !== undefined && !vec3(p.rotation)) problems.push(`${label} has an invalid rotation`)\n for (const f of ['mass', 'hp', 'thickness'] as const) if (!nonNegative(p?.[f])) problems.push(`${label} ${f} must be a finite number >= 0`)\n if (p?.fill !== undefined && !(finite(p.fill) && p.fill >= 0 && p.fill <= 1)) problems.push(`${label} fill must be between 0 and 1`)\n if (p?.structure) {\n if (!STRUCTURE_ROLES.includes(p.structure.role)) problems.push(`${label} has an invalid structure role \"${String(p.structure.role)}\"`)\n if (!nonNegative(p.structure.capacity)) problems.push(`${label} structure capacity must be a finite number >= 0`)\n }\n if (p?.contains && !nonNegative(p.contains.liters)) problems.push(`${label} contains.liters must be a finite number >= 0`)\n checkStagedFields(p, label, materials, problems)\n }\n\n const jointIds = new Set<string>()\n const edges: Array<[string, string]> = []\n for (const j of spec?.joints ?? []) {\n const id = j.id ?? `${j.a}~${j.b}`\n const label = `joint \"${id}\"`\n if (jointIds.has(id)) problems.push(`duplicate joint id \"${id}\"`)\n jointIds.add(id)\n if (!ids.has(j.a)) problems.push(`${label} references unknown part \"${String(j.a)}\"`)\n if (!ids.has(j.b)) problems.push(`${label} references unknown part \"${String(j.b)}\"`)\n if (j.a === j.b) problems.push(`${label} joins part \"${String(j.a)}\" to itself`)\n if (j.bears !== undefined && !BEARS.includes(j.bears)) problems.push(`${label} has an invalid bears \"${String(j.bears)}\"`)\n if (!nonNegative(j.strength)) problems.push(`${label} strength must be a finite number >= 0`)\n if (!nonNegative(j.seconds)) problems.push(`${label} seconds must be a finite number >= 0`)\n if (j.bears !== false && ids.has(j.a) && ids.has(j.b) && j.a !== j.b) edges.push([j.a, j.b])\n }\n\n // structural parts must reach an anchored part over the joints (only when the spec lists its joints: otherwise they are inferred later)\n const structural = parts.filter((p) => p?.structure && p.structure.role !== 'none')\n if (structural.length && spec.joints) {\n const anchored = parts.filter((p) => p.anchored || p.structure?.role === 'foundation').map((p) => p.id)\n const adj = new Map<string, string[]>()\n for (const [a, b] of edges) {\n adj.set(a, [...(adj.get(a) ?? []), b])\n adj.set(b, [...(adj.get(b) ?? []), a])\n }\n const seen = new Set(anchored)\n const queue = [...anchored]\n while (queue.length) {\n for (const n of adj.get(queue.pop()!) ?? []) {\n if (seen.has(n)) continue\n seen.add(n)\n queue.push(n)\n }\n }\n const lost = structural.filter((p) => !seen.has(p.id)).map((p) => p.id)\n if (lost.length) warnings.push(`structural part(s) ${lost.map((i) => `\"${i}\"`).join(', ')} cannot reach an anchored part, so they will fall`)\n }\n return { ok: problems.length === 0, problems, warnings }\n}\n","import type { BallisticExtras, Hit, HitIgnition, Vec3 } from './destruction'\n\n/**\n * A ballistic shot (one per pellet) as a weapon module produces it. Plain data: any damage model can consume it (`shotToHit` makes the `Hit`).\n */\nexport interface Shot {\n origin: Vec3\n /** Unit vector. */\n direction: Vec3\n /** Muzzle energy in joules. */\n energy: number\n /** Bullet diameter in millimetres. */\n caliber: number\n /** snake_case calibre id, e.g. `9x19`. */\n caliberId: string\n /** Muzzle speed in m/s. */\n speed: number\n /** Projectile mass in grams (one pellet for a shotgun load). */\n mass?: number\n /** Energy at a distance in metres, with the calibre's drag. */\n energyAt(distance: number): number\n pellet: number\n /** snake_case weapon id. */\n weapon: string\n source?: string\n /** Kind of the round that was loaded (`fmj`, `hp`, `ap`, `buck`, `slug`, `incendiary`, `tracer`...). */\n ammo?: string\n /** Millimetres of steel, concrete, brick and wood the round goes through at the muzzle (the ammo table of the weapons module). `shotToHit` scales it by the energy left. */\n penetration?: Readonly<Record<string, number>>\n incendiary?: boolean\n tracer?: boolean\n /** Chances that the round lights flammable vapour or fuel. */\n ignition?: HitIgnition\n}\n\n/**\n * The fields of the round that a damage model takes besides the geometry: the penetration by material scaled by the energy that is left at `distance`\n * (linear in the energy: half the energy goes through half as much), the incendiary and tracer flags and the ignition chances. Pass it as the sixth\n * argument of `DestructionWorld.shoot` or spread it in a `Hit`.\n */\nexport const shotExtras = (shot: Shot, distance = 0): BallisticExtras => {\n const out: { -readonly [K in keyof BallisticExtras]: BallisticExtras[K] } = {}\n if (shot.penetration) {\n const k = shot.energy > 0 ? Math.min(1, Math.max(0, shot.energyAt(distance) / shot.energy)) : 1\n const scaled: Record<string, number> = {}\n for (const key of Object.keys(shot.penetration)) scaled[key] = shot.penetration[key]! * k\n out.penetration = scaled\n }\n if (shot.incendiary) out.incendiary = true\n if (shot.tracer) out.tracer = true\n if (shot.ignition) out.ignition = { spark: shot.ignition.spark, flame: shot.ignition.flame }\n return out\n}\n\n/** One use of a non-firearm (melee swing, flame tick, heat tick, throw, tool tick, extinguisher tick). */\nexport type Use =\n | { kind: 'melee'; damage: 'blunt' | 'slash'; energy: number; radius: number; reach: number; origin: Vec3; direction: Vec3 }\n | { kind: 'flame'; temperature: number; radius: number; range: number; cone: boolean; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'heat'; temperature: number; radius: number; range: number; duration: number; origin: Vec3; direction: Vec3 }\n | {\n kind: 'throw'\n energy: number\n radius: number\n fuse: number\n velocity: Vec3\n origin: Vec3\n fire?: { temperature: number; radius: number; seconds: number }\n /** Explosive id (`m67`, `molotov`, `pg7v`...): the host looks the numbers up in the arsenal. Absent on a plain thrown object. */\n explosive?: string\n /** What sets it off: `timer` after `fuse` seconds, `impact` on the first hit once armed. */\n trigger?: 'timer' | 'impact'\n /** Metres the projectile must fly before an impact sets it off; closer, it hits without exploding (rocket launchers). */\n armDistance?: number\n /** Radius in metres of the wounding area (`radius` is the lethal one). */\n injuryRadius?: number\n /** Seconds the blast stuns whoever is inside the injury radius (flashbang). */\n stunSeconds?: number\n /** Seconds the thrower held it after pulling the pin: `fuse` is what was left of the full fuse when it left the hand. */\n cooked?: number\n /** Went off in the thrower's hand (held past the fuse): no velocity, `fuse` is 0 and the blast is at `origin`. */\n inHand?: boolean\n }\n /** An explosive set down where the trigger is pulled (mine, C4, charge): it waits for its trigger instead of flying. */\n | {\n kind: 'place'\n explosive: string\n /** `wire`: something trips it; `remote`: the player fires it; `timer`: after `fuse` seconds. */\n triggers: readonly ('timer' | 'wire' | 'remote')[]\n energy: number\n /** Lethal radius in metres. */\n radius: number\n injuryRadius: number\n /** Cone the blast goes through, in front of `direction` (mine); absent for a blast in every direction. */\n cone?: { halfAngleDeg: number; length: number }\n /** The wire a `wire` charge stretches from where it is set down: metres out, degrees from its facing (counter-clockwise from above) and metres above it. */\n tripwire?: { length: number; angleDeg: number; height: number }\n fuse?: number\n origin: Vec3\n direction: Vec3\n }\n /** The blast behind a rocket launcher: dangerous inside `radius` metres, in the cone around `direction` (pointing away from the target). */\n | { kind: 'backblast'; radius: number; halfAngleDeg: number; origin: Vec3; direction: Vec3 }\n | { kind: 'tool'; tool: string; reach: number; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'extinguish'; range: number; coolC: number; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'hand'; reach: number; origin: Vec3; direction: Vec3 }\n\n/**\n * The `Hit` a ballistic shot makes at `point` (world space): the energy is what is left of the shot at that distance from its origin\n * (`shot.energyAt`), the direction and calibre come from the shot. `normal` (the surface normal at the point, when the caller knows it) does not\n * change the hit; it is accepted so that callers can pass what their raycast returned and so that a stricter implementation could use it.\n */\nexport const shotToHit = (shot: Shot, point: Vec3, normal?: Vec3): Hit => {\n void normal\n const dx = point[0] - shot.origin[0]\n const dy = point[1] - shot.origin[1]\n const dz = point[2] - shot.origin[2]\n const distance = Math.hypot(dx, dy, dz)\n return {\n type: 'ballistic',\n point: [point[0], point[1], point[2]],\n direction: [shot.direction[0], shot.direction[1], shot.direction[2]],\n energy: shot.energyAt(distance),\n caliber: shot.caliber,\n ...(shot.mass === undefined ? {} : { mass: shot.mass }),\n ...shotExtras(shot, distance),\n source: shot.source ?? shot.weapon,\n }\n}\n\n/**\n * The `Hit` a non-firearm `Use` makes at `point` (world space), or `null` for uses that do not damage by themselves.\n *\n * - `melee` -> `blunt` / `slash` with the swing's `energy`, contact `radius` and `direction`;\n * - `flame` -> `fire` and `heat` -> `heat`, with `temperature`, `duration` and a contact radius of at most 0.3 m (one ray of the cone);\n * - `throw` -> the blast (`explosive`, `energy`, `radius`, centred on `point`, the place it detonates). A thrown item with no blast energy but\n * a `fire` patch (a molotov) gives that patch as a `fire` hit (`temperature`, `radius`, `seconds` as `duration`). No energy and no fire -> `null`;\n * - `tool`, `extinguish` and `hand` -> `null` (tools work on joints, the extinguisher cools, the hand only grabs).\n *\n * `normal` does not change the hit (same as `shotToHit`).\n */\nexport const useToHit = (use: Use, point: Vec3, normal?: Vec3): Hit | null => {\n void normal\n const p: Vec3 = [point[0], point[1], point[2]]\n switch (use.kind) {\n case 'melee':\n return { type: use.damage, point: p, direction: [use.direction[0], use.direction[1], use.direction[2]], energy: use.energy, radius: use.radius }\n case 'flame':\n case 'heat':\n return {\n type: use.kind === 'flame' ? 'fire' : 'heat',\n point: p,\n direction: [use.direction[0], use.direction[1], use.direction[2]],\n temperature: use.temperature,\n radius: Math.min(use.radius, 0.3),\n duration: use.duration,\n }\n case 'throw':\n if (use.energy > 0) return { type: 'explosive', point: p, energy: use.energy, radius: use.radius }\n if (use.fire) return { type: 'fire', point: p, temperature: use.fire.temperature, radius: use.fire.radius, duration: use.fire.seconds }\n return null\n default:\n return null\n }\n}\n","import type { Hit } from './destruction'\n\n/**\n * How a container of flammable fluid (a fuel tank, a jerrycan, a gas bottle) is set alight by what hits it. A normal bullet that goes through only\n * makes a leak; the leak lights when something supplies a flame: a spark of a hard round on metal, an incendiary or tracer round, a fire nearby\n * or a blast. These are the numbers of the target side; the numbers of the round (core, jacket, incendiary, tracer) are in the weapons module\n * (`rules.ignition` of `@adatechnology/game-weapons`) and reach the target in `Hit.ignition`. Every number is a default to be changed per game.\n */\nexport interface IgnitionRules {\n /**\n * How much of the spark and of the flame of a round reaches the vapour, by how full the container is: points `[filled, factor]`, `filled` 0..1\n * (litres over capacity), `factor` 0..1, linear between points. A full container holds a mixture too rich to burn (the vapour has no air) and\n * mostly leaks; a nearly empty one holds the vapour/air mixture of a flammable range and lights the easiest; a dry one has little vapour left.\n * Default: 0.6 empty, 1 at 10 percent, 0.6 at 35, 0.3 at 65, 0.15 at 90 and 0.12 full.\n */\n fillCurve: ReadonlyArray<readonly [number, number]>\n /** 0..1: how much of the fill factor also weighs on the flame of an incendiary or tracer (1: as much as on a spark; 0: not at all). Default 0.5. */\n flameFillInfluence: number\n /** Chance of the flame of a round that is flagged `incendiary` or `tracer` but carries no `ignition` numbers (a host that passes only the flags). */\n flameDefaults: { incendiary: number; tracer: number }\n /**\n * What a spark needs to strike: a factor 0..1 by material id of the struck part. A material that is not listed but is metal takes\n * `sparkMetalDefault`; a part that is not metal never sparks. Default: steel and iron 1, aluminium 0.25.\n */\n sparkTarget: Readonly<Record<string, number>>\n sparkMetalDefault: number\n /**\n * Factor 0..1 on the spark by fluid (default 1): diesel has a flash point of 52 degrees C and more, so a spark that lights petrol vapour does\n * not light it (a flame does). Default: diesel 0.15.\n */\n sparkFluid: Readonly<Record<string, number>>\n /** A leaking container of flammable fluid within `radius` metres of a burning part or a burning fuel stain lights at `chancePerSecond`. */\n fireNear: { radius: number; chancePerSecond: number }\n}\n\n/** Defaults of `IgnitionRules`. */\nexport const DEFAULT_IGNITION_RULES: IgnitionRules = {\n fillCurve: [\n [0, 0.6],\n [0.1, 1],\n [0.35, 0.6],\n [0.65, 0.3],\n [0.9, 0.15],\n [1, 0.12],\n ],\n flameFillInfluence: 0.5,\n flameDefaults: { incendiary: 0.7, tracer: 0.2 },\n sparkTarget: { steel: 1, iron: 1, aluminium: 0.25 },\n sparkMetalDefault: 0.5,\n sparkFluid: { diesel: 0.15 },\n fireNear: { radius: 1.5, chancePerSecond: 0.5 },\n}\n\n/** Merges a partial `IgnitionRules` over the defaults (an object is merged one level deep). */\nexport const resolveIgnitionRules = (rules?: Partial<IgnitionRules>): IgnitionRules => ({\n ...DEFAULT_IGNITION_RULES,\n ...rules,\n flameDefaults: { ...DEFAULT_IGNITION_RULES.flameDefaults, ...rules?.flameDefaults },\n sparkTarget: { ...DEFAULT_IGNITION_RULES.sparkTarget, ...rules?.sparkTarget },\n sparkFluid: { ...DEFAULT_IGNITION_RULES.sparkFluid, ...rules?.sparkFluid },\n fireNear: { ...DEFAULT_IGNITION_RULES.fireNear, ...rules?.fireNear },\n})\n\n/** Linear interpolation of the fill curve at `fill` (0..1); outside the points it keeps the first or the last. */\nexport const fillFactor = (fill: number, curve: IgnitionRules['fillCurve'] = DEFAULT_IGNITION_RULES.fillCurve): number => {\n if (curve.length === 0) return 1\n const f = Math.min(1, Math.max(0, fill))\n if (f <= curve[0]![0]) return curve[0]![1]\n for (let i = 1; i < curve.length; i++) {\n const [x1, y1] = curve[i]!\n const [x0, y0] = curve[i - 1]!\n if (f <= x1) return x1 === x0 ? y1 : y0 + ((y1 - y0) * (f - x0)) / (x1 - x0)\n }\n return curve[curve.length - 1]![1]\n}\n\n/** The factor of what a spark strikes: 0 when the struck part is not metal. */\nexport const sparkTargetFactor = (materialId: string, metal: boolean, rules: IgnitionRules = DEFAULT_IGNITION_RULES): number =>\n !metal ? 0 : rules.sparkTarget[materialId] ?? rules.sparkMetalDefault\n\n/**\n * Chance that one round that went through the wall of a container lights the fluid that leaks: the spark (only on metal) and the flame (any\n * wall), each weighed by the fill of the container; the two are independent. 0 when the round carries nothing (`Hit.ignition` absent and not\n * flagged `incendiary` or `tracer`): a plain bullet on a plain wall only leaks.\n */\nexport const ignitionChance = (hit: Pick<Hit, 'incendiary' | 'tracer' | 'ignition'>, target: { sparkTarget: number; fill: number; fluid?: string }, rules: IgnitionRules = DEFAULT_IGNITION_RULES): number => {\n const k = fillFactor(target.fill, rules.fillCurve)\n const spark = (hit.ignition?.spark ?? 0) * target.sparkTarget * (target.fluid === undefined ? 1 : rules.sparkFluid[target.fluid] ?? 1) * k\n const flameBase = hit.ignition?.flame ?? Math.max(hit.incendiary ? rules.flameDefaults.incendiary : 0, hit.tracer ? rules.flameDefaults.tracer : 0)\n const flame = flameBase * (1 - rules.flameFillInfluence * (1 - k))\n return 1 - (1 - Math.min(1, Math.max(0, spark))) * (1 - Math.min(1, Math.max(0, flame)))\n}\n","import type { Vec3 } from './destruction'\n\n/** Pose of one part (or of the whole item) at a key: position offset in metres and Euler XYZ rotation in radians, both relative to the rest pose. */\nexport interface AnimationPose {\n position?: Vec3\n rotation?: Vec3\n}\n\nexport interface AnimationKey {\n /** Seconds from the start of the clip. */\n t: number\n /** Pose of the item's root. */\n position?: Vec3\n rotation?: Vec3\n /** Poses of named parts: the key is a destruction part id (a pistol `reload` moves `mag`; a car door `open` rotates `door_l`). */\n parts?: Record<string, AnimationPose>\n}\n\nexport interface AnimationEventSpec {\n /** Seconds from the start of the clip. */\n t: number\n /** snake_case type the host maps (`sound`, `eject_casing`, `door_latch`...). */\n type: string\n data?: Record<string, unknown>\n}\n\n/** A use animation a module brings with its item. Poses between keys are interpolated by the player (the test room does it linearly). */\nexport interface AnimationClipSpec {\n /** snake_case: `reload`, `open`, `drink`... Unique within the item. */\n id: string\n /** Seconds. */\n duration: number\n keys: AnimationKey[]\n events?: AnimationEventSpec[]\n}\n\n/** What an item exposes when it brings its own animations. */\nexport interface ItemAnimationContract {\n animations: AnimationClipSpec[]\n}\n\nconst finite3 = (v: unknown): boolean => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === 'number' && Number.isFinite(n))\n\n/**\n * Problems with a clip (empty when it is valid): a snake_case id, a positive finite duration, at least one key, key and event times inside\n * `[0, duration]` and not going backwards, finite vectors, every named part one of `partIds`, event types that are non-empty strings and\n * JSON-serializable event data.\n */\nexport const checkAnimation = (clip: AnimationClipSpec, partIds: Iterable<string>): { ok: boolean; problems: string[] } => {\n const problems: string[] = []\n const parts = new Set(partIds)\n const id = String(clip?.id ?? '')\n if (!/^[a-z][a-z0-9_]*$/.test(id)) problems.push(`clip id \"${id}\" must be snake_case`)\n const label = `clip \"${id}\"`\n if (!(typeof clip?.duration === 'number' && Number.isFinite(clip.duration) && clip.duration > 0)) problems.push(`${label} needs a positive finite duration`)\n const dur = typeof clip?.duration === 'number' && clip.duration > 0 ? clip.duration : Infinity\n if (!Array.isArray(clip?.keys) || clip.keys.length === 0) {\n problems.push(`${label} needs at least one key`)\n return { ok: false, problems }\n }\n let prev = -Infinity\n clip.keys.forEach((k, i) => {\n if (!(typeof k.t === 'number' && Number.isFinite(k.t)) || k.t < 0 || k.t > dur + 1e-9) problems.push(`${label} key ${i} has t=${String(k.t)} outside 0..${dur}`)\n else if (k.t < prev) problems.push(`${label} key ${i} goes back in time (${k.t} < ${prev})`)\n else prev = k.t\n for (const f of ['position', 'rotation'] as const) if (k[f] !== undefined && !finite3(k[f])) problems.push(`${label} key ${i} has an invalid ${f}`)\n for (const [pid, pose] of Object.entries(k.parts ?? {})) {\n if (!parts.has(pid)) problems.push(`${label} key ${i} moves unknown part \"${pid}\"`)\n for (const f of ['position', 'rotation'] as const) if (pose?.[f] !== undefined && !finite3(pose[f])) problems.push(`${label} key ${i} part \"${pid}\" has an invalid ${f}`)\n }\n })\n let prevEv = -Infinity\n ;(clip.events ?? []).forEach((e, i) => {\n if (!(typeof e.t === 'number' && Number.isFinite(e.t)) || e.t < 0 || e.t > dur + 1e-9) problems.push(`${label} event ${i} has t=${String(e.t)} outside 0..${dur}`)\n else if (e.t < prevEv) problems.push(`${label} event ${i} goes back in time`)\n else prevEv = e.t\n if (typeof e.type !== 'string' || e.type === '') problems.push(`${label} event ${i} needs a type`)\n try {\n JSON.stringify(e.data ?? {})\n } catch {\n problems.push(`${label} event ${i} data is not JSON`)\n }\n })\n return { ok: problems.length === 0, problems }\n}\n","import type { AnimationClipSpec, AnimationEventSpec, AnimationPose } from './animation'\nimport { checkAnimation } from './animation'\nimport type { HostEvent } from './module'\n\n/** Clips a tool can bring, one per move of its cycle. A tool brings the ones it has; a missing clip is simply not played. */\nexport const TOOL_CLIP = {\n DRAW: 'draw',\n SHOT: 'shot',\n MISFIRE: 'misfire',\n EMPTY: 'empty',\n CYCLE: 'cycle',\n RELOAD: 'reload',\n FEED: 'feed',\n FAULT: 'fault',\n CLEAR: 'clear',\n SAFETY_ON: 'safety_on',\n SAFETY_OFF: 'safety_off',\n MODE: 'mode',\n /** A throwable with the pin out, held until it is thrown. */\n COOK: 'cook',\n /** The stock folds, unfolds, slides in or out (variants `fold`, `unfold`, `collapse`, `extend`). */\n STOCK: 'stock',\n /** A light or a laser is switched (variants `on`, `off`). */\n TOGGLE: 'toggle',\n} as const\nexport type ToolClipId = (typeof TOOL_CLIP)[keyof typeof TOOL_CLIP]\n\n/** What a renderer does when a rule event arrives. `variant` picks `<clip>_<variant>` when the tool has it, else `<clip>`. */\nexport type ToolAnimationStep =\n | { readonly type: 'play'; readonly clip: ToolClipId; readonly variant?: string; readonly duration?: number; readonly repeat?: number }\n /** Move the clip that is playing to the marker (a clip event `stage` with `data.stage`) the rule just reached. */\n | { readonly type: 'sync'; readonly stage: string }\n /** The rule says the phase is over: the clip jumps to its end if it is still running. */\n | { readonly type: 'finish'; readonly clip: ToolClipId }\n\nconst stockVariant = (event: Extract<HostEvent, { type: 'stock' }>): string => {\n const isFolding = event.kind === undefined ? false : event.kind.includes('fold')\n if (event.state === 'collapsed') return isFolding ? 'fold' : 'collapse'\n return isFolding ? 'unfold' : 'extend'\n}\n\n/** The move a host event asks of the tool in hand, or null when it has no motion. */\nexport const toolAnimationStep = (event: HostEvent): ToolAnimationStep | null => {\n switch (event.type) {\n case 'tool':\n return { type: 'play', clip: TOOL_CLIP.DRAW, ...(event.duration === undefined ? {} : { duration: event.duration }) }\n case 'shot':\n return { type: 'play', clip: TOOL_CLIP.SHOT, repeat: Math.max(1, event.rounds ?? 1) }\n case 'misfire':\n return { type: 'play', clip: TOOL_CLIP.MISFIRE }\n case 'empty':\n return { type: 'play', clip: TOOL_CLIP.EMPTY }\n case 'mode':\n return { type: 'play', clip: TOOL_CLIP.MODE, variant: event.mode }\n case 'fault':\n return { type: 'play', clip: TOOL_CLIP.FAULT, variant: event.fault }\n case 'clear':\n if (event.phase === 'end') return { type: 'finish', clip: TOOL_CLIP.CLEAR }\n return { type: 'play', clip: TOOL_CLIP.CLEAR, variant: event.fault, ...(event.duration === undefined ? {} : { duration: event.duration }) }\n case 'safety':\n return { type: 'play', clip: event.on ? TOOL_CLIP.SAFETY_ON : TOOL_CLIP.SAFETY_OFF }\n case 'cycle':\n if (event.phase === 'end') return { type: 'finish', clip: TOOL_CLIP.CYCLE }\n return { type: 'play', clip: TOOL_CLIP.CYCLE, ...(event.duration === undefined ? {} : { duration: event.duration }) }\n case 'reload':\n if (event.phase === 'end') return { type: 'finish', clip: TOOL_CLIP.RELOAD }\n return { type: 'play', clip: TOOL_CLIP.RELOAD, ...(event.kind === undefined ? {} : { variant: event.kind }), ...(event.duration === undefined ? {} : { duration: event.duration }) }\n case 'stage':\n return { type: 'sync', stage: event.stage }\n case 'feed':\n return { type: 'play', clip: TOOL_CLIP.FEED }\n case 'cook':\n if (event.phase === 'start') return { type: 'play', clip: TOOL_CLIP.COOK, duration: event.fuse }\n return { type: 'finish', clip: TOOL_CLIP.COOK }\n case 'stock':\n return { type: 'play', clip: TOOL_CLIP.STOCK, variant: stockVariant(event), ...(event.duration === undefined ? {} : { duration: event.duration }) }\n case 'toggle':\n return { type: 'play', clip: TOOL_CLIP.TOGGLE, variant: event.on ? 'on' : 'off' }\n default:\n return null\n }\n}\n\n/** Where a renderer puts the poses: every part of the tool by id (the ids the clips move), and the whole tool. Poses are offsets from rest. */\nexport interface ToolRig {\n setPartPose(partId: string, pose: AnimationPose): void\n setRootPose?(pose: AnimationPose): void\n}\n\n/** Clips by id, with the variants as `<clip>_<variant>`. */\nexport type ToolClipSet = Readonly<Record<string, AnimationClipSpec>>\n\n/** A renderer's tool animator: feed it the host events and the frame time. */\nexport interface ToolAnimator {\n handle(event: HostEvent): void\n update(dt: number): void\n /** Ids of the clips playing now. */\n readonly playing: readonly string[]\n dispose(): void\n}\n\nexport const STAGE_EVENT = 'stage'\n\n/** The clip with its times (keys and events) scaled so that it lasts `duration` seconds. */\nexport const scaleClip = (clip: AnimationClipSpec, duration: number): AnimationClipSpec => {\n const factor = clip.duration > 0 ? duration / clip.duration : 1\n return {\n ...clip,\n duration,\n keys: clip.keys.map((key) => ({ ...key, t: key.t * factor })),\n ...(clip.events ? { events: clip.events.map((event) => ({ ...event, t: event.t * factor })) } : {}),\n }\n}\n\nconst ZERO: [number, number, number] = [0, 0, 0]\nconst mix = (from: readonly number[], to: readonly number[], fraction: number): [number, number, number] => [\n (from[0] ?? 0) + ((to[0] ?? 0) - (from[0] ?? 0)) * fraction,\n (from[1] ?? 0) + ((to[1] ?? 0) - (from[1] ?? 0)) * fraction,\n (from[2] ?? 0) + ((to[2] ?? 0) - (from[2] ?? 0)) * fraction,\n]\n\n/** Pose of the whole tool (`partId` undefined) or of one part at `time`, linear between keys; a key that does not name it means rest. */\nexport const sampleClip = (clip: AnimationClipSpec, partId: string | undefined, time: number): { position: [number, number, number]; rotation: [number, number, number] } => {\n const points = clip.keys.map((key) => {\n const pose = partId === undefined ? key : key.parts?.[partId]\n return { t: key.t, position: pose?.position ?? ZERO, rotation: pose?.rotation ?? ZERO }\n })\n const first = points[0]\n if (!first || first.t > 0) points.unshift({ t: 0, position: ZERO, rotation: ZERO })\n const last = points[points.length - 1]\n if (!last) return { position: ZERO, rotation: ZERO }\n if (time >= last.t) return { position: mix(last.position, last.position, 0), rotation: mix(last.rotation, last.rotation, 0) }\n const index = Math.max(0, points.findIndex((point, i) => time >= point.t && time < (points[i + 1]?.t ?? Infinity)))\n const from = points[index]\n const to = points[index + 1]\n if (!from || !to) return { position: ZERO, rotation: ZERO }\n const fraction = to.t > from.t ? (time - from.t) / (to.t - from.t) : 0\n return { position: mix(from.position, to.position, fraction), rotation: mix(from.rotation, to.rotation, fraction) }\n}\n\n/** Problems with a clip set (empty when valid): each key is the clip id (or `<clip>_<variant>`), every clip passes `checkAnimation`, and a `stage` event names its stage. */\nexport const checkToolClips = (clips: ToolClipSet, partIds: Iterable<string>): { ok: boolean; problems: string[] } => {\n const parts = [...partIds]\n const problems: string[] = []\n for (const [key, clip] of Object.entries(clips)) {\n if (clip.id !== key) problems.push(`clip \"${key}\" has the id \"${clip.id}\"`)\n if (!(clip.duration > 0)) problems.push(`clip \"${key}\" has no duration`)\n problems.push(...checkAnimation(clip, parts).problems)\n for (const event of clip.events ?? []) if (event.type === STAGE_EVENT && typeof event.data?.stage !== 'string') problems.push(`clip \"${key}\" has a stage event without data.stage`)\n }\n return { ok: problems.length === 0, problems }\n}\n\ninterface Instance {\n readonly name: string\n readonly clip: AnimationClipSpec\n time: number\n delay: number\n fired: number\n}\n\nexport interface ToolClipPlayerOptions {\n clips: ToolClipSet\n rig: ToolRig\n /** Called for each clip event as its time passes (`eject_casing`, `drop_magazine`, `stage`...). */\n onEvent?(event: AnimationEventSpec, clip: string): void\n /** Seconds between the rounds of a `repeat` (a burst): default 0.1. */\n repeatInterval?(): number\n}\n\n/** The default player of a tool's clips: runs the steps of `toolAnimationStep` against a rig, scaled to the duration the rule gives. */\nexport interface ToolClipPlayer {\n apply(step: ToolAnimationStep): void\n update(dt: number): void\n readonly playing: readonly string[]\n /** The clips being played with the time each has reached and the clip itself (scaled to the duration the rule gave), for a renderer that has to follow what the clip does now (the hands working a part). */\n readonly running: ReadonlyArray<{ readonly name: string; readonly time: number; readonly clip: AnimationClipSpec }>\n}\n\nexport const createToolClipPlayer = (options: ToolClipPlayerOptions): ToolClipPlayer => {\n const { clips, rig, onEvent } = options\n let instances: Instance[] = []\n\n const find = (clip: string, variant?: string): { name: string; clip: AnimationClipSpec } | undefined => {\n const name = variant !== undefined && clips[`${clip}_${variant}`] ? `${clip}_${variant}` : clip\n const found = clips[name]\n return found ? { name, clip: found } : undefined\n }\n\n const render = (instance: Instance): void => {\n const time = Math.min(instance.time, instance.clip.duration)\n const root = sampleClip(instance.clip, undefined, time)\n if (instance.clip.keys.some((key) => key.position || key.rotation)) rig.setRootPose?.(root)\n const touched = new Set(instance.clip.keys.flatMap((key) => Object.keys(key.parts ?? {})))\n for (const id of touched) rig.setPartPose(id, sampleClip(instance.clip, id, time))\n }\n\n const fireEvents = (instance: Instance, upTo: number): void => {\n const events = instance.clip.events ?? []\n while (instance.fired < events.length && (events[instance.fired]?.t ?? Infinity) <= upTo + 1e-9) {\n const event = events[instance.fired]\n instance.fired += 1\n if (event) onEvent?.(event, instance.name)\n }\n }\n\n return {\n apply(step) {\n if (step.type === 'sync') {\n for (const instance of instances) {\n const marker = (instance.clip.events ?? []).find((event) => event.type === STAGE_EVENT && event.data?.stage === step.stage)\n if (marker && instance.time < marker.t) instance.time = marker.t\n }\n return\n }\n if (step.type === 'finish') {\n for (const instance of instances) {\n if (instance.name !== step.clip && !instance.name.startsWith(`${step.clip}_`)) continue\n fireEvents(instance, instance.clip.duration)\n instance.time = instance.clip.duration\n }\n return\n }\n const found = find(step.clip, step.variant)\n if (!found) return\n const clip = step.duration && step.duration > 0 ? scaleClip(found.clip, step.duration) : found.clip\n const interval = options.repeatInterval?.() ?? 0.1\n instances = instances.filter((existing) => existing.name !== found.name || existing.delay > 0)\n for (let round = 0; round < (step.repeat ?? 1); round++) {\n instances.push({ name: found.name, clip, time: 0, delay: round * interval, fired: 0 })\n }\n },\n update(dt) {\n for (const instance of instances) {\n const carried = instance.delay > 0 ? dt - instance.delay : dt\n if (instance.delay > 0) {\n instance.delay = Math.max(0, instance.delay - dt)\n if (instance.delay > 0) continue\n }\n instance.time += carried\n fireEvents(instance, instance.time)\n render(instance)\n }\n instances = instances.filter((instance) => instance.delay > 0 || instance.time < instance.clip.duration)\n },\n get playing() {\n return instances.map((instance) => instance.name)\n },\n get running() {\n return instances.filter((instance) => instance.delay <= 0).map((instance) => ({ name: instance.name, time: instance.time, clip: instance.clip }))\n },\n }\n}\n","type PlainObject = Record<string, unknown>\ntype Entry = { readonly id: string }\n\n/** Keys that would reach the prototype of the result when a patch comes from parsed JSON. */\nconst UNSAFE_KEYS: ReadonlySet<string> = new Set(['__proto__', 'constructor', 'prototype'])\n\nconst isPlainObject = (value: unknown): value is PlainObject =>\n typeof value === 'object' && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype\n\n/** Recursively optional; arrays and functions are replaced as a whole, objects are merged. */\nexport type DeepPartial<TValue> = TValue extends (...args: never[]) => unknown\n ? TValue\n : TValue extends readonly unknown[]\n ? TValue\n : TValue extends object\n ? { [TKey in keyof TValue]?: DeepPartial<TValue[TKey]> }\n : TValue\n\n/** Merges `patch` over `base`: plain objects merge field by field, arrays, functions and primitives replace. `undefined` in the patch is ignored. */\nexport const deepMerge = <TBase extends object>(base: TBase, patch: object | undefined): TBase => {\n if (!patch) return base\n const result: PlainObject = { ...(base as PlainObject) }\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined || UNSAFE_KEYS.has(key)) continue\n const current = result[key]\n result[key] = isPlainObject(current) && isPlainObject(value) ? deepMerge(current, value) : value\n }\n return result as TBase\n}\n\n/** The shipped entries with the ones from `extra` added or, when the id exists, replaced. */\nexport const mergeById = <TEntry extends Entry>(shipped: readonly TEntry[], extra: readonly TEntry[] | undefined): Map<string, TEntry> => {\n const entries = new Map<string, TEntry>()\n for (const entry of [...shipped, ...(extra ?? [])]) entries.set(entry.id, entry)\n return entries\n}\n\n/** Applies `patch` by id; a patch for an id that does not exist is ignored. */\nexport const applyPatches = <TEntry extends Entry>(entries: Map<string, TEntry>, patches: Readonly<Record<string, DeepPartial<TEntry> | Partial<TEntry>>> | undefined): void => {\n for (const [id, patch] of Object.entries(patches ?? {})) {\n const entry = entries.get(id)\n if (entry) entries.set(id, deepMerge(entry, patch))\n }\n}\n\nexport const removeIds = (entries: Map<string, unknown>, ids: readonly string[] | undefined): void => {\n for (const id of ids ?? []) entries.delete(id)\n}\n","import type { Vec3 } from './destruction'\n\n/**\n * The bench of a module: controls, layers and counters it declares as data. The host keeps the values, draws them in a generic way and hands\n * every change back through `ModuleHost.bench`. Nothing here is specific to a kind of module.\n */\nexport type ControlValue = number | boolean | string\n\nexport interface ChoiceOption {\n readonly id: string\n readonly name: string\n /** Heading the host lists the option under (`optgroup`, a grouped row of buttons). Options of a group must sit next to each other. */\n readonly group?: string\n}\n\ninterface ControlBase {\n /** snake_case, unique across the module's controls, layers and metrics. */\n readonly id: string\n /** Display name, already translated for the locale the host asked for. */\n readonly name: string\n readonly hint?: string\n readonly hidden?: boolean\n /**\n * The change (or the press, for an action) takes a noticeable time, a third of a second or more: the host shows its loading indicator while it works, with this text, or the control's name for `true`.\n * Only the host's own controls panel does it; a change made through `TestRoom.bench().set / act` runs at once.\n */\n readonly slow?: boolean | string\n}\n\nexport interface NumberControl extends ControlBase {\n readonly kind: 'number'\n readonly min: number\n readonly max: number\n readonly step?: number\n readonly unit?: string\n readonly default: number\n}\n\nexport interface BooleanControl extends ControlBase {\n readonly kind: 'boolean'\n readonly default: boolean\n}\n\nexport interface ChoiceControl extends ControlBase {\n readonly kind: 'choice'\n readonly options: readonly ChoiceOption[]\n readonly default: string\n}\n\n/** A button: no value, only the press. */\nexport interface ActionControl extends ControlBase {\n readonly kind: 'action'\n}\n\nexport type ModuleControl = NumberControl | BooleanControl | ChoiceControl | ActionControl\n\n/** Something of the module the viewer turns on or off (smoke, flash, casings). */\nexport interface ModuleLayer {\n readonly id: string\n readonly name: string\n /** On unless `false`. */\n readonly default?: boolean\n}\n\n/** Pulled: the room calls read only when it samples (~4 Hz, or on TestRoom.metrics()). */\nexport interface ModuleMetric {\n readonly id: string\n readonly name: string\n readonly unit?: string\n /** Budget: the host shows a bar and warns past it. */\n readonly max?: number\n readonly digits?: number\n read(): number | string\n}\n\nexport type BenchChange =\n | { readonly kind: 'control'; readonly id: string; readonly value: ControlValue }\n | { readonly kind: 'layer'; readonly id: string; readonly on: boolean }\n | { readonly kind: 'action'; readonly id: string }\n\nexport interface ControlPatch {\n readonly hidden?: boolean\n readonly disabled?: boolean\n readonly options?: readonly ChoiceOption[]\n readonly value?: ControlValue\n}\n\n/** What the host lends a module that declares a bench: the current values and the changes. */\nexport interface ModuleBench {\n value(id: string): ControlValue | undefined\n layer(id: string): boolean\n /** Right after attach returns, the host sends one change per control (not actions) and per layer, in declaration order. */\n onChange(listener: (change: BenchChange) => void): () => void\n /** `value` sets without emitting (no echo loops); `options` falls back to the first option if the value is no longer offered. */\n patch(id: string, patch: ControlPatch): void\n /** Move the host camera (a near / middle / far choice). */\n look(eye: Vec3, target: Vec3): void\n /** Shows a sheet on the host's panel, or takes it down with `null`. A host without a sheet area ignores it. */\n sheet?(sheet: BenchSheet | null): void\n}\n\n/**\n * A card of text the module puts on the host's panel (the sheet of an effect, of a weapon): what it is, its numbers, its steps, its colours and,\n * optionally, a strip of segments over time. Names and texts are already translated for the host's locale.\n */\nexport interface BenchSheet {\n readonly kicker?: string\n readonly title: string\n readonly body?: string\n readonly facts?: readonly { readonly label: string; readonly value: string }[]\n readonly steps?: readonly { readonly at: string; readonly text: string }[]\n readonly swatches?: readonly { readonly color: string; readonly name: string }[]\n readonly note?: string\n /** A strip of named segments (seconds), with tick marks and a playhead that follows the module metric `metric`. */\n readonly timeline?: {\n readonly segments: readonly { readonly name: string; readonly start: number; readonly end: number; readonly color: string }[]\n readonly ticks: readonly number[]\n readonly metric: string\n }\n}\n\nexport interface BenchReport {\n readonly ok: boolean\n readonly problems: readonly string[]\n}\n\nexport interface BenchDeclaration {\n readonly controls?: readonly ModuleControl[]\n readonly layers?: readonly ModuleLayer[]\n readonly metrics?: readonly ModuleMetric[]\n}\n\nconst SNAKE_CASE = /^[a-z][a-z0-9_]*$/\nconst finite = (n: unknown): n is number => typeof n === 'number' && Number.isFinite(n)\n\nfunction checkControl(control: ModuleControl, problems: string[]): void {\n const label = `control \"${control.id}\"`\n if (control.kind === 'number') {\n if (!finite(control.min) || !finite(control.max) || control.min >= control.max) problems.push(`${label} needs min < max`)\n if (control.step !== undefined && !(finite(control.step) && control.step > 0)) problems.push(`${label} needs a step > 0`)\n if (!finite(control.default) || control.default < control.min || control.default > control.max) problems.push(`${label} default ${String(control.default)} is outside ${control.min}..${control.max}`)\n return\n }\n if (control.kind !== 'choice') return\n if (control.options.length === 0) {\n problems.push(`${label} needs at least one option`)\n return\n }\n const seen = new Set<string>()\n const groups = new Set<string>()\n let previousGroup: string | undefined\n for (const option of control.options) {\n if (seen.has(option.id)) problems.push(`${label} repeats option \"${option.id}\"`)\n seen.add(option.id)\n if (option.group !== undefined && (typeof option.group !== 'string' || option.group === '')) problems.push(`${label} option \"${option.id}\" has an empty group`)\n else if (option.group !== undefined && option.group !== previousGroup && groups.has(option.group)) problems.push(`${label} group \"${option.group}\" is split: its options must be next to each other`)\n if (typeof option.group === 'string' && option.group !== '') groups.add(option.group)\n previousGroup = option.group\n }\n if (!seen.has(control.default)) problems.push(`${label} default \"${control.default}\" is not among its options`)\n}\n\n/** snake_case ids unique across controls, layers and metrics; min < max; step > 0; default in range; options non-empty, unique, default among them, each group in one run; metric max > 0. */\nexport function checkBench(declared: BenchDeclaration): BenchReport {\n const problems: string[] = []\n const ids = new Set<string>()\n const claim = (kind: string, id: string): void => {\n if (!SNAKE_CASE.test(id)) problems.push(`${kind} id \"${id}\" must be snake_case`)\n if (ids.has(id)) problems.push(`duplicate id \"${id}\" across controls, layers and metrics`)\n ids.add(id)\n }\n for (const control of declared.controls ?? []) {\n claim('control', control.id)\n checkControl(control, problems)\n }\n for (const layer of declared.layers ?? []) claim('layer', layer.id)\n for (const metric of declared.metrics ?? []) {\n claim('metric', metric.id)\n if (metric.max !== undefined && !(finite(metric.max) && metric.max > 0)) problems.push(`metric \"${metric.id}\" needs a max > 0`)\n }\n return { ok: problems.length === 0, problems }\n}\n","/** The weather, light and ground as the world is right now. Written by whoever drives the weather, read by whoever reacts to it. */\nexport interface EnvironmentState {\n /** Rain in mm/h; 0 is dry. */\n readonly rain: number\n /** Wind in m/s along x and z, and its strength (the length of that vector). */\n readonly windX: number\n readonly windZ: number\n readonly windSpeed: number\n /** Strongest gust in m/s. */\n readonly gust: number\n /** Render fog far distance in metres. */\n readonly fogFar: number\n /** How far one can see for gameplay, in metres (not the render fog). */\n readonly visibility: number\n /** 0 is full daylight, 1 is the darkest night. */\n readonly nightFactor: number\n readonly temperatureC: number\n /** Wet ground, 0..1: rises with rain and dries after it stops. */\n readonly wetness: number\n}\n\nexport type EnvironmentKey = keyof EnvironmentState\n\n/** A calm dry day (the normal world, before anything went wrong). */\nexport const DEFAULT_ENVIRONMENT: EnvironmentState = {\n rain: 0,\n windX: 0,\n windZ: 0,\n windSpeed: 0,\n gust: 0,\n fogFar: 220,\n visibility: 220,\n nightFactor: 0,\n temperatureC: 20,\n wetness: 0,\n}\n\n/** The host's environment (`ModuleHost.environment`): a store the weather module writes to and the others read. */\nexport interface EnvironmentBus {\n get(): EnvironmentState\n /** Merges `patch` into the state; listeners hear it only when a value really changed. */\n set(patch: Partial<EnvironmentState>): void\n /** Hears every change with the keys that changed. Returns the unsubscribe function. */\n on(listener: (state: EnvironmentState, changed: readonly EnvironmentKey[]) => void): () => void\n}\n\n/** A plain store that implements the bus; the test room and tests use it, a game may bring its own. */\nexport function createEnvironmentBus(initial: Partial<EnvironmentState> = {}): EnvironmentBus {\n let state: EnvironmentState = { ...DEFAULT_ENVIRONMENT, ...initial }\n const listeners = new Set<(state: EnvironmentState, changed: readonly EnvironmentKey[]) => void>()\n return {\n get: () => state,\n set(patch) {\n const changed = (Object.keys(patch) as EnvironmentKey[]).filter((key) => patch[key] !== undefined && patch[key] !== state[key])\n if (changed.length === 0) return\n const next = { ...state } as { -readonly [Key in EnvironmentKey]: number }\n for (const key of changed) next[key] = patch[key] as number\n state = next\n for (const listener of [...listeners]) listener(state, changed)\n },\n on(listener) {\n listeners.add(listener)\n return () => void listeners.delete(listener)\n },\n }\n}\n","import { applyPatches, deepMerge, mergeById, removeIds } from './registry'\nimport type { DeepPartial } from './registry'\n\n/** One category (kind) of items. Defaults and required contracts are inherited from the `extends` chain. */\nexport interface CategoryDef<TDefaults extends object = Record<string, unknown>> {\n readonly id: string\n /** Parent category id; the child inherits its defaults and its required contracts. */\n readonly extends?: string\n /** Field values every entry of this kind starts with; objects merge down the chain, arrays replace. */\n readonly defaults?: DeepPartial<TDefaults>\n /** Contract ids (`ContractChecker.id`) every entry of this kind, or of a descendant, must satisfy. */\n readonly requires?: readonly string[]\n /** An abstract category only groups: entries cannot declare it directly, only its descendants. */\n readonly abstract?: boolean\n}\n\n/** Where a checker is being run: the entry's kind and the chain of categories root first. */\nexport interface ContractContext {\n readonly kind: string\n readonly chain: readonly string[]\n}\n\n/** A named contract: returns the problems of an entry, each a short phrase like `missing leftover.despawnSeconds`. Empty means it holds. */\nexport interface ContractChecker<TEntry> {\n readonly id: string\n check(entry: TEntry, ctx: ContractContext): string[]\n}\n\nexport interface CategoryOptions {\n /** Categories added to the shipped ones; the same id replaces. */\n readonly extra?: readonly CategoryDef[]\n /** Partial changes by id, applied after `extra`. */\n readonly patches?: Readonly<Record<string, DeepPartial<CategoryDef> | Partial<CategoryDef>>>\n /** Ids taken out of the tree (their children are then reported by `check()`). */\n readonly remove?: readonly string[]\n}\n\nexport interface CategoryRegistry {\n get(id: string): CategoryDef | undefined\n has(id: string): boolean\n ids(): string[]\n /** Root first, ending at `id`. Throws on an unknown id, an unknown parent or a cycle. */\n chain(id: string): string[]\n /** `defaults` merged along the chain, root first, so the closest category wins. */\n defaults(id: string): Record<string, unknown>\n /** Union of `requires` along the chain, order kept, without duplicates. */\n requires(id: string): string[]\n /** True when `id` is `ancestorId` or descends from it. */\n isA(id: string, ancestorId: string): boolean\n /** Problems in the tree itself: duplicate ids, unknown `extends`, cycles. */\n check(): string[]\n}\n\nexport interface CategorizedOptions {\n /** Field of the entry that names its category. Default `kind`. */\n readonly kindField?: string\n /** Known gaps by entry id: problems starting with one of these texts go to `allowed`, so the list can shrink over time. */\n readonly allow?: Readonly<Record<string, readonly string[]>>\n}\n\nexport interface CategorizedReport {\n readonly ok: boolean\n readonly problems: string[]\n readonly allowed: string[]\n}\n\nexport const defineCategories = (defs: readonly CategoryDef[], options: CategoryOptions = {}): CategoryRegistry => {\n const problems: string[] = []\n const duplicates = (list: readonly CategoryDef[]): void => {\n const ids = new Set<string>()\n for (const def of list) {\n if (ids.has(def.id)) problems.push(`duplicate category \"${def.id}\"`)\n ids.add(def.id)\n }\n }\n duplicates(defs)\n duplicates(options.extra ?? [])\n\n const entries = mergeById(defs, options.extra)\n applyPatches(entries, options.patches)\n removeIds(entries, options.remove)\n\n const chain = (id: string): string[] => {\n const path: string[] = []\n let current: string | undefined = id\n while (current !== undefined) {\n const def = entries.get(current)\n if (!def) throw new Error(path.length === 0 ? `unknown category \"${current}\"` : `category \"${path[0]}\" has unknown parent \"${current}\"`)\n if (path.includes(current)) throw new Error(`category cycle: ${[...path, current].reverse().join(' -> ')}`)\n path.push(current)\n current = def.extends\n }\n return path.reverse()\n }\n\n const registry: CategoryRegistry = {\n get: (id) => entries.get(id),\n has: (id) => entries.has(id),\n ids: () => [...entries.keys()],\n chain,\n defaults: (id) => chain(id).reduce<Record<string, unknown>>((acc, ancestor) => deepMerge(acc, entries.get(ancestor)?.defaults), {}),\n requires: (id) => {\n const out: string[] = []\n for (const ancestor of chain(id)) for (const contract of entries.get(ancestor)?.requires ?? []) if (!out.includes(contract)) out.push(contract)\n return out\n },\n isA: (id, ancestorId) => {\n try {\n return chain(id).includes(ancestorId)\n } catch {\n return false\n }\n },\n check: () => {\n const found = [...problems]\n for (const [id, def] of entries) {\n if (def.extends !== undefined && !entries.has(def.extends)) found.push(`category \"${id}\" extends unknown \"${def.extends}\"`)\n else {\n try {\n chain(id)\n } catch (error) {\n const message = (error as Error).message\n // The same cycle is found from each of its members; the dedupe keeps one line per distinct message.\n if (message.startsWith('category cycle') && !found.includes(message)) found.push(message)\n }\n }\n }\n return found\n },\n }\n return registry\n}\n\n/** The entry with the defaults of its category underneath: the entry wins, objects merge, arrays replace. */\nexport const resolveEntry = <TEntry extends object>(registry: CategoryRegistry, entry: TEntry, kindField = 'kind'): TEntry =>\n deepMerge(registry.defaults(String((entry as Record<string, unknown>)[kindField])) as TEntry, entry)\n\n/** Checks that each entry has a known, concrete kind and satisfies every contract its chain requires. */\nexport const checkCategorized = <TEntry extends { readonly id: string }>(\n entries: readonly TEntry[],\n registry: CategoryRegistry,\n checkers: readonly ContractChecker<TEntry>[],\n options: CategorizedOptions = {},\n): CategorizedReport => {\n const kindField = options.kindField ?? 'kind'\n const byId = new Map(checkers.map((checker) => [checker.id, checker]))\n const problems: string[] = [...registry.check()]\n const allowed: string[] = []\n\n for (const entry of entries) {\n const kind = String((entry as Record<string, unknown>)[kindField])\n if (!registry.has(kind)) {\n problems.push(`\"${entry.id}\" has unknown kind \"${kind}\"`)\n continue\n }\n if (registry.get(kind)?.abstract) {\n problems.push(`\"${entry.id}\" has abstract kind \"${kind}\"`)\n continue\n }\n let path: string[]\n try {\n path = registry.chain(kind)\n } catch (error) {\n problems.push(`\"${entry.id}\" (${kind}): ${(error as Error).message}`)\n continue\n }\n const resolved = resolveEntry(registry, entry, kindField)\n for (const contract of registry.requires(kind)) {\n const owner = path.find((ancestor) => registry.get(ancestor)?.requires?.includes(contract)) ?? kind\n const checker = byId.get(contract)\n if (!checker) {\n problems.push(`kind \"${kind}\" requires contract \"${contract}\" but no checker is registered`)\n continue\n }\n const prefix = `\"${entry.id}\" (${kind}): `\n for (const problem of checker.check(resolved, { kind, chain: path })) {\n const text = `${prefix}${problem}, required by ${owner}`\n if ((options.allow?.[entry.id] ?? []).some((start) => problem.startsWith(start))) allowed.push(text)\n else problems.push(text)\n }\n }\n }\n return { ok: problems.length === 0, problems: [...new Set(problems)], allowed }\n}\n","/**\n * Where a drawn object comes from. Every builder of the kit that draws a piece (a car of the fleet, a house or a wall, a piece of furniture, a person, an\n * animal, a plant, an item, a weapon) marks the root it returns, and every renderer that draws for the room (effects, debris, liquids, the room itself)\n * marks its own roots, so a host can tell a piece of the kit from something drawn on the spot. Duck-typed on three.js objects (`userData`, `parent`,\n * `children`), so the contracts stay free of three.js.\n */\nexport const KIT_ORIGIN_KEY = 'kitOrigin'\n\n/** What drew an object: the package and the kind of piece (`vehicle`, `structure`, `furniture`, `person`... or `effect`, `room` for what is not a piece). */\nexport interface KitOrigin {\n readonly owner: string\n readonly kind: string\n /** The catalogue id of the piece, when it has one. */\n readonly id?: string\n /** Drawn as a simple stand-in shape because the kit has no model of the piece yet (an item with no team model): a model still to make. */\n readonly standIn?: boolean\n}\n\ninterface Node {\n userData?: Record<string, unknown>\n parent?: Node | null\n children?: readonly Node[]\n name?: string\n visible?: boolean\n isMesh?: boolean\n isPoints?: boolean\n isLine?: boolean\n isSprite?: boolean\n}\n\n/** Marks `object` as drawn by `origin.owner` and returns it. */\nexport function markKitOrigin<T>(object: T, origin: KitOrigin): T {\n const node = object as unknown as Node\n node.userData = node.userData ?? {}\n node.userData[KIT_ORIGIN_KEY] = { ...origin }\n return object\n}\n\n/** The origin of the object or of its nearest marked ancestor; null when nothing up the tree is marked. */\nexport function kitOriginOf(object: unknown): KitOrigin | null {\n for (let node = object as Node | null | undefined; node; node = node.parent) {\n const origin = node.userData?.[KIT_ORIGIN_KEY] as KitOrigin | undefined\n if (origin && typeof origin.owner === 'string') return origin\n }\n return null\n}\n\n/** A drawn thing that no builder of the kit marked: what was drawn on the spot. */\nexport interface UnmarkedDrawing {\n /** The names of the object and its named ancestors, outermost first. */\n readonly path: string\n readonly object: unknown\n}\n\n/** Every visible mesh, line, point cloud or sprite under `root` with no marked ancestor (`root` itself not counted as one). */\nexport function unmarkedDrawings(root: unknown): UnmarkedDrawing[] {\n const out: UnmarkedDrawing[] = []\n const pathOf = (node: Node): string => {\n const names: string[] = []\n for (let n: Node | null | undefined = node; n && n !== root; n = n.parent) if (n.name) names.push(n.name)\n return names.reverse().join(' > ') || '(no name)'\n }\n const walk = (node: Node, marked: boolean): void => {\n if (node.visible === false) return\n const here = marked || (node !== root && Boolean((node.userData?.[KIT_ORIGIN_KEY] as KitOrigin | undefined)?.owner))\n if (!here && (node.isMesh || node.isPoints || node.isLine || node.isSprite)) out.push({ path: pathOf(node), object: node })\n for (const child of node.children ?? []) walk(child, here)\n }\n walk(root as Node, false)\n return out\n}\n","/** A floor rectangle in an object's own space, metres (x across, z along). */\nexport interface Footprint {\n readonly minX: number\n readonly maxX: number\n readonly minZ: number\n readonly maxZ: number\n}\n\n/** What `footprintBlocker` reads of an object: its world matrix (three.js `Object3D`, duck-typed so the contracts stay free of three.js). */\nexport interface FootprintObject {\n readonly matrixWorld: { readonly elements: ArrayLike<number> }\n readonly parent?: unknown\n updateWorldMatrix?(updateParents: boolean, updateChildren: boolean): void\n}\n\n/**\n * The `ModuleHost.addBlocker` function of a solid object standing on the floor: the walking player stays out of `footprint`, a rectangle in the object's own space,\n * so the object blocks turned with it and follows it when it moves. An object out of the scene (no parent) blocks nothing.\n */\nexport function footprintBlocker(object: FootprintObject | (() => FootprintObject | null | undefined), footprint: Footprint | (() => Footprint | null | undefined)): (x: number, z: number, radius: number) => boolean {\n return (x, z, radius) => {\n const o = typeof object === 'function' ? object() : object\n const fp = typeof footprint === 'function' ? footprint() : footprint\n if (!o || !fp || !o.parent) return false\n o.updateWorldMatrix?.(true, false)\n const e = o.matrixWorld.elements\n // the world point in the object's own floor axes: its x and z columns are at right angles, so a projection on each (over its squared length, for a scale) inverts the matrix\n const dx = x - (e[12] as number)\n const dz = z - (e[14] as number)\n const ax = (e[0] as number) ** 2 + (e[1] as number) ** 2 + (e[2] as number) ** 2\n const az = (e[8] as number) ** 2 + (e[9] as number) ** 2 + (e[10] as number) ** 2\n const lx = (dx * (e[0] as number) + dz * (e[2] as number)) / (ax || 1)\n const lz = (dx * (e[8] as number) + dz * (e[10] as number)) / (az || 1)\n return lx > fp.minX - radius && lx < fp.maxX + radius && lz > fp.minZ - radius && lz < fp.maxZ + radius\n }\n}\n","import type { WeatherLayerId } from '../renderer'\n\n/** The names of the visual layers in the bench; any other locale falls back to English. */\nexport type WeatherVisualText = { readonly layers: Readonly<Record<Exclude<WeatherLayerId, 'fog'>, string>> }\n\nconst TEXTS: Readonly<Record<string, WeatherVisualText>> = {\n en: { layers: { far_rain: 'Rain curtain', near_rain: 'Near rain and splashes', mist: 'Mist and ash', sky: 'Sky', lightning: 'Lightning', headlights: 'Headlights', haze: 'Heat haze' } },\n 'pt-BR': { layers: { far_rain: 'Cortina de chuva', near_rain: 'Chuva perto e respingos', mist: 'Névoa e cinza', sky: 'Céu', lightning: 'Relâmpago', headlights: 'Faróis', haze: 'Névoa de calor' } },\n}\n\nexport const weatherVisualTextOf = (locale: string): WeatherVisualText => TEXTS[locale] ?? TEXTS[locale.split('-')[0] ?? 'en'] ?? TEXTS.en!\n","import { footprintBlocker } from '@adatechnology/game-contracts'\nimport type { Footprint, ModuleHost, ModuleLayer } from '@adatechnology/game-contracts'\nimport type { VfxWorld } from '@adatechnology/game-vfx'\nimport type { WeatherVisual, WeatherVisualContext, WeatherVisualFactory } from '@adatechnology/game-weather'\nimport type * as THREE_NS from 'three'\nimport { HEADLIGHTS } from '../headlights'\nimport { createWeatherRenderer } from '../renderer'\nimport type { WeatherLayerId, WeatherRenderer, WeatherRendererOptions } from '../renderer'\nimport type { ThreeLib } from '../types'\nimport { weatherVisualTextOf } from './text'\n\nexport type WeatherVisualOptions = Pick<WeatherRendererOptions, 'profile' | 'random' | 'zone' | 'puddles' | 'puddleAt' | 'headlights'> & {\n /** The effects world of the room (`room.vfx`); without it the near rain, mist, ash, white flash and lit drops are left out. */\n readonly vfx?: VfxWorld\n /** Where the lamp rig stands (the two headlights of the Workshop's car, to see the beams); `false` leaves it out. Default 2.5 m ahead of the test room camera, lamps toward it. */\n readonly rig?: readonly [x: number, z: number, turn: number] | false\n /**\n * The car the lamps are mounted on, built by the vehicles SDK for the host's three.js (`createVehiclesThree` of game-vehicles-three with the fleet installed), so the rig follows the fleet when it\n * changes. Without it the rig has only the lamps and their beams: no car drawn here.\n */\n readonly vehicles?: (THREE: ThreeLib) => RigVehicles\n /** Fleet id of the rig's car (default `midsize_sedan`). */\n readonly rigVehicle?: string\n /** Which layers start on (all do by default); the room that is not about weather starts the sky and the fog off to keep its own look. */\n readonly layers?: Partial<Record<WeatherLayerId, boolean>>\n}\n\n/** What the rig needs of the vehicles SDK (`VehiclesThree` of game-vehicles-three): a car in the game frame (front on +z, wheels on y = 0) and its length. */\nexport interface RigVehicles {\n build(key: string, options?: { quality?: 'high' | 'lite' }): { readonly group: THREE_NS.Object3D; readonly length: number; readonly width?: number; dispose(): void }\n}\n\nconst RIG_VEHICLE = 'midsize_sedan'\nconst LAYER_ORDER: readonly Exclude<WeatherLayerId, 'fog'>[] = ['far_rain', 'near_rain', 'mist', 'sky', 'lightning', 'headlights', 'haze']\nconst RIG_DEFAULT = [1.6, -2.5, 0] as const\n\n/**\n * The lamps of the Workshop's car and, when the vehicles SDK is lent, a car of the fleet under them, its nose just ahead of the lamps: what the headlights are attached to when the viewer has no\n * car in the room.\n */\nfunction buildRig(THREE: ThreeLib, vehicles: RigVehicles | undefined, key: string): { readonly group: THREE_NS.Group; readonly footprint?: Footprint; dispose(): void } {\n const group = new THREE.Group()\n group.name = 'weather-lamp-rig'\n const car = vehicles?.build(key, { quality: 'lite' })\n if (car) {\n car.group.position.z = HEADLIGHTS.lamp[2] + 0.1 - car.length / 2\n group.add(car.group)\n }\n // the car is solid: the walking player stops at its body (its length and width, measured by the vehicles SDK) instead of walking through it\n const footprint = car ? footprintOf(car) : undefined\n return {\n group,\n ...(footprint ? { footprint } : {}),\n dispose() {\n car?.dispose()\n },\n }\n}\n\n/**\n * The picture of the weather for the weather bench: `createWeatherTestModule({ visual: createWeatherVisual({ vfx: room.vfx }) })`. It draws the weather the bench shows with\n * `createWeatherRenderer` on the room's scene and camera, switches each part with the bench layers and puts a lamp rig in front of the camera for the headlights.\n */\nexport function createWeatherVisual(options: WeatherVisualOptions = {}): WeatherVisualFactory {\n return {\n layers(locale): readonly ModuleLayer[] {\n const text = weatherVisualTextOf(locale)\n return LAYER_ORDER.map((id) => ({ id, name: text.layers[id], default: options.layers?.[id] ?? true }))\n },\n create({ host, weather }: WeatherVisualContext): WeatherVisual {\n const THREE = host.THREE as ThreeLib\n const renderer: WeatherRenderer = createWeatherRenderer(THREE, {\n scene: host.scene as THREE_NS.Scene,\n camera: host.camera as THREE_NS.Camera,\n weather,\n ...(options.vfx ? { vfx: options.vfx } : {}),\n ...(options.profile ? { profile: options.profile } : {}),\n ...(options.random ? { random: options.random } : {}),\n ...(options.zone ? { zone: options.zone } : {}),\n ...(options.puddles ? { puddles: options.puddles } : {}),\n ...(options.puddleAt ? { puddleAt: options.puddleAt } : {}),\n ...(options.headlights ? { headlights: options.headlights } : {}),\n ...(options.layers ? { layers: options.layers } : {}),\n })\n return attachRig(THREE, host, renderer, options.rig === undefined ? RIG_DEFAULT : options.rig, options.vehicles?.(THREE), options.rigVehicle ?? RIG_VEHICLE)\n },\n }\n}\n\nfunction footprintOf(car: { readonly group: THREE_NS.Object3D; readonly length: number; readonly width?: number }): Footprint | undefined {\n if (!car.width) return undefined\n const z = car.group.position.z\n return { minX: -car.width / 2, maxX: car.width / 2, minZ: z - car.length / 2, maxZ: z + car.length / 2 }\n}\n\nfunction attachRig(THREE: ThreeLib, host: ModuleHost, renderer: WeatherRenderer, place: readonly [number, number, number] | false, vehicles: RigVehicles | undefined, key: string): WeatherVisual {\n const scene = host.scene as THREE_NS.Scene\n const rig = place === false ? undefined : buildRig(THREE, vehicles, key)\n let detach: (() => void) | undefined\n let unblock: (() => void) | undefined\n if (rig && place) {\n rig.group.position.set(place[0], 0, place[1])\n rig.group.rotation.y = place[2]\n scene.add(rig.group)\n detach = renderer.headlights.attach(rig.group)\n if (rig.footprint) unblock = host.addBlocker?.(footprintBlocker(rig.group, rig.footprint))\n }\n return {\n update: (dt) => renderer.update(dt),\n setLayer(id, on) {\n if ((WEATHER_IDS as readonly string[]).includes(id)) renderer.setLayer(id as WeatherLayerId, on)\n },\n dispose() {\n detach?.()\n unblock?.()\n if (rig) {\n scene.remove(rig.group)\n rig.dispose()\n }\n renderer.dispose()\n },\n }\n}\n\nconst WEATHER_IDS: readonly WeatherLayerId[] = ['far_rain', 'near_rain', 'mist', 'fog', 'sky', 'lightning', 'headlights', 'haze']\n"],"mappings":"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","names":["camera","lens","camera","flash","screen","lerp","behavior","spawn","call","UP","FRESH_BLOOD","local","BLOOD","SPEED","FORWARD","BODY_Z","TIP","PORT","WALL_Z","FACING","CENTRE","call","UP","FALL_SECONDS","BODIES","SPLINTER_DIRECTION","at","SCRAPE_DIRECTION","CAR_ORIGIN","CAR_ROTATION","WALK_SPEED","DROPS","SLANT","SPEED","SPARK_DIRECTION","call","UP","HITS","HIT_X","HIT_Y","FRESH_BLOOD","blow","WALKERS","FACING","FORWARD","ORIGIN","puff","at","MUZZLE_FLASH","QUARTER_TURN","QUARTER_TURN","camera","camera","camera"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/sky.ts","../src/fog.ts","../src/headlights.ts","../src/heat-haze.ts","../src/lightning.ts","../../game-vfx/src/area-cap.ts","../../game-vfx/src/budget.ts","../../game-vfx/src/data/explosion-aliases.json","../../game-vfx/src/data/explosions.json","../../game-vfx/src/catalog.ts","../../game-vfx/src/color.ts","../../game-vfx/src/constants.ts","../../game-vfx/src/pool.ts","../../game-vfx/src/pools/chunks.ts","../../game-vfx/src/vector.ts","../../game-vfx/src/pools/glows.ts","../../game-vfx/src/pools/puffs.ts","../../game-vfx/src/pools/sparks.ts","../../game-vfx/src/spawn.ts","../../game-vfx/src/emitters/fire.ts","../../game-vfx/src/effects/dsl.ts","../../game-vfx/src/effects/steps.ts","../../game-vfx/src/effects/creatures/bloater-cloud.ts","../../game-vfx/src/effects/creatures/blood-hit.ts","../../game-vfx/src/effects/creatures/corpse-decay.ts","../../game-vfx/src/effects/creatures/eyes-in-fog.ts","../../game-vfx/src/effects/creatures/dismember.ts","../../game-vfx/src/effects/creatures/headshot.ts","../../game-vfx/src/effects/creatures/reanimation.ts","../../game-vfx/src/effects/creatures/screamer-shriek.ts","../../game-vfx/src/effects/props.ts","../../game-vfx/src/effects/weapons/impact-by-material.ts","../../game-vfx/src/effects/weapons/penetration.ts","../../game-vfx/src/effects/weapons/casings-and-magazine.ts","../../game-vfx/src/effects/weapons/molotov.ts","../../game-vfx/src/effects/weapons/grenade.ts","../../game-vfx/src/effects/weapons/explosive-barrel.ts","../../game-vfx/src/effects/weapons/muzzle-flash.ts","../../game-vfx/src/effects/destruction/shopfront-glass.ts","../../game-vfx/src/effects/vector-math.ts","../../game-vfx/src/effects/destruction/door-stages.ts","../../game-vfx/src/effects/destruction/building-collapse.ts","../../game-vfx/src/effects/destruction/block-wall.ts","../../game-vfx/src/effects/destruction/spot.ts","../../game-vfx/src/effects/destruction/metal-door.ts","../../game-vfx/src/effects/destruction/iron-railing.ts","../../game-vfx/src/effects/destruction/wooden-fence.ts","../../game-vfx/src/effects/destruction/hit-point-breakage.ts","../../game-vfx/src/effects/destruction/pressure-cylinders.ts","../../game-vfx/src/effects/world/building-fire.ts","../../game-vfx/src/effects/world/fog-and-ash.ts","../../game-vfx/src/workshop/rain.ts","../../game-vfx/src/effects/world/rain.ts","../../game-vfx/src/effects/vehicles/car.ts","../../game-vfx/src/effects/vehicles/headlights.ts","../../game-vfx/src/effects/vehicles/scraping-sparks.ts","../../game-vfx/src/effects/vehicles/engine-smoke.ts","../../game-vfx/src/effects/vehicles/boom.ts","../../game-vfx/src/effects/vehicles/fuel-leak.ts","../../game-vfx/src/effects/vehicles/car-explosion.ts","../../game-vfx/src/effects/vehicles/windshield.ts","../../game-vfx/src/effects/screen/damage-and-bite.ts","../../game-vfx/src/effects/screen/infection-phases.ts","../../game-vfx/src/effects/screen/panic.ts","../../game-vfx/src/effects/screen/bloater-blindness.ts","../../game-vfx/src/effects/screen/campfire.ts","../../game-vfx/src/effects/screen/cough-and-breath.ts","../../game-vfx/src/effects/events.ts","../../game-vfx/src/effects/weapons/viewmodel.ts","../../game-vfx/src/effects/vehicles/game-effects.ts","../../game-vfx/src/effects/catalog.ts","../../game-vfx/src/random.ts","../../game-vfx/src/effects/builders/impact-table.ts","../../game-vfx/src/effects/builders/blood.ts","../../game-vfx/src/effects/behaviors/blocks.ts","../../game-vfx/src/effects/behaviors/car.ts","../../game-vfx/src/effects/behaviors/cracks.ts","../../game-vfx/src/effects/builders/shared.ts","../../game-vfx/src/effects/behaviors/cylinder.ts","../../game-vfx/src/effects/behaviors/plate.ts","../../game-vfx/src/effects/behaviors/rain.ts","../../game-vfx/src/effects/behaviors/walkers.ts","../../game-vfx/src/effects/behaviors.ts","../../game-vfx/src/schedule.ts","../../game-vfx/src/effects/expression.ts","../../game-vfx/src/effects/builders/fire.ts","../../game-vfx/src/effects/builders/weapon.ts","../../game-vfx/src/effects/macros.ts","../../game-vfx/src/effects/instance.ts","../../game-vfx/src/effects/player.ts","../../game-vfx/src/lights.ts","../../game-vfx/src/lod.ts","../../game-vfx/src/marks.ts","../../game-vfx/src/pools/rings.ts","../../game-vfx/src/pools/shocks.ts","../../game-vfx/src/explosion.ts","../../game-vfx/src/pools/chunk-motion.ts","../../game-vfx/src/screen.ts","../../game-vfx/src/sprites/kind-pool.ts","../../game-vfx/src/data/sprite-kinds.json","../../game-vfx/src/sprites/kinds.ts","../../game-vfx/src/sprites/system.ts","../../game-vfx/src/workshop/direction.ts","../../game-vfx/src/workshop/debris.ts","../../game-vfx/src/workshop/decals.ts","../../game-vfx/src/workshop/meshes.ts","../../game-vfx/src/workshop/sprites.ts","../../game-vfx/src/workshop/streaks.ts","../../game-vfx/src/workshop/waves.ts","../../game-vfx/src/workshop/engine.ts","../../game-vfx/src/types.ts","../../game-vfx/src/world.ts","../../game-vfx/src/data/event-effects.json","../../game-vfx/src/effects/event-map.ts","../../game-vfx/src/locales/en.ts","../../game-vfx/src/locales/pt-BR.ts","../../game-vfx/src/locale.ts","../../game-vfx/src/effects/camera.ts","../../game-vfx/src/effects/runner.ts","../../game-vfx/src/ground.ts","../src/rain-near.ts","../src/rain-curtain.ts","../src/renderer.ts","../../game-contracts/src/blast-shove.ts","../../game-contracts/src/impact.ts","../../game-contracts/src/body.ts","../../game-contracts/src/spec.ts","../../game-contracts/src/weapons.ts","../../game-contracts/src/ignition.ts","../../game-contracts/src/animation.ts","../../game-contracts/src/tool-animation.ts","../../game-contracts/src/registry.ts","../../game-contracts/src/bench.ts","../../game-contracts/src/environment.ts","../../game-contracts/src/categories.ts","../../game-contracts/src/origin.ts","../../game-contracts/src/footprint.ts","../src/bench/text.ts","../src/bench/visual.ts"],"sourcesContent":["/**\n * @adatechnology/game-weather-three: the picture of `@adatechnology/game-weather` in three.js. The game's rain curtain (14 000 GPU points in a 70 m box), the near rain\n * and its splashes and rings, mist and ash on the pools of `@adatechnology/game-vfx`, linear and exponential fog with zone fog, the sky that dims in rain, lightning,\n * headlights in rain and a heat haze, plus the layers a weather bench shows.\n */\nexport { createWeatherRenderer, WEATHER_LAYERS } from './renderer'\nexport type { WeatherLayerId, WeatherRenderer, WeatherRendererOptions } from './renderer'\nexport { createRainCurtain, rainCurtainPositions, RAIN_CURTAIN, RAIN_CURTAIN_FRAGMENT, RAIN_CURTAIN_FULL_AT, RAIN_CURTAIN_PROFILES, RAIN_CURTAIN_VERTEX } from './rain-curtain'\nexport type { RainCurtain, RainCurtainOptions } from './rain-curtain'\nexport { createNearRain, SPLASH_FROM_MM } from './rain-near'\nexport type { NearRain, NearRainOptions } from './rain-near'\nexport { ASH, createFog, densityForFar, emitAirborne, FOG_EXP_FROM, FOG_NEAR, FOG_ZONE_NEAR, fogOf, MIST } from './fog'\nexport type { Fog, FogValues } from './fog'\nexport { createSky, SKY_DAY, SKY_NIGHT, ZONE_FOG_DEFAULTS, ZONE_LIGHT } from './sky'\nexport type { Sky } from './sky'\nexport { createLightning, LIGHTNING } from './lightning'\nexport type { Lightning } from './lightning'\nexport { createHeadlights, defaultHeadlightRule, HEADLIGHTS } from './headlights'\nexport type { HeadlightRules, Headlights } from './headlights'\nexport { createHeatHaze, HEAT_HAZE, HEAT_HAZE_FRAGMENT, HEAT_HAZE_VERTEX } from './heat-haze'\nexport type { HeatHaze } from './heat-haze'\nexport { createWeatherVisual } from './bench/visual'\nexport type { RigVehicles, WeatherVisualOptions } from './bench/visual'\nexport { weatherVisualTextOf } from './bench/text'\nexport type { WeatherVisualText } from './bench/text'\nexport type { ThreeLib, Vec3, WeatherProfileName, WeatherSource, ZoneFog } from './types'\n","import type * as THREE_NS from 'three'\nimport type { WeatherSample } from '@adatechnology/game-weather'\nimport type { ThreeLib, ZoneFog } from './types'\n\n/** The colours of the game's `skyUpdate` (jogo.js `_skyN` and `_skyD`): the sky goes from the night one to the day one with the sun height. */\nexport const SKY_NIGHT = 0x0b0f18\nexport const SKY_DAY = 0x8796a6\n/** The zone fog colour is lit by `0.35 + 0.65 s` (the page's `ZONA_NEBLINA` blend). */\nexport const ZONE_LIGHT = { base: 0.35, sun: 0.65 } as const\n/** The page's zone fog (`ZONA_NEBLINA`): colour and far distance; its weight is the game's to set. */\nexport const ZONE_FOG_DEFAULTS = { color: 0x3a4a3a, far: 60 } as const\n\nexport type Sky = {\n /** The sky colour for this weather: night to day by `sample.sun`, dimmed by `sample.skyScale` (the page's 0.8 in rain), pulled to the zone colour by its weight. */\n update(sample: WeatherSample, zone?: ZoneFog): THREE_NS.Color\n readonly color: THREE_NS.Color\n}\n\n/** Writes the sky into `scene.background` (a `Color`; made when the scene has none) and hands the colour to the fog. */\nexport function createSky(THREE: ThreeLib, scene: { background: unknown }): Sky {\n const night = new THREE.Color(SKY_NIGHT)\n const day = new THREE.Color(SKY_DAY)\n const zoneColor = new THREE.Color(ZONE_FOG_DEFAULTS.color)\n const tint = new THREE.Color()\n const color = new THREE.Color()\n return {\n color,\n update(sample, zone) {\n color.lerpColors(night, day, sample.sun)\n color.multiplyScalar(sample.skyScale)\n if (zone && zone.weight > 0) {\n zoneColor.setHex(zone.color ?? ZONE_FOG_DEFAULTS.color)\n color.lerp(tint.copy(zoneColor).multiplyScalar(ZONE_LIGHT.base + sample.sun * ZONE_LIGHT.sun), zone.weight)\n }\n const background = scene.background as THREE_NS.Color | null | undefined\n if (background && (background as { isColor?: boolean }).isColor) background.copy(color)\n else scene.background = color.clone()\n return color\n },\n }\n}\n","import type * as THREE_NS from 'three'\nimport type { WeatherSample } from '@adatechnology/game-weather'\nimport type { WorkshopEngine } from '@adatechnology/game-vfx'\nimport { ZONE_FOG_DEFAULTS } from './sky'\nimport type { ThreeLib, Vec3, ZoneFog } from './types'\n\n/** The page's linear fog starts at 25 m and, in the zone, at 4 m (`scene.fog.near = lerp(25, 4, weight)`). */\nexport const FOG_NEAR = 25\nexport const FOG_ZONE_NEAR = 4\n/** Below this exponential-squared density the weather uses the linear fog (the clear weathers have 0). */\nexport const FOG_EXP_FROM = 0.0005\n/** exp2 density that sees as far as a linear fog of `far` metres (2 % left): used to blend the zone fog in the exponential case. */\nexport const densityForFar = (far: number): number => Math.sqrt(Math.log(50)) / far\n\nexport type FogValues = { readonly mode: 'linear'; readonly near: number; readonly far: number } | { readonly mode: 'exp2'; readonly density: number }\n\n/** The fog the weather and the zone ask for. Pure. */\nexport function fogOf(sample: WeatherSample, zone?: ZoneFog, expFrom: number = FOG_EXP_FROM): FogValues {\n const weight = zone?.weight ?? 0\n const zoneFar = zone?.far ?? ZONE_FOG_DEFAULTS.far\n if (sample.fogDensity >= expFrom) return { mode: 'exp2', density: sample.fogDensity + (densityForFar(zoneFar) - sample.fogDensity) * weight }\n return { mode: 'linear', near: FOG_NEAR + (FOG_ZONE_NEAR - FOG_NEAR) * weight, far: sample.fogFar + (zoneFar - sample.fogFar) * weight }\n}\n\nexport type Fog = {\n /** Puts the weather's fog on the scene (the page's `fog.far` and `fog.near`, or the exponential-squared one) with the sky's colour. */\n update(sample: WeatherSample, sky: THREE_NS.Color, zone?: ZoneFog): void\n /** Takes the fog off the scene (a layer switched off). */\n clear(): void\n}\n\nexport function createFog(THREE: ThreeLib, scene: { fog: unknown }, options: { readonly expFrom?: number } = {}): Fog {\n // the fog the host had before: it comes back when the layer is switched off (the first update changes it in place, so a copy is kept)\n const original = scene.fog\n const kept = original && (original as { isFog?: boolean }).isFog ? (original as THREE_NS.Fog).clone() : original\n return {\n update(sample, sky, zone) {\n const values = fogOf(sample, zone, options.expFrom)\n const current = scene.fog as (THREE_NS.Fog & THREE_NS.FogExp2 & { isFog?: boolean; isFogExp2?: boolean }) | null | undefined\n if (values.mode === 'linear') {\n if (current?.isFog) {\n current.color.copy(sky)\n current.near = values.near\n current.far = values.far\n } else scene.fog = new THREE.Fog(sky.getHex(), values.near, values.far)\n } else if (current?.isFogExp2) {\n current.color.copy(sky)\n current.density = values.density\n } else scene.fog = new THREE.FogExp2(sky.getHex(), values.density)\n const fog = scene.fog as { color: THREE_NS.Color }\n fog.color.copy(sky)\n },\n clear() {\n scene.fog = kept && (kept as { isFog?: boolean }).isFog ? (kept as THREE_NS.Fog).clone() : (kept ?? null)\n },\n }\n}\n\n/** The Workshop's mist (W1073: low, slow, 3 to 5 m wide) and ash (fine, falling in a spiral), in a box that follows the camera. */\nexport const MIST = { box: { x: [-14, 10], y: [0.2, 1.1], z: [-3, 8] }, drift: [0.15, 0.35], life: [9, 13], size: [3, 5], grow: 1.4, color: 0x7d8aa0, alpha: 0.13, fadeIn: 0.35 } as const\nexport const ASH = { box: { x: [-10, 8], y: [4, 6], z: [-3, 7] }, fall: -0.35, life: [8, 12], size: [0.025, 0.04], color: 0xa09a90, alpha: 0.75, fadeIn: 0.1 } as const\n\n/**\n * Feeds the `normal` sprite pool of the Workshop with `mistPerSecond` puffs of mist and `ashPerSecond` flakes of ash a second (the page's `emit('fog', ...)`\n * and `emit('ash', ...)`, at the weather's rate instead of 5 and 14), around `camera`. The renderer of game-vfx-three draws them.\n */\nexport function emitAirborne(engine: WorkshopEngine, sample: WeatherSample, dt: number, camera: Vec3): void {\n const { random } = engine\n const around = (box: { readonly x: readonly number[]; readonly y: readonly number[]; readonly z: readonly number[] }): [number, number, number] => [\n camera[0] + random.between(box.x[0]!, box.x[1]!),\n random.between(box.y[0]!, box.y[1]!),\n camera[2] + random.between(box.z[0]!, box.z[1]!),\n ]\n if (sample.mistPerSecond > 0) {\n engine.emit('weather-mist', 'normal', sample.mistPerSecond, dt, () => ({\n p: around(MIST.box),\n v: [random.between(MIST.drift[0], MIST.drift[1]), 0, random.between(-0.05, 0.05)],\n sp: 0,\n life: MIST.life,\n size: MIST.size,\n grow: MIST.grow,\n c0: MIST.color,\n a: MIST.alpha,\n fi: MIST.fadeIn,\n }))\n }\n if (sample.ashPerSecond > 0) {\n engine.emit('weather-ash', 'normal', sample.ashPerSecond, dt, () => ({\n p: around(ASH.box),\n v: [random.between(0.1, 0.4), ASH.fall, random.between(-0.1, 0.1)],\n sp: [0.05, 0.15],\n life: ASH.life,\n size: ASH.size,\n c0: ASH.color,\n a: ASH.alpha,\n fi: ASH.fadeIn,\n gnd: 1,\n }))\n }\n}\n","import type * as THREE_NS from 'three'\nimport type { WorkshopEngine } from '@adatechnology/game-vfx'\nimport type { WeatherSample } from '@adatechnology/game-weather'\nimport type { ThreeLib, Vec3 } from './types'\n\n/** The Workshop's car lamps (W101-103) and the rain that glows inside their beam (W1161-1170). */\nexport const HEADLIGHTS = {\n spot: { color: 0xfff1d0, intensity: 2.4, distance: 26, angle: 0.33, penumbra: 0.5, decay: 1.6 },\n /** Lamp position on the car (x is mirrored), where the spot points, and the cone drawn in the air. */\n lamp: [0.65, 0.74, 2.15],\n aim: [0.65, 0.2, 12],\n cone: { radius: 2.4, length: 9, segments: 18, color: 0xfff1d0, opacity: 0.06 },\n lens: { on: 0xfff1d0, off: 0x3a3a34 },\n /** Lit drops a second, thrown into the beam 1 to 13 m ahead (W1176). */\n lit: { rate: 600, depth: [1, 13], spread: 0.33, color: 0xcfdcf0, alpha: 0.9, life: 0.06, len: 0.035, velocity: [0.8, -14, 0] },\n /** Beyond this many metres a car keeps only the cone: no real light (W-b text of `farol`). */\n lightRange: 40,\n} as const\n\nexport type HeadlightRules = {\n /** Whether the lamps are lit in this weather and light (default: night, rain or fog). */\n readonly lit?: (sample: WeatherSample) => boolean\n readonly lightRange?: number\n}\n\n/** Night (the sun below the page's 0.3 night factor), rain or fog make a driver turn the lamps on. */\nexport const defaultHeadlightRule = (sample: WeatherSample): boolean => sample.nightFactor > 0.3 || sample.rain > 0 || sample.fogDensity > 0.02 || sample.visibility < 150\n\ntype Rig = { readonly car: THREE_NS.Object3D; readonly spots: THREE_NS.SpotLight[]; readonly cones: THREE_NS.Mesh[]; readonly lens: THREE_NS.MeshBasicMaterial | undefined; readonly targets: THREE_NS.Object3D[] }\n\nexport type Headlights = {\n /** Gives `car` its two spot lights and two beam cones (`lens`, when given, is the material of the lamp glass, lit with them). Returns the function that takes them off. */\n attach(car: THREE_NS.Object3D, lens?: THREE_NS.MeshBasicMaterial): () => void\n /** Lights the lamps of the cars attached, the real light only within range of `camera`; throws the lit drops of the beams into the Workshop's streak pool. */\n update(sample: WeatherSample, dt: number, camera: Vec3, engine?: WorkshopEngine, enabled?: boolean): void\n count(): number\n dispose(): void\n}\n\nexport function createHeadlights(THREE: ThreeLib, rules: HeadlightRules = {}): Headlights {\n const rigs = new Set<Rig>()\n const cone = HEADLIGHTS.cone\n const lampWorld = new THREE.Vector3()\n const aimWorld = new THREE.Vector3()\n const forward = new THREE.Vector3()\n const rule = rules.lit ?? defaultHeadlightRule\n const range = rules.lightRange ?? HEADLIGHTS.lightRange\n\n function attach(car: THREE_NS.Object3D, lens?: THREE_NS.MeshBasicMaterial): () => void {\n const spots: THREE_NS.SpotLight[] = []\n const cones: THREE_NS.Mesh[] = []\n const targets: THREE_NS.Object3D[] = []\n for (const side of [-1, 1]) {\n const spot = new THREE.SpotLight(HEADLIGHTS.spot.color, 0, HEADLIGHTS.spot.distance, HEADLIGHTS.spot.angle, HEADLIGHTS.spot.penumbra, HEADLIGHTS.spot.decay)\n spot.position.set(side * HEADLIGHTS.lamp[0], HEADLIGHTS.lamp[1], HEADLIGHTS.lamp[2])\n const target = new THREE.Object3D()\n target.position.set(side * HEADLIGHTS.aim[0], HEADLIGHTS.aim[1], HEADLIGHTS.aim[2])\n car.add(spot, target)\n spot.target = target\n const geometry = new THREE.ConeGeometry(cone.radius, cone.length, cone.segments, 1, true)\n geometry.translate(0, -cone.length / 2, 0)\n geometry.rotateX(-Math.PI / 2)\n const material = new THREE.MeshBasicMaterial({ color: cone.color, transparent: true, opacity: cone.opacity, blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide, fog: false })\n const beam = new THREE.Mesh(geometry, material)\n beam.position.set(side * HEADLIGHTS.lamp[0], HEADLIGHTS.lamp[1], HEADLIGHTS.lamp[2])\n beam.visible = false\n car.add(beam)\n spots.push(spot)\n cones.push(beam)\n targets.push(target)\n }\n const rig: Rig = { car, spots, cones, lens, targets }\n rigs.add(rig)\n return () => {\n rigs.delete(rig)\n for (const object of [...spots, ...cones, ...targets]) car.remove(object)\n for (const beam of cones) {\n beam.geometry.dispose()\n ;(beam.material as THREE_NS.Material).dispose()\n }\n for (const spot of spots) spot.dispose()\n lens?.color.setHex(HEADLIGHTS.lens.off)\n }\n }\n\n return {\n attach,\n update(sample, dt, camera, engine, enabled = true) {\n const on = enabled && rule(sample)\n let index = 0\n for (const rig of rigs) {\n rig.car.updateWorldMatrix(true, false)\n lampWorld.set(0, HEADLIGHTS.lamp[1], HEADLIGHTS.lamp[2]).applyMatrix4(rig.car.matrixWorld)\n const near = Math.hypot(lampWorld.x - camera[0], lampWorld.z - camera[2]) <= range\n for (const spot of rig.spots) spot.intensity = on && near ? HEADLIGHTS.spot.intensity : 0\n for (const beam of rig.cones) beam.visible = on\n rig.lens?.color.setHex(on ? HEADLIGHTS.lens.on : HEADLIGHTS.lens.off)\n if (!on || !engine || sample.rain <= 0 || !near) {\n index++\n continue\n }\n aimWorld.set(0, HEADLIGHTS.lamp[1], HEADLIGHTS.lamp[2] + 1).applyMatrix4(rig.car.matrixWorld)\n forward.copy(aimWorld).sub(lampWorld).normalize()\n const side: [number, number] = [forward.z, -forward.x]\n const { random } = engine\n const lit = HEADLIGHTS.lit\n engine.emit(`weather-lit-${index}`, 'streak', lit.rate, dt, () => {\n const depth = random.between(lit.depth[0], lit.depth[1])\n const spread = depth * lit.spread * Math.sqrt(random.next())\n const turn = random.between(0, 6.283)\n return {\n p: [lampWorld.x + forward.x * depth + side[0] * Math.cos(turn) * spread, lampWorld.y + forward.y * depth + Math.sin(turn) * spread * 0.6, lampWorld.z + forward.z * depth + side[1] * Math.cos(turn) * spread],\n v: lit.velocity as unknown as [number, number, number],\n sp: 0,\n life: lit.life,\n len: lit.len,\n c0: lit.color,\n a: lit.alpha,\n }\n })\n index++\n }\n },\n count: () => rigs.size,\n dispose() {\n for (const rig of [...rigs]) {\n rigs.delete(rig)\n for (const object of [...rig.spots, ...rig.cones, ...rig.targets]) rig.car.remove(object)\n }\n },\n }\n}\n","import type * as THREE_NS from 'three'\nimport type { WeatherSample } from '@adatechnology/game-weather'\nimport type { ThreeLib } from './types'\n\n/** Peak opacity of the shimmer at full haze, and how fast the bands rise (SDK-made: no team delivered heat haze, see the open question). */\nexport const HEAT_HAZE = { opacity: 0.09, bands: 38, rise: 1.4, color: [1, 0.93, 0.82] } as const\n\nexport const HEAT_HAZE_VERTEX = 'varying vec2 vUv; void main(){ vUv = uv; gl_Position = vec4(position.xy, 0., 1.); }'\nexport const HEAT_HAZE_FRAGMENT =\n 'uniform float uTime; uniform float uHaze; uniform vec3 uColor; varying vec2 vUv; void main(){ float low = 1. - smoothstep(0.15, 0.7, vUv.y); float wave = sin(vUv.y*38. - uTime*1.4 + sin(vUv.x*11. + uTime*.7)*2.2) * .5 + .5; gl_FragColor = vec4(uColor, uHaze*low*wave*.09); }'\n\nexport type HeatHaze = {\n readonly object: THREE_NS.Mesh\n update(sample: WeatherSample, time: number): void\n dispose(): void\n}\n\n/**\n * The heat shimmer of `extreme_heat` (`WeatherSample.haze`, 0..1): a screen-space layer of warm bands that rise over the lower half of the view. It is a layer, not a\n * distortion of the picture, because bending the picture needs a render target that only the host owns.\n */\nexport function createHeatHaze(THREE: ThreeLib): HeatHaze {\n const uniforms = { uTime: { value: 0 }, uHaze: { value: 0 }, uColor: { value: new THREE.Vector3(...HEAT_HAZE.color) } }\n const material = new THREE.ShaderMaterial({ uniforms, vertexShader: HEAT_HAZE_VERTEX, fragmentShader: HEAT_HAZE_FRAGMENT, transparent: true, depthTest: false, depthWrite: false })\n const object = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material)\n object.frustumCulled = false\n object.renderOrder = 999\n object.name = 'weather-heat-haze'\n object.visible = false\n return {\n object,\n update(sample, time) {\n uniforms.uHaze.value = sample.haze\n uniforms.uTime.value = time\n object.visible = sample.haze > 0\n },\n dispose() {\n object.geometry.dispose()\n material.dispose()\n },\n }\n}\n","import type * as THREE_NS from 'three'\nimport type { VfxWorld } from '@adatechnology/game-vfx'\nimport type { WeatherSample } from '@adatechnology/game-weather'\nimport type { ThreeLib } from './types'\n\n/**\n * What the Workshop's double flash does to the scene (W1072): the hemisphere light gains `I * 3.2`, the background gains `I * (.25, .27, .32)` and the screen goes\n * white by `I * .12`, where `I` is the flash the weather samples (1, then .2, then .8 for 0.26 s).\n */\nexport const LIGHTNING = { light: 3.2, background: [0.25, 0.27, 0.32], white: 0.12, skyColor: 0x3a4c6a, groundColor: 0x0b0b0c } as const\n\nexport type Lightning = {\n readonly light: THREE_NS.HemisphereLight\n /**\n * Lights the scene with the flash of `sample`. `background`, when given, is the sky just written for this frame (the renderer passes it only when it owns the sky),\n * and gains the flash; a background the host keeps for itself is left alone.\n */\n update(sample: WeatherSample, background?: THREE_NS.Color): void\n dispose(): void\n}\n\nexport function createLightning(THREE: ThreeLib, scene: { add(object: THREE_NS.Object3D): unknown; remove(object: THREE_NS.Object3D): unknown }, vfx?: VfxWorld): Lightning {\n const light = new THREE.HemisphereLight(LIGHTNING.skyColor, LIGHTNING.groundColor, 0)\n light.name = 'weather-lightning'\n scene.add(light)\n return {\n light,\n update(sample, background) {\n const flash = sample.flash\n light.intensity = flash * LIGHTNING.light\n if (flash <= 0) return\n if (background) {\n background.r += flash * LIGHTNING.background[0]!\n background.g += flash * LIGHTNING.background[1]!\n background.b += flash * LIGHTNING.background[2]!\n }\n const screen = vfx?.workshop().screen\n if (screen) screen.white = Math.max(screen.white, flash * LIGHTNING.white)\n },\n dispose() {\n scene.remove(light)\n light.dispose()\n },\n }\n}\n","import type { AreaCap } from './types'\n\n/** The game's cap (G68194-68202): from the 7th explosion within 40 m in 4 s, the extra ones are made at 0.3. */\nexport const AREA_CAP_GAME: AreaCap = { radius: 40, window: 4, count: 6, excessScale: 0.3 }\n\nexport interface AreaCapTracker {\n /** Registers an explosion at (`x`, `z`) at `time` seconds and answers the scale it gets: 1, or the cap's `excessScale` when `count` or more are already recent in the area. */\n register(time: number, x: number, z: number): number\n clear(): void\n}\n\ninterface Recent {\n time: number\n x: number\n z: number\n}\n\nexport function createAreaCap(cap: AreaCap): AreaCapTracker {\n const recent: Recent[] = []\n const spare: Recent[] = []\n const radiusSquared = cap.radius * cap.radius\n return {\n register(time, x, z) {\n let kept = 0\n for (const entry of recent) {\n if (time - entry.time < cap.window) recent[kept++] = entry\n else spare.push(entry)\n }\n recent.length = kept\n let near = 0\n for (const entry of recent) if ((entry.x - x) ** 2 + (entry.z - z) ** 2 < radiusSquared) near++\n const entry = spare.pop() ?? { time: 0, x: 0, z: 0 }\n entry.time = time\n entry.x = x\n entry.z = z\n recent.push(entry)\n return near >= cap.count ? cap.excessScale : 1\n },\n clear() {\n for (const entry of recent) spare.push(entry)\n recent.length = 0\n },\n }\n}\n","import type { ExplosionDef, VfxProfile, VfxProfileName } from './types'\n\n/**\n * The pool sizes of the three profiles. `high` is the bench and the goldens (the Lab's caps L131/240/295/367 and the Workshop's W221), `medium` the\n * game's default (G67747/67762), `low` close to the game's own budgets (G68074, G68108).\n */\nexport const PROFILES: Readonly<Record<VfxProfileName, VfxProfile>> = {\n high: { puffs: 2600, sparks: 3200, glows: 320, chunks: 300, sprites: { add: 9000, normal: 9000 }, streaks: 3000, debris: 500 },\n medium: { puffs: 1300, sparks: 1600, glows: 160, chunks: 150, sprites: { add: 1500, normal: 600 }, streaks: 1000, debris: 120 },\n low: { puffs: 260, sparks: 800, glows: 80, chunks: 120, sprites: { add: 600, normal: 300 }, streaks: 400, debris: 60 },\n}\n\n/** The Lab's per-event meters (L1043-1044): 400 GPU particles, 40 physics debris, and 4 + 2 lights. */\nexport const EVENT_BUDGET = { particles: 400, debris: 40, blastLights: 4, fireLights: 2 } as const\n\n/** Particles an explosion makes on the GPU, the number of the Lab's effect sheet (L1033): fireballs, smoke, dust, shrapnel, the mushroom and the embers. */\nexport function estimateCost(def: ExplosionDef): number {\n const mushroom = def.mushroom ? def.mushroom.count + def.mushroom.stem : 0\n return def.fireballs + def.smoke.count + def.dust.count + def.shrapnel.count + mushroom + (def.shrapnel.embers ?? 0)\n}\n","{\n \"carl_gustaf_84mm\": \"at4_84mm\",\n \"vog_25\": \"m79_40mm\"\n}\n","{\n \"m67\": {\n \"id\": \"m67\",\n \"lethal\": 5,\n \"injury\": 15,\n \"visualRadius\": 1.3,\n \"fireballs\": 16,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 6972767,\n \"count\": 18,\n \"life\": 5.5\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 30\n },\n \"shrapnel\": {\n \"count\": 150,\n \"kind\": 0,\n \"speed\": 34\n },\n \"chunks\": {\n \"count\": 14,\n \"colors\": [\n 3881528,\n 2894634,\n 4867650\n ],\n \"hot\": 0.25\n },\n \"mark\": {\n \"scorch\": 1.7,\n \"crater\": 0.9,\n \"dent\": 0.25\n },\n \"flash\": {\n \"light\": 45,\n \"life\": 0.45,\n \"color\": 16752720,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.1,\n \"push\": 9,\n \"sources\": [\n \"lab:L796\",\n \"lab:L793\"\n ]\n },\n \"molotov\": {\n \"id\": \"molotov\",\n \"lethal\": 0,\n \"injury\": 4,\n \"visualRadius\": 1.1,\n \"fireballs\": 24,\n \"fuel\": true,\n \"heat\": 0.95,\n \"heatLifeScale\": 1.5,\n \"smoke\": {\n \"color\": 3814449,\n \"count\": 3,\n \"life\": 3.5\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 0\n },\n \"shrapnel\": {\n \"count\": 0,\n \"kind\": 0,\n \"speed\": 28\n },\n \"chunks\": {\n \"count\": 0,\n \"colors\": [\n 3881528,\n 2894634,\n 4867650\n ],\n \"hot\": 0.3\n },\n \"mark\": {\n \"scorch\": 2.4\n },\n \"flash\": {\n \"light\": 22,\n \"life\": 1.2,\n \"color\": 16747066,\n \"intensity\": 6\n },\n \"shock\": false,\n \"shake\": 0.25,\n \"push\": 0,\n \"ignite\": 4.2,\n \"sources\": [\n \"lab:L797\",\n \"lab:L793\"\n ]\n },\n \"pipe_bomb\": {\n \"id\": \"pipe_bomb\",\n \"lethal\": 3,\n \"injury\": 10,\n \"visualRadius\": 1,\n \"fireballs\": 8,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 11841704,\n \"count\": 26,\n \"life\": 6,\n \"kind\": 3\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 22\n },\n \"shrapnel\": {\n \"count\": 220,\n \"kind\": 1,\n \"speed\": 30,\n \"width\": 0.03\n },\n \"chunks\": {\n \"count\": 8,\n \"colors\": [\n 5132886,\n 3816768\n ],\n \"plate\": true,\n \"hot\": 0.2\n },\n \"mark\": {\n \"scorch\": 1.3,\n \"crater\": 0.7,\n \"dent\": 0.18\n },\n \"flash\": {\n \"light\": 30,\n \"life\": 0.45,\n \"color\": 16752720,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1,\n \"push\": 7,\n \"sources\": [\n \"lab:L798\",\n \"lab:L793\"\n ]\n },\n \"propane_p13\": {\n \"id\": \"propane_p13\",\n \"lethal\": 5,\n \"injury\": 14,\n \"visualRadius\": 2.2,\n \"fireballs\": 34,\n \"fuel\": true,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 3814449,\n \"count\": 10,\n \"life\": 8\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 26\n },\n \"shrapnel\": {\n \"count\": 70,\n \"kind\": 0,\n \"speed\": 28,\n \"embers\": 30\n },\n \"chunks\": {\n \"count\": 8,\n \"colors\": [\n 3891098,\n 2835824\n ],\n \"plate\": true,\n \"hot\": 0.5\n },\n \"mark\": {\n \"scorch\": 2.6\n },\n \"flash\": {\n \"light\": 70,\n \"life\": 1.1,\n \"color\": 16751168,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.4,\n \"push\": 8,\n \"mushroom\": {\n \"height\": 7,\n \"radius\": 2.6,\n \"count\": 36,\n \"stem\": 16,\n \"life\": 8\n },\n \"residualFires\": [\n {\n \"radius\": 1.4,\n \"life\": 7,\n \"rate\": 20\n }\n ],\n \"ignite\": 5,\n \"hotParts\": true,\n \"sources\": [\n \"lab:L799\",\n \"lab:L793\"\n ]\n },\n \"dynamite\": {\n \"id\": \"dynamite\",\n \"lethal\": 4,\n \"injury\": 12,\n \"visualRadius\": 1.4,\n \"fireballs\": 12,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 8019006,\n \"count\": 24,\n \"life\": 7\n },\n \"dust\": {\n \"color\": 8019006,\n \"count\": 46\n },\n \"shrapnel\": {\n \"count\": 90,\n \"kind\": 0,\n \"speed\": 28\n },\n \"chunks\": {\n \"count\": 26,\n \"colors\": [\n 6965806,\n 8018490,\n 4863526\n ],\n \"hot\": 0.15,\n \"speed\": 1.2\n },\n \"mark\": {\n \"scorch\": 2.4,\n \"crater\": 1.8,\n \"dent\": 0.7\n },\n \"flash\": {\n \"light\": 55,\n \"life\": 0.45,\n \"color\": 16752720,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.5,\n \"push\": 10,\n \"sources\": [\n \"lab:L800\",\n \"lab:L793\"\n ]\n },\n \"c4\": {\n \"id\": \"c4\",\n \"lethal\": 6,\n \"injury\": 18,\n \"visualRadius\": 1.6,\n \"fireballs\": 20,\n \"heat\": 1.25,\n \"smoke\": {\n \"color\": 6972767,\n \"count\": 22,\n \"life\": 7\n },\n \"dust\": {\n \"color\": 9407106,\n \"count\": 50\n },\n \"shrapnel\": {\n \"count\": 200,\n \"kind\": 0,\n \"speed\": 42\n },\n \"chunks\": {\n \"count\": 32,\n \"colors\": [\n 9078140,\n 7301730,\n 3881528\n ],\n \"hot\": 0.3,\n \"speed\": 1.4\n },\n \"mark\": {\n \"scorch\": 3,\n \"crater\": 2.3,\n \"dent\": 0.9\n },\n \"flash\": {\n \"light\": 90,\n \"life\": 0.35,\n \"color\": 16773328,\n \"intensity\": 9\n },\n \"shock\": {\n \"duration\": 0.28\n },\n \"shake\": 2.2,\n \"push\": 13,\n \"sources\": [\n \"lab:L801\",\n \"lab:L793\"\n ]\n },\n \"claymore\": {\n \"id\": \"claymore\",\n \"lethal\": 15,\n \"injury\": 50,\n \"visualRadius\": 0.7,\n \"fireballs\": 6,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 6972767,\n \"count\": 8,\n \"life\": 4\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 34\n },\n \"shrapnel\": {\n \"count\": 700,\n \"kind\": 1,\n \"speed\": 55,\n \"width\": 0.025\n },\n \"chunks\": {\n \"count\": 6,\n \"colors\": [\n 3881528,\n 2894634,\n 4867650\n ],\n \"hot\": 0.3\n },\n \"mark\": {\n \"scorch\": 0.9\n },\n \"flash\": {\n \"light\": 28,\n \"life\": 0.45,\n \"color\": 16752720,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1,\n \"push\": 8,\n \"cone\": {\n \"angle\": 0,\n \"half\": 0.5235987755982988,\n \"length\": 50\n },\n \"directionBias\": 2,\n \"sources\": [\n \"lab:L802\",\n \"lab:L793\"\n ]\n },\n \"flashbang\": {\n \"id\": \"flashbang\",\n \"lethal\": 0,\n \"injury\": 10,\n \"visualRadius\": 0.5,\n \"fireballs\": 0,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 11841704,\n \"count\": 10,\n \"life\": 5,\n \"kind\": 3\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 0\n },\n \"shrapnel\": {\n \"count\": 60,\n \"kind\": 2,\n \"speed\": 12\n },\n \"chunks\": {\n \"count\": 0,\n \"colors\": [\n 3881528,\n 2894634,\n 4867650\n ],\n \"hot\": 0.3\n },\n \"mark\": {\n \"scorch\": 0.4\n },\n \"flash\": {\n \"light\": 160,\n \"life\": 0.2,\n \"color\": 16054015,\n \"intensity\": 14,\n \"blinding\": true\n },\n \"shock\": false,\n \"shake\": 0.4,\n \"push\": 0,\n \"sources\": [\n \"lab:L803\",\n \"lab:L793\"\n ]\n },\n \"m79_40mm\": {\n \"id\": \"m79_40mm\",\n \"lethal\": 4,\n \"injury\": 12,\n \"visualRadius\": 1.1,\n \"fireballs\": 14,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 6972767,\n \"count\": 14,\n \"life\": 5\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 24\n },\n \"shrapnel\": {\n \"count\": 130,\n \"kind\": 0,\n \"speed\": 28\n },\n \"chunks\": {\n \"count\": 12,\n \"colors\": [\n 3881528,\n 2894634,\n 4867650\n ],\n \"hot\": 0.3\n },\n \"mark\": {\n \"scorch\": 1.5,\n \"crater\": 0.8,\n \"dent\": 0.22\n },\n \"flash\": {\n \"light\": 40,\n \"life\": 0.45,\n \"color\": 16752720,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1,\n \"push\": 8,\n \"sources\": [\n \"lab:L804\",\n \"lab:L793\"\n ]\n },\n \"m72_66mm\": {\n \"id\": \"m72_66mm\",\n \"lethal\": 5,\n \"injury\": 15,\n \"visualRadius\": 1.5,\n \"fireballs\": 22,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 6972767,\n \"count\": 16,\n \"life\": 6\n },\n \"dust\": {\n \"color\": 9407106,\n \"count\": 30\n },\n \"shrapnel\": {\n \"count\": 180,\n \"kind\": 0,\n \"speed\": 40\n },\n \"chunks\": {\n \"count\": 24,\n \"colors\": [\n 9078140,\n 7301730\n ],\n \"hot\": 0.3\n },\n \"mark\": {\n \"scorch\": 2\n },\n \"flash\": {\n \"light\": 60,\n \"life\": 0.45,\n \"color\": 16752720,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.5,\n \"push\": 11,\n \"directionBias\": 1.4,\n \"sources\": [\n \"lab:L805\",\n \"lab:L793\"\n ]\n },\n \"at4_84mm\": {\n \"id\": \"at4_84mm\",\n \"lethal\": 5,\n \"injury\": 16,\n \"visualRadius\": 1.7,\n \"fireballs\": 26,\n \"heat\": 1.15,\n \"smoke\": {\n \"color\": 6972767,\n \"count\": 18,\n \"life\": 6.5\n },\n \"dust\": {\n \"color\": 9407106,\n \"count\": 34\n },\n \"shrapnel\": {\n \"count\": 220,\n \"kind\": 0,\n \"speed\": 46\n },\n \"chunks\": {\n \"count\": 30,\n \"colors\": [\n 9078140,\n 7301730\n ],\n \"hot\": 0.35\n },\n \"mark\": {\n \"scorch\": 2.2,\n \"crater\": 1.4,\n \"dent\": 0.5\n },\n \"flash\": {\n \"light\": 70,\n \"life\": 0.45,\n \"color\": 16752720,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.7,\n \"push\": 12,\n \"directionBias\": 1.6,\n \"sources\": [\n \"lab:L806\",\n \"lab:L793\"\n ]\n },\n \"pg7v\": {\n \"id\": \"pg7v\",\n \"lethal\": 5,\n \"injury\": 16,\n \"visualRadius\": 1.8,\n \"fireballs\": 28,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 6972767,\n \"count\": 18,\n \"life\": 7\n },\n \"dust\": {\n \"color\": 9407106,\n \"count\": 34\n },\n \"shrapnel\": {\n \"count\": 200,\n \"kind\": 0,\n \"speed\": 42,\n \"embers\": 20\n },\n \"chunks\": {\n \"count\": 28,\n \"colors\": [\n 9078140,\n 7301730\n ],\n \"hot\": 0.4\n },\n \"mark\": {\n \"scorch\": 2.2\n },\n \"flash\": {\n \"light\": 70,\n \"life\": 0.45,\n \"color\": 16752720,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.7,\n \"push\": 12,\n \"directionBias\": 1.2,\n \"sources\": [\n \"lab:L807\",\n \"lab:L793\"\n ]\n },\n \"fuel_barrel\": {\n \"id\": \"fuel_barrel\",\n \"lethal\": 4,\n \"injury\": 12,\n \"visualRadius\": 1.9,\n \"fireballs\": 28,\n \"fuel\": true,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 3814449,\n \"count\": 8,\n \"life\": 8\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 22\n },\n \"shrapnel\": {\n \"count\": 60,\n \"kind\": 0,\n \"speed\": 28,\n \"embers\": 24\n },\n \"chunks\": {\n \"count\": 10,\n \"colors\": [\n 10165272,\n 5902864\n ],\n \"plate\": true,\n \"hot\": 0.6\n },\n \"mark\": {\n \"scorch\": 2.4\n },\n \"flash\": {\n \"light\": 60,\n \"life\": 1,\n \"color\": 16748608,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.2,\n \"push\": 8,\n \"mushroom\": {\n \"height\": 6,\n \"radius\": 2.1,\n \"count\": 30,\n \"stem\": 14,\n \"life\": 8\n },\n \"residualFires\": [\n {\n \"dx\": 1.6,\n \"dz\": 0.4,\n \"radius\": 0.9,\n \"life\": 6\n },\n {\n \"dx\": -1.1,\n \"dz\": 1.2,\n \"radius\": 0.7,\n \"life\": 5.5\n },\n {\n \"dx\": -0.4,\n \"dz\": -1.5,\n \"radius\": 0.8,\n \"life\": 6.5\n }\n ],\n \"ignite\": 4,\n \"hotParts\": true,\n \"sources\": [\n \"lab:L808\",\n \"lab:L793\"\n ]\n },\n \"car_fuel_tank\": {\n \"id\": \"car_fuel_tank\",\n \"lethal\": 5,\n \"injury\": 14,\n \"visualRadius\": 2.3,\n \"fireballs\": 36,\n \"fuel\": true,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 3814449,\n \"count\": 12,\n \"life\": 9\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 30\n },\n \"shrapnel\": {\n \"count\": 90,\n \"kind\": 0,\n \"speed\": 28,\n \"embers\": 36\n },\n \"chunks\": {\n \"count\": 14,\n \"colors\": [\n 3881528,\n 2894634,\n 4867650\n ],\n \"plate\": true,\n \"hot\": 0.5\n },\n \"mark\": {\n \"scorch\": 3.2\n },\n \"flash\": {\n \"light\": 80,\n \"life\": 1.2,\n \"color\": 16748608,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.8,\n \"push\": 9,\n \"mushroom\": {\n \"height\": 7.5,\n \"radius\": 2.8,\n \"count\": 40,\n \"stem\": 16,\n \"life\": 9\n },\n \"ignite\": 5,\n \"hotParts\": true,\n \"sources\": [\n \"lab:L809\",\n \"lab:L793\"\n ]\n },\n \"fuel_pump\": {\n \"id\": \"fuel_pump\",\n \"lethal\": 4,\n \"injury\": 12,\n \"visualRadius\": 1.7,\n \"fireballs\": 24,\n \"fuel\": true,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 3814449,\n \"count\": 6,\n \"life\": 7\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 16\n },\n \"shrapnel\": {\n \"count\": 60,\n \"kind\": 0,\n \"speed\": 28,\n \"embers\": 16\n },\n \"chunks\": {\n \"count\": 12,\n \"colors\": [\n 14209732,\n 2058810,\n 9407106\n ],\n \"plate\": true,\n \"hot\": 0.5\n },\n \"mark\": {\n \"scorch\": 2.4\n },\n \"flash\": {\n \"light\": 55,\n \"life\": 0.9,\n \"color\": 16748608,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.2,\n \"push\": 8,\n \"mushroom\": {\n \"height\": 5,\n \"radius\": 1.8,\n \"count\": 22,\n \"stem\": 10,\n \"life\": 7\n },\n \"residualFires\": [\n {\n \"radius\": 1.1,\n \"life\": 14,\n \"rate\": 22\n }\n ],\n \"ignite\": 4,\n \"hotParts\": true,\n \"sources\": [\n \"lab:L810\",\n \"lab:L793\"\n ]\n },\n \"fuel_station_tank\": {\n \"id\": \"fuel_station_tank\",\n \"lethal\": 12,\n \"injury\": 30,\n \"visualRadius\": 4.5,\n \"fireballs\": 44,\n \"fuel\": true,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 3814449,\n \"count\": 20,\n \"life\": 11\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 46\n },\n \"shrapnel\": {\n \"count\": 160,\n \"kind\": 0,\n \"speed\": 40,\n \"embers\": 60\n },\n \"chunks\": {\n \"count\": 30,\n \"colors\": [\n 9407106,\n 3881528,\n 2058810\n ],\n \"hot\": 0.5\n },\n \"mark\": {\n \"scorch\": 6,\n \"crater\": 4,\n \"dent\": 1.6\n },\n \"flash\": {\n \"light\": 140,\n \"life\": 1.8,\n \"color\": 16748608,\n \"intensity\": 8\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 3,\n \"push\": 14,\n \"mushroom\": {\n \"height\": 16,\n \"radius\": 5.5,\n \"count\": 60,\n \"stem\": 26,\n \"life\": 12,\n \"riseRate\": 0.42\n },\n \"residualFires\": [\n {\n \"radius\": 3.2,\n \"life\": 20,\n \"rate\": 26,\n \"smoke\": 1.4\n },\n {\n \"dx\": 4,\n \"dz\": 3,\n \"radius\": 1.4,\n \"life\": 16\n },\n {\n \"dx\": -4,\n \"dz\": 2,\n \"radius\": 1.2,\n \"life\": 15\n }\n ],\n \"ignite\": 12,\n \"hotParts\": true,\n \"sources\": [\n \"lab:L811\",\n \"lab:L793\"\n ]\n },\n \"generic\": {\n \"id\": \"generic\",\n \"lethal\": 5,\n \"injury\": 15,\n \"visualRadius\": 1.3,\n \"fireballs\": 16,\n \"heat\": 1,\n \"smoke\": {\n \"color\": 6972767,\n \"count\": 18,\n \"life\": 5.5\n },\n \"dust\": {\n \"color\": 8221544,\n \"count\": 30\n },\n \"shrapnel\": {\n \"count\": 150,\n \"kind\": 0,\n \"speed\": 34\n },\n \"chunks\": {\n \"count\": 14,\n \"colors\": [\n 3881528,\n 2894634,\n 4867650\n ],\n \"hot\": 0.25\n },\n \"mark\": {\n \"scorch\": 1.7,\n \"crater\": 0.9,\n \"dent\": 0.25\n },\n \"flash\": {\n \"light\": 45,\n \"life\": 0.45,\n \"color\": 16752720,\n \"intensity\": 6\n },\n \"shock\": {\n \"duration\": 0.32\n },\n \"shake\": 1.1,\n \"push\": 9,\n \"sources\": [\n \"lab:L796\",\n \"lab:L793\",\n \"derived: the M67 profile, for a part with no explosive id; the caller scales it by injury / 15\"\n ]\n }\n}\n","import aliasData from './data/explosion-aliases.json'\nimport explosionData from './data/explosions.json'\nimport type { ExplosionDef } from './types'\n\n/**\n * The 16 profiles of the Explosion Lab (`PROF`, L795-812) keyed by the id of the explosive in `game-weapons` (`m67`, `molotov`, `pipe_bomb`,\n * `propane_p13`, `dynamite`, `c4`, `claymore`, `flashbang`, `m79_40mm`, `m72_66mm`, `at4_84mm`, `pg7v`, `fuel_barrel`, `car_fuel_tank`, `fuel_pump`,\n * `fuel_station_tank`), plus `generic` (the M67 for a part with no explosive id: scale it by `injury / 15`). Each one lists the Lab lines it comes from.\n */\nexport const EXPLOSIONS = explosionData as unknown as Readonly<Record<string, ExplosionDef>>\n\n/** Explosives with no profile of their own: the Carl Gustaf blasts like the AT4, the VOG-25 like the M79. */\nexport const EXPLOSION_ALIASES: Readonly<Record<string, string>> = aliasData\n\n/** A copy of a catalogue with some profiles changed (a partial replaces the fields it names) or removed (`null`); a game adds, tunes or drops effects without touching the core. */\nexport function patchExplosions(base: Readonly<Record<string, ExplosionDef>>, changes: Readonly<Record<string, Partial<ExplosionDef> | ExplosionDef | null>>): Record<string, ExplosionDef> {\n const result: Record<string, ExplosionDef> = { ...base }\n for (const [id, change] of Object.entries(changes)) {\n if (change === null) delete result[id]\n else result[id] = { ...(base[id] ?? {}), ...change, id } as ExplosionDef\n }\n return result\n}\n","export type Rgb = readonly [number, number, number]\n\nconst BYTE_MAX = 255\nconst SRGB_LINEAR_SLOPE = 0.0773993808\nconst SRGB_THRESHOLD = 0.04045\n\n/** The channels of `0xRRGGBB` exactly as three.js r128 reads them (no colour-space conversion): the numbers the Lab's shaders and lights were tuned with. */\nexport function hexToRgb(hex: number): Rgb {\n const whole = Math.floor(hex)\n return [((whole >> 16) & BYTE_MAX) / BYTE_MAX, ((whole >> 8) & BYTE_MAX) / BYTE_MAX, (whole & BYTE_MAX) / BYTE_MAX]\n}\n\nexport function srgbChannelToLinear(channel: number): number {\n return channel < SRGB_THRESHOLD ? channel * SRGB_LINEAR_SLOPE : Math.pow(channel * 0.9478672986 + 0.0521327014, 2.4)\n}\n\n/** `new Color(hex).convertSRGBToLinear()` of r128: the smoke, dust and fire colours the puff shader multiplies the light with. Converted once, at spawn. */\nexport function hexToLinearRgb(hex: number): Rgb {\n const [red, green, blue] = hexToRgb(hex)\n return [srgbChannelToLinear(red), srgbChannelToLinear(green), srgbChannelToLinear(blue)]\n}\n\nexport function linearRgbFromRaw(color: Rgb): Rgb {\n return [srgbChannelToLinear(color[0]), srgbChannelToLinear(color[1]), srgbChannelToLinear(color[2])]\n}\n","import { hexToLinearRgb } from './color'\n\n/** The Lab's smoke colours (L211-214), converted to linear once. Colours of an explosion in data are `0xRRGGBB` and go through `hexToLinearRgb` the same way. */\nexport const SMOKE_COLORS = {\n grey: hexToLinearRgb(0x6a655f),\n dark: hexToLinearRgb(0x4a4440),\n black: hexToLinearRgb(0x3a3431),\n brown: hexToLinearRgb(0x7a5c3e),\n dust: hexToLinearRgb(0x7d7368),\n concrete: hexToLinearRgb(0x8f8a82),\n white: hexToLinearRgb(0xb4b0a8),\n blood: hexToLinearRgb(0x5a0b0b),\n bile: hexToLinearRgb(0x7a8a2a),\n gas: hexToLinearRgb(0xe8ecef),\n} as const\n\nexport const DEFAULT_GRAVITY = 9.8\nexport const DEFAULT_WIND = { x: 0.55, y: 0, z: 0.2 } as const\nexport const FRAME_STEP_LIMIT = 0.05\n","/**\n * A pool that keeps its live records in the order they were added, reuses dead records instead of allocating, and compacts in place. When it is full\n * the oldest record goes (the Lab's `P.shift()`); a pool may choose which one instead (the chunks drop a resting one first).\n */\nexport class OrderedPool<T extends object> {\n /** The live records, oldest first. */\n readonly items: T[] = []\n private readonly spare: T[] = []\n\n constructor(\n readonly capacity: number,\n private readonly create: () => T,\n private readonly release?: (item: T) => void,\n ) {}\n\n get count(): number {\n return this.items.length\n }\n\n /** A record to fill in. At capacity the record `victim` picks (default: the oldest) is dropped and reused. */\n acquire(victim?: (items: readonly T[]) => number): T {\n if (this.items.length >= this.capacity) this.removeAt(victim ? victim(this.items) : 0)\n const record = this.spare.pop() ?? this.create()\n this.items.push(record)\n return record\n }\n\n removeAt(index: number): void {\n const record = this.items[index]\n if (record === undefined) return\n for (let shifted = index; shifted < this.items.length - 1; shifted++) this.items[shifted] = this.items[shifted + 1]!\n this.items.length -= 1\n this.release?.(record)\n this.spare.push(record)\n }\n\n /** Keeps the records `keep` accepts, in the same order, without allocating. `context` goes to `keep` so no closure is needed per frame. */\n compact<TContext>(keep: (item: T, context: TContext) => boolean, context: TContext): void {\n let write = 0\n for (let read = 0; read < this.items.length; read++) {\n const record = this.items[read]!\n if (keep(record, context)) {\n this.items[write++] = record\n continue\n }\n this.release?.(record)\n this.spare.push(record)\n }\n this.items.length = write\n }\n\n clear(): void {\n for (const record of this.items) {\n this.release?.(record)\n this.spare.push(record)\n }\n this.items.length = 0\n }\n}\n","import type { Rgb } from '../color'\nimport { OrderedPool } from '../pool'\nimport type { RandomStream } from '../random'\nimport type { Glow } from './glows'\n\n/** Debris with embers (L366-400). The last block is what the renderer reads: the colour already blended towards the glow of a hot piece. */\nexport interface Chunk {\n x: number\n y: number\n z: number\n vx: number\n vy: number\n vz: number\n rx: number\n ry: number\n rz: number\n ax: number\n ay: number\n az: number\n sx: number\n sy: number\n sz: number\n color: Rgb\n plate: boolean\n hot: boolean\n rest: boolean\n blood: boolean\n splat: boolean\n t: number\n trail: number\n glow: Glow | null\n rng: RandomStream\n outColor: [number, number, number]\n}\n\nexport type ChunkInit = Partial<Pick<Chunk, 'x' | 'y' | 'z' | 'vx' | 'vy' | 'vz' | 'sx' | 'sy' | 'sz' | 'color' | 'plate' | 'hot' | 'blood'>>\n\nconst DEFAULT_COLOR: Rgb = [0x55 / 255, 0x55 / 255, 0x55 / 255]\n\n/** A glow that follows a chunk keeps the spot the chunk had when the chunk leaves the pool, as the Lab's does (the object stays where it was). */\nfunction freezeFollowingGlow(chunk: Chunk): void {\n const glow = chunk.glow\n chunk.glow = null\n if (!glow || glow.follow !== chunk) return\n glow.x = chunk.x + glow.fx\n glow.y = chunk.y + glow.fy\n glow.z = chunk.z + glow.fz\n glow.fx = glow.fy = glow.fz = 0\n glow.follow = null\n}\n\nexport function createChunkPool(capacity: number, placeholder: RandomStream): OrderedPool<Chunk> {\n return new OrderedPool<Chunk>(\n capacity,\n () => ({\n x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, rx: 0, ry: 0, rz: 0, ax: 0, ay: 0, az: 0, sx: 0.2, sy: 0.2, sz: 0.2, color: DEFAULT_COLOR, plate: false, hot: false, rest: false,\n blood: false, splat: false, t: 0, trail: 0, glow: null, rng: placeholder, outColor: [0, 0, 0],\n }),\n freezeFollowingGlow,\n )\n}\n\nfunction firstResting(chunks: readonly Chunk[]): number {\n const index = chunks.findIndex((chunk) => chunk.rest)\n return index >= 0 ? index : 0\n}\n\n/** `chunk()` (L368-376). Draws rotation and spin first (six numbers, always), then lets the caller add what it wants; a full pool drops a resting piece, else the oldest. */\nexport function spawnChunk(pool: OrderedPool<Chunk>, random: RandomStream, init: ChunkInit): Chunk {\n const chunk = pool.acquire(firstResting)\n chunk.x = init.x ?? 0\n chunk.y = init.y ?? 0\n chunk.z = init.z ?? 0\n chunk.vx = init.vx ?? 0\n chunk.vy = init.vy ?? 0\n chunk.vz = init.vz ?? 0\n chunk.rx = random.next() * 6\n chunk.ry = random.next() * 6\n chunk.rz = random.next() * 6\n chunk.ax = random.between(-12, 12)\n chunk.ay = random.between(-12, 12)\n chunk.az = random.between(-12, 12)\n chunk.sx = init.sx ?? 0.2\n chunk.sy = init.sy ?? 0.2\n chunk.sz = init.sz ?? 0.2\n chunk.color = init.color ?? DEFAULT_COLOR\n chunk.plate = init.plate ?? false\n chunk.hot = init.hot ?? false\n chunk.rest = false\n chunk.blood = init.blood ?? false\n chunk.splat = false\n chunk.t = 0\n chunk.trail = 0\n chunk.glow = null\n chunk.rng = random\n return chunk\n}\n\nexport const HOT_COLOR: Rgb = [1, 0.42, 0.1]\nexport const HOT_SECONDS = 1.6\nconst HOT_BLEND = 0.85\n\n/** `_hotCol` (L399-400): the colour of a hot piece, blended towards the glow while it is young. */\nexport function colorOfChunk(chunk: Chunk): readonly [number, number, number] {\n if (!chunk.hot || chunk.t >= HOT_SECONDS) {\n chunk.outColor[0] = chunk.color[0]\n chunk.outColor[1] = chunk.color[1]\n chunk.outColor[2] = chunk.color[2]\n return chunk.outColor\n }\n const amount = Math.max(0, 1 - chunk.t / HOT_SECONDS) * HOT_BLEND\n chunk.outColor[0] = chunk.color[0] + (HOT_COLOR[0] - chunk.color[0]) * amount\n chunk.outColor[1] = chunk.color[1] + (HOT_COLOR[1] - chunk.color[1]) * amount\n chunk.outColor[2] = chunk.color[2] + (HOT_COLOR[2] - chunk.color[2]) * amount\n return chunk.outColor\n}\n","import type { MutableVec3 } from './random'\n\n/** Operations on `{ x, y, z }` written the way three.js r128 does them (`normalize` multiplies by the reciprocal), so a port repeats the Lab's numbers bit for bit. */\n\nexport function createVec3(x = 0, y = 0, z = 0): MutableVec3 {\n return { x, y, z }\n}\n\nexport function addScaled(target: MutableVec3, direction: MutableVec3, scale: number): MutableVec3 {\n target.x += direction.x * scale\n target.y += direction.y * scale\n target.z += direction.z * scale\n return target\n}\n\nexport function normalize(target: MutableVec3): MutableVec3 {\n const length = Math.sqrt(target.x * target.x + target.y * target.y + target.z * target.z) || 1\n const inverse = 1 / length\n target.x *= inverse\n target.y *= inverse\n target.z *= inverse\n return target\n}\n\nexport function distanceBetween(ax: number, ay: number, az: number, bx: number, by: number, bz: number): number {\n const dx = ax - bx\n const dy = ay - by\n const dz = az - bz\n return Math.sqrt(dx * dx + dy * dy + dz * dz)\n}\n\nexport function lerp(from: number, to: number, amount: number): number {\n return from + (to - from) * amount\n}\n\nexport function clamp(value: number, min: number, max: number): number {\n return value < min ? min : value > max ? max : value\n}\n","import type { Rgb } from '../color'\nimport { lerp } from '../vector'\nimport { OrderedPool } from '../pool'\n\n/** The position a glow follows (a chunk, or anything with `x`, `y`, `z`). */\nexport interface FollowPoint {\n readonly x: number\n readonly y: number\n readonly z: number\n}\n\n/** An additive halo (L294-318). The last block is what the renderer reads after `update`: where it is, how big and how bright. */\nexport interface Glow {\n x: number\n y: number\n z: number\n s: number\n s1: number | null\n i: number\n t: number\n life: number\n color: Rgb\n curve: number\n flick: boolean\n follow: FollowPoint | null\n fx: number\n fy: number\n fz: number\n outX: number\n outY: number\n outZ: number\n outSize: number\n outIntensity: number\n}\n\nexport type GlowInit = Partial<Omit<Glow, 't' | 'outX' | 'outY' | 'outZ' | 'outSize' | 'outIntensity'>>\n\nconst DEFAULT_COLOR: Rgb = [1, 0.6, 0.25]\n\nexport function createGlowPool(capacity: number): OrderedPool<Glow> {\n return new OrderedPool<Glow>(capacity, () => ({\n x: 0, y: 0, z: 0, s: 1, s1: null, i: 1, t: 0, life: 0.3, color: DEFAULT_COLOR, curve: 2, flick: false, follow: null, fx: 0, fy: 0, fz: 0,\n outX: 0, outY: 0, outZ: 0, outSize: 0, outIntensity: 0,\n }))\n}\n\nexport function spawnGlow(pool: OrderedPool<Glow>, init: GlowInit): Glow {\n const glow = pool.acquire()\n glow.x = init.x ?? 0\n glow.y = init.y ?? 0\n glow.z = init.z ?? 0\n glow.s = init.s ?? 1\n glow.s1 = init.s1 ?? null\n glow.i = init.i ?? 1\n glow.t = 0\n glow.life = init.life ?? 0.3\n glow.color = init.color ?? DEFAULT_COLOR\n glow.curve = init.curve ?? 2\n glow.flick = init.flick ?? false\n glow.follow = init.follow ?? null\n glow.fx = init.fx ?? 0\n glow.fy = init.fy ?? 0\n glow.fz = init.fz ?? 0\n return glow\n}\n\nfunction ageOrKeep(glow: Glow, dt: number): boolean {\n glow.t += dt\n return glow.t < glow.life\n}\n\nconst FLICKER_RATE = 40\nconst FLICKER_SPREAD = 9\n\n/** `updateGlows` (L305-318). */\nexport function updateGlows(pool: OrderedPool<Glow>, dt: number): void {\n pool.compact(ageOrKeep, dt)\n for (const glow of pool.items) {\n const age = glow.t / glow.life\n let x = glow.x\n let y = glow.y\n let z = glow.z\n if (glow.follow) {\n x = glow.follow.x + glow.fx\n y = glow.follow.y + glow.fy\n z = glow.follow.z + glow.fz\n }\n glow.outX = x\n glow.outY = y\n glow.outZ = z\n glow.outSize = glow.s1 !== null ? lerp(glow.s, glow.s1, 1 - Math.pow(1 - age, 3)) : glow.s\n const flicker = glow.flick ? 0.75 + 0.25 * Math.sin(glow.t * FLICKER_RATE + glow.s * FLICKER_SPREAD) : 1\n glow.outIntensity = glow.i * Math.pow(1 - age, glow.curve) * flicker\n }\n}\n","import type { Rgb } from '../color'\nimport { SMOKE_COLORS } from '../constants'\nimport { OrderedPool } from '../pool'\nimport type { RandomStream } from '../random'\n\n/** The ring a mushroom cloud's puffs travel on (L722-735). */\nexport interface Ring {\n x: number\n z: number\n y0: number\n rise: number\n rk: number\n R0: number\n R1: number\n r: number\n roll: number\n t: number\n life: number\n}\n\n/**\n * Fire, smoke, dust, blood and gas (L193-238). `kind`: 0 fire that turns into smoke, 1 dust, 2 blood, 3 gas or white powder smoke, 4 bile.\n * The last block is what the renderer reads after `update`.\n */\nexport interface Puff {\n x: number\n y: number\n z: number\n vx: number\n vy: number\n vz: number\n s0: number\n s1: number\n sg: number\n gk: number\n life: number\n t: number\n h0: number\n hl: number\n drag: number\n buoy: number\n grav: number\n kind: number\n seed: number\n wind: number\n color: Rgb\n ring: Ring | null\n ph: number\n th: number\n rng: RandomStream\n age: number\n heat: number\n size: number\n}\n\nexport type PuffInit = Partial<Omit<Puff, 'seed' | 'age' | 'heat' | 'size' | 't' | 'rng'>>\n\nconst FLOOR_FRACTION = 0.4\nconst FLOOR_BOUNCE = -0.2\nconst ARRIVAL_FRAME = 30\nconst ARRIVAL_BASE = 0.3\nconst GROWTH_CAP_SECONDS = 3.5\n\nexport function createPuffPool(capacity: number): OrderedPool<Puff> {\n const placeholder: RandomStream = { next: () => 0, between: (min) => min, direction: (out) => out, state: 0 }\n return new OrderedPool<Puff>(capacity, () => ({\n x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, s0: 0.2, s1: 1, sg: 0.1, gk: 6, life: 3, t: 0, h0: 0, hl: 0.4, drag: 3, buoy: 0.5, grav: 0, kind: 0, seed: 0, wind: 1,\n color: SMOKE_COLORS.grey, ring: null, ph: 0, th: 0, rng: placeholder, age: 0, heat: 0, size: 0,\n }))\n}\n\n/** `puff()` of the Lab: draws `seed` first, always, even when the caller gives one (L196), and drops the oldest puff when the pool is full. */\nexport function spawnPuff(pool: OrderedPool<Puff>, random: RandomStream, init: PuffInit): Puff {\n const puff = pool.acquire()\n puff.x = init.x ?? 0\n puff.y = init.y ?? 0\n puff.z = init.z ?? 0\n puff.vx = init.vx ?? 0\n puff.vy = init.vy ?? 0\n puff.vz = init.vz ?? 0\n puff.s0 = init.s0 ?? 0.2\n puff.s1 = init.s1 ?? 1\n puff.sg = init.sg ?? 0.1\n puff.gk = init.gk ?? 6\n puff.life = init.life ?? 3\n puff.t = 0\n puff.h0 = init.h0 ?? 0\n puff.hl = init.hl ?? 0.4\n puff.drag = init.drag ?? 3\n puff.buoy = init.buoy ?? 0.5\n puff.grav = init.grav ?? 0\n puff.kind = init.kind ?? 0\n puff.seed = random.next()\n puff.wind = init.wind ?? 1\n puff.color = init.color ?? SMOKE_COLORS.grey\n puff.ring = init.ring ?? null\n puff.ph = init.ph ?? 0\n puff.th = init.th ?? 0\n puff.rng = random\n puff.age = 0\n puff.heat = 0\n puff.size = 0\n return puff\n}\n\nfunction ageOrKeep(puff: Puff, dt: number): boolean {\n puff.t += dt\n return puff.t < puff.life\n}\n\nfunction moveOnRing(puff: Puff, ring: Ring, wind: { x: number; z: number }): void {\n const ringTime = ring.t\n const centerY = ring.y0 + ring.rise * (1 - Math.exp(-ringTime * ring.rk))\n const majorRadius = ring.R0 + (ring.R1 - ring.R0) * (1 - Math.exp(-ringTime * 1.6))\n const theta = puff.th + ringTime * ring.roll\n const minorRadius = ring.r * (0.8 + 0.4 * Math.min(1, ringTime))\n const radius = majorRadius + minorRadius * Math.cos(theta)\n puff.x = ring.x + Math.cos(puff.ph) * radius + wind.x * ringTime * 0.6\n puff.z = ring.z + Math.sin(puff.ph) * radius + wind.z * ringTime * 0.6\n puff.y = centerY + minorRadius * Math.sin(theta)\n}\n\nfunction moveFree(puff: Puff, dt: number, age: number, heat: number, wind: { x: number; z: number }): void {\n const damping = Math.exp(-puff.drag * dt)\n puff.vx *= damping\n puff.vy *= damping\n puff.vz *= damping\n puff.vy += (puff.buoy * (heat > 0 ? 1.6 : 1) - puff.grav) * dt\n puff.x += (puff.vx + wind.x * puff.wind * age) * dt\n puff.y += puff.vy * dt\n puff.z += (puff.vz + wind.z * puff.wind * age) * dt\n if (puff.y < puff.s0 * FLOOR_FRACTION) {\n puff.y = puff.s0 * FLOOR_FRACTION\n if (puff.vy < 0) puff.vy *= FLOOR_BOUNCE\n }\n}\n\n/** `updatePuffs` (L205-238): ages, removes what ended, then moves the rest with the wind (or along their mushroom ring) and derives what the shader reads. */\nexport function updatePuffs(pool: OrderedPool<Puff>, dt: number, wind: { x: number; z: number }): void {\n pool.compact(ageOrKeep, dt)\n for (const puff of pool.items) {\n const age = puff.t / puff.life\n const heat = puff.h0 > 0 ? puff.h0 * Math.pow(Math.max(0, 1 - puff.t / puff.hl), 1.25) : 0\n if (puff.ring) moveOnRing(puff, puff.ring, wind)\n else moveFree(puff, dt, age, heat, wind)\n const size = puff.s0 + (puff.s1 - puff.s0) * (1 - Math.exp(-puff.t * puff.gk)) + puff.sg * Math.min(puff.t, GROWTH_CAP_SECONDS)\n puff.size = size * Math.min(1, puff.t * ARRIVAL_FRAME + ARRIVAL_BASE)\n puff.age = age\n puff.heat = heat\n }\n}\n","import { OrderedPool } from '../pool'\n\n/** Shrapnel, sparks and embers, stretched along their velocity by the shader (L239-293). `kind`: 0 hot fragment, 1 steel ball, 2 white spark, 3 glass, 4 slow ember. */\nexport interface Spark {\n x: number\n y: number\n z: number\n vx: number\n vy: number\n vz: number\n t: number\n life: number\n w: number\n st: number\n kind: number\n drag: number\n g: number\n bounce: number\n age: number\n}\n\nexport type SparkInit = Partial<Omit<Spark, 't' | 'age'>>\n\nconst GROUND_LIFT = 0.02\nconst BOUNCE_SLIDE = 0.55\n\nexport function createSparkPool(capacity: number): OrderedPool<Spark> {\n return new OrderedPool<Spark>(capacity, () => ({ x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, t: 0, life: 0.8, w: 0.04, st: 0.035, kind: 0, drag: 0.6, g: 9.8, bounce: 0.35, age: 0 }))\n}\n\nexport function spawnSpark(pool: OrderedPool<Spark>, init: SparkInit): Spark {\n const spark = pool.acquire()\n spark.x = init.x ?? 0\n spark.y = init.y ?? 0\n spark.z = init.z ?? 0\n spark.vx = init.vx ?? 0\n spark.vy = init.vy ?? 0\n spark.vz = init.vz ?? 0\n spark.t = 0\n spark.life = init.life ?? 0.8\n spark.w = init.w ?? 0.04\n spark.st = init.st ?? 0.035\n spark.kind = init.kind ?? 0\n spark.drag = init.drag ?? 0.6\n spark.g = init.g ?? 9.8\n spark.bounce = init.bounce ?? 0.35\n spark.age = 0\n return spark\n}\n\nfunction ageOrKeep(spark: Spark, dt: number): boolean {\n spark.t += dt\n return spark.t < spark.life\n}\n\nexport interface SparkStep {\n readonly dt: number\n readonly groundAt: (x: number, z: number) => number\n}\n\n/** `updateSparks` (L277-293): drag, gravity, a bounce that kills some speed on the ground. */\nexport function updateSparks(pool: OrderedPool<Spark>, step: SparkStep): void {\n pool.compact(ageOrKeep, step.dt)\n for (const spark of pool.items) {\n const damping = Math.exp(-spark.drag * step.dt)\n spark.vx *= damping\n spark.vy *= damping\n spark.vz *= damping\n spark.vy -= spark.g * step.dt\n spark.x += spark.vx * step.dt\n spark.y += spark.vy * step.dt\n spark.z += spark.vz * step.dt\n const floor = step.groundAt(spark.x, spark.z) + GROUND_LIFT\n if (spark.y < floor) {\n spark.y = floor\n if (spark.vy < 0) {\n spark.vy *= -spark.bounce\n spark.vx *= BOUNCE_SLIDE\n spark.vz *= BOUNCE_SLIDE\n }\n }\n spark.age = spark.t / spark.life\n }\n}\n","import type { VfxCore } from './core'\nimport { spawnChunk } from './pools/chunks'\nimport type { Chunk, ChunkInit } from './pools/chunks'\nimport { spawnGlow } from './pools/glows'\nimport type { Glow, GlowInit } from './pools/glows'\nimport { spawnPuff } from './pools/puffs'\nimport type { Puff, PuffInit } from './pools/puffs'\nimport { spawnSpark } from './pools/sparks'\nimport type { Spark, SparkInit } from './pools/sparks'\nimport type { RandomStream } from './random'\n\nconst HOT_GLOW_COLOR = [1, 0.45, 0.12] as const\n\n/** The spawners count what an explosion makes (the Lab's `evCount`) on top of the pools, and a hot chunk carries a halo, as in the Lab (L371-376). */\nexport function addPuff(core: VfxCore, random: RandomStream, init: PuffInit): Puff {\n core.spawned.puffs++\n const puff = spawnPuff(core.puffs, random, init)\n core.trace?.puff?.(puff)\n return puff\n}\n\nexport function addSpark(core: VfxCore, init: SparkInit): Spark {\n core.spawned.sparks++\n const spark = spawnSpark(core.sparks, init)\n core.trace?.spark?.(spark)\n return spark\n}\n\nexport function addGlow(core: VfxCore, init: GlowInit): Glow {\n const glow = spawnGlow(core.glows, init)\n core.trace?.glow?.(glow)\n return glow\n}\n\nexport function addChunk(core: VfxCore, random: RandomStream, init: ChunkInit): Chunk {\n const chunk = spawnChunk(core.chunks, random, init)\n core.spawned.chunks++\n if (chunk.hot && core.layers.shrapnel) {\n chunk.glow = addGlow(core, { follow: chunk, s: 0.35 + chunk.sx, i: 2.2, life: random.between(1, 2.2), color: HOT_GLOW_COLOR, curve: 1.5 })\n }\n core.trace?.chunk?.(chunk)\n return chunk\n}\n","import type { Rgb } from '../color'\nimport { SMOKE_COLORS } from '../constants'\nimport type { VfxCore } from '../core'\nimport { OrderedPool } from '../pool'\nimport type { RandomStream } from '../random'\nimport { addPuff, addSpark } from '../spawn'\nimport type { Follow } from '../types'\nimport { lerp } from '../vector'\n\n/** A lingering fire (L424-442): it grows, gives flames that cool into smoke, and fades out. */\nexport interface Fire {\n x: number\n y: number\n z: number\n r: number\n r0: number\n grow: number\n life: number\n t: number\n rate: number\n acc: number\n smoke: number\n smokeAcc: number\n fade: number\n follow: Follow | null\n fx: number\n fy: number\n fz: number\n color: Rgb\n rng: RandomStream\n handle: number\n}\n\nexport type FireInit = Partial<Pick<Fire, 'x' | 'y' | 'z' | 'r' | 'grow' | 'life' | 'rate' | 'smoke' | 'follow' | 'fx' | 'fy' | 'fz' | 'color'>> & { startRadius?: number }\n\nexport function createFirePool(capacity: number, placeholder: RandomStream): OrderedPool<Fire> {\n return new OrderedPool<Fire>(capacity, () => ({\n x: 0, y: 0, z: 0, r: 0.6, r0: 0.6, grow: 0, life: 6, t: 0, rate: 22, acc: 0, smoke: 0.6, smokeAcc: 0, fade: 1, follow: null, fx: 0, fy: 0, fz: 0, color: SMOKE_COLORS.black,\n rng: placeholder, handle: 0,\n }))\n}\n\nexport function spawnFire(pool: OrderedPool<Fire>, random: RandomStream, init: FireInit, handle: number): Fire {\n const fire = pool.acquire()\n fire.x = init.x ?? 0\n fire.y = init.y ?? 0\n fire.z = init.z ?? 0\n fire.r = init.r ?? 0.6\n fire.r0 = init.startRadius ?? fire.r\n fire.grow = init.grow ?? 0\n fire.life = init.life ?? 6\n fire.t = 0\n fire.rate = init.rate ?? 22\n fire.acc = 0\n fire.smoke = init.smoke ?? 0.6\n fire.smokeAcc = 0\n fire.fade = 1\n fire.follow = init.follow ?? null\n fire.fx = init.fx ?? 0\n fire.fy = init.fy ?? 0\n fire.fz = init.fz ?? 0\n fire.color = init.color ?? SMOKE_COLORS.black\n fire.rng = random\n fire.handle = handle\n return fire\n}\n\nexport function addFire(core: VfxCore, random: RandomStream, init: FireInit, handle: number): Fire {\n const fire = spawnFire(core.fires, random, init, handle)\n core.trace?.fire?.(fire)\n return fire\n}\n\nfunction positionOf(follow: Follow): { x: number; y: number; z: number } {\n return 'position' in follow ? follow.position : follow\n}\n\nfunction emitFlames(core: VfxCore, fire: Fire, radius: number, dt: number): void {\n const random = fire.rng\n fire.acc += dt * fire.rate * (0.5 + radius * 0.9) * fire.fade * core.clock.lod\n while (fire.acc >= 1) {\n fire.acc -= 1\n const angle = random.next() * Math.PI * 2\n const distance = Math.sqrt(random.next()) * radius\n addPuff(core, random, {\n x: fire.x + Math.cos(angle) * distance, y: fire.y + 0.05, z: fire.z + Math.sin(angle) * distance,\n vx: random.between(-0.2, 0.2), vy: random.between(0.6, 1.4), vz: random.between(-0.2, 0.2),\n s0: 0.1, s1: random.between(0.28, 0.46) * (0.8 + Math.min(radius, 3) * 0.22), sg: -0.08,\n life: random.between(0.45, 0.85), h0: random.between(1, 1.25), hl: random.between(0.35, 0.6),\n drag: 1.5, buoy: 3.2, color: fire.color, kind: 0, wind: 0.4,\n })\n }\n}\n\nfunction emitSmoke(core: VfxCore, fire: Fire, radius: number, dt: number): void {\n const random = fire.rng\n fire.smokeAcc += dt * fire.smoke * (1 + radius * 0.8) * fire.fade * core.clock.lod\n while (fire.smokeAcc >= 1 && core.layers.smoke) {\n fire.smokeAcc -= 1\n addPuff(core, random, {\n x: fire.x + random.between(-radius, radius) * 0.5, y: fire.y + 0.6 + radius * 0.3, z: fire.z + random.between(-radius, radius) * 0.5,\n vy: random.between(0.8, 1.5), s0: 0.25, s1: 0.6 + Math.min(radius, 2) * 0.3, sg: 0.3, life: random.between(4, 6.5),\n h0: 0.25, hl: 0.25, drag: 0.6, buoy: 0.55, color: fire.color, kind: 0,\n })\n }\n}\n\nfunction emitEmber(core: VfxCore, fire: Fire, radius: number, dt: number): void {\n const random = fire.rng\n if (!(random.next() < dt * 6 * fire.fade && core.layers.shrapnel)) return\n addSpark(core, {\n x: fire.x + random.between(-radius, radius) * 0.6, y: fire.y + 0.4, z: fire.z + random.between(-radius, radius) * 0.6,\n vx: random.between(-0.4, 0.4), vy: random.between(1.5, 3.2), vz: random.between(-0.4, 0.4),\n life: random.between(0.8, 1.6), w: 0.025, st: 0.02, kind: 4, g: -0.6, drag: 1.2,\n })\n}\n\n/** `updateFires` (L430-442), walking the list backwards like the Lab. */\nexport function updateFires(core: VfxCore, dt: number): void {\n const pool = core.fires\n for (let index = pool.items.length - 1; index >= 0; index--) {\n const fire = pool.items[index]!\n fire.t += dt\n if (fire.follow) {\n const point = positionOf(fire.follow)\n fire.x = point.x + fire.fx\n fire.y = point.y + fire.fy\n fire.z = point.z + fire.fz\n }\n const radius = fire.grow ? lerp(fire.r0, fire.r, Math.min(1, fire.t / fire.grow)) : fire.r\n fire.fade = fire.t < fire.life - 1.5 ? Math.min(1, fire.t * 3) : Math.max(0, (fire.life - fire.t) / 1.5)\n if (fire.t >= fire.life) {\n pool.removeAt(index)\n continue\n }\n if (!core.layers.fire) continue\n emitFlames(core, fire, radius, dt)\n emitSmoke(core, fire, radius, dt)\n emitEmber(core, fire, radius, dt)\n }\n}\n","import type { Expr } from './expression'\n\n/** Constructors for the computed fields of a recipe. They only build data (`{ $, a }`); `expression.ts` runs it. */\nconst call =\n (name: string) =>\n (...args: readonly unknown[]): Expr => ({ $: name, a: args })\n\nexport const rnd = call('rnd')\nexport const rand = call('rand')\nexport const add = call('add')\nexport const concat = call('concat')\nexport const sub = call('sub')\nexport const mul = call('mul')\nexport const div = call('div')\nexport const mod = call('mod')\nexport const neg = call('neg')\nexport const abs = call('abs')\nexport const sqrt = call('sqrt')\nexport const exp = call('exp')\nexport const sin = call('sin')\nexport const cos = call('cos')\nexport const atan2 = call('atan2')\nexport const min = call('min')\nexport const max = call('max')\nexport const floor = call('floor')\nexport const ceil = call('ceil')\nexport const round = call('round')\nexport const clamp = call('clamp')\nexport const lerp = call('lerp')\nexport const ease = call('ease')\nexport const lt = call('lt')\nexport const gt = call('gt')\nexport const le = call('le')\nexport const ge = call('ge')\nexport const eq = call('eq')\nexport const ne = call('ne')\nexport const not = call('not')\nexport const and = call('and')\nexport const or = call('or')\nexport const ite = call('ite')\nexport const vadd = call('vadd')\nexport const vsub = call('vsub')\nexport const vscale = call('vscale')\nexport const vnorm = call('vnorm')\nexport const vneg = call('vneg')\nexport const vlerp = call('vlerp')\nexport const vx = call('vx')\nexport const vy = call('vy')\nexport const vz = call('vz')\nexport const vdist = call('vdist')\nexport const vwith = call('with')\nexport const pick = call('at')\nexport const scaled = call('scaled')\nexport const param = (...values: readonly unknown[]): Expr => ({ $: 'param', a: [values] })\n/** Binds `name` to `value` while `body` resolves: the page's `const a = ...` inside a spawn callback. */\nexport const local = (name: string, value: unknown, body: unknown): Expr => ({ $: 'let', a: [name, value, body] })\nexport const query = (behavior: string, name: string, ...args: readonly unknown[]): Expr => ({ $: 'query', a: [behavior, name, ...args] })\nexport const variable = (name: string): Expr => ({ $: 'var', a: [name] })\nexport const cameraForward = call('cameraForward')\nexport const TIME: Expr = { $: 't' }\nexport const PARAM: Expr = { $: 'paramIndex' }\nexport const DELTA: Expr = { $: 'dt' }\nexport const INDEX: Expr = { $: 'index' }\nexport const QUALITY: Expr = { $: 'quality' }\nexport const FRESH: Expr = { $: 'fresh' }\nexport const DIRECTION: Expr = { $: 'direction' }\nexport const NORMAL: Expr = { $: 'normal' }\nexport const CAMERA_POSITION: Expr = { $: 'cameraPosition' }\nexport const UP = [0, 1, 0] as const\n","import type { Computed, EffectStep, ScreenKey } from './types'\n\ntype System = 'add' | 'normal' | 'streak'\n\n/** Constructors for the steps of a recipe (they only build data). */\nexport const burst = (system: System, count: Computed, spawn: Computed): EffectStep => ({ op: 'burst', system, count, spawn })\nexport const spawn = (system: System, options: Computed): EffectStep => ({ op: 'spawn', system, spawn: options })\nexport const emit = (key: string, system: System, rate: Computed, options: Computed): EffectStep => ({ op: 'emit', key, system, rate, spawn: options })\n/** A solid particle (`MeshSpawn`): a cone, a sphere, an icosahedron. */\nexport const mesh = (options: Computed): EffectStep => ({ op: 'mesh', spawn: options })\nexport const debris = (options: Computed): EffectStep => ({ op: 'debris', spawn: options })\nexport const decal = (...args: readonly Computed[]): EffectStep => ({ op: 'decal', args })\nexport const ring = (...args: readonly Computed[]): EffectStep => ({ op: 'ring', args })\nexport const flash = (...args: readonly Computed[]): EffectStep => ({ op: 'flash', args })\nexport const lens = (...args: readonly Computed[]): EffectStep => ({ op: 'lens', args })\nexport const screen = (change: { add?: Partial<Record<ScreenKey, Computed>>; set?: Partial<Record<ScreenKey, Computed>>; max?: Partial<Record<ScreenKey, Computed>> }): EffectStep => ({ op: 'screen', ...change })\nexport const fireLight = (slot: 0 | 1, point: Computed, intensity: Computed, time: Computed): EffectStep => ({ op: 'fire_light', slot, point, intensity, time })\nexport const fireLightOff = (slot: 0 | 1): EffectStep => ({ op: 'fire_light_off', slot })\nexport const cue = (name: string, args?: Computed, draws?: number): EffectStep => ({ op: 'cue', cue: name, ...(args !== undefined ? { args } : {}), ...(draws !== undefined ? { draws } : {}) })\nexport const call = (macro: string, ...args: readonly Computed[]): EffectStep => ({ op: 'call', macro, args })\nexport const after = (time: Computed, ...steps: readonly EffectStep[]): EffectStep => ({ op: 'at', time, steps })\nexport const when = (condition: Computed, steps: readonly EffectStep[], otherwise?: readonly EffectStep[]): EffectStep => ({ op: 'when', if: condition, steps, ...(otherwise ? { otherwise } : {}) })\nexport const repeat = (count: Computed, ...steps: readonly EffectStep[]): EffectStep => ({ op: 'repeat', count, steps })\nexport const set = (name: string, value: Computed): EffectStep => ({ op: 'set', name, value })\nexport const behavior = (id: string, action: string, args?: Computed): EffectStep => ({ op: 'behavior', id, action, ...(args !== undefined ? { args } : {}) })\nexport const append = (name: string, value: Computed): EffectStep => ({ op: 'append', name, value })\nexport const camera = (eye: Computed, target: Computed, instant?: boolean): EffectStep => ({ op: 'camera', eye, target, ...(instant ? { instant } : {}) })\nexport const post = (values: Readonly<Record<string, Computed>>): EffectStep => ({ op: 'post', values })\n","import { gt, lt, and, INDEX, ite, mod, scaled, TIME } from '../dsl'\nimport { after, burst, debris, decal, emit, repeat, ring, screen, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst CENTRE: readonly [number, number, number] = [0, 1.1, 0]\nconst UP: readonly [number, number, number] = [0, 1, 0]\n\n/** W501-520: the belly bursts after a second and leaves a green cloud near the ground for eight. */\nexport const BLOATER_CLOUD: EffectDef = {\n id: 'bloater_cloud',\n pageId: 'inchado',\n team: 'creatures',\n budget: { particles: 180 },\n loop: 9,\n view: { eye: [5.5, 3.2, 6], target: [0, 1, 0] },\n timeline: [\n after(\n 1,\n burst('normal', 40, { p: CENTRE, r: 0.4, sp: [1.5, 4], drag: 2.2, life: [4, 6], size: [1, 2.2], grow: 2, c0: 0x6f8a3c, c1: 0x3b4a26, a: 0.32, fi: 0.08, g: 0.05 }),\n burst('normal', 30, { p: CENTRE, dir: UP, cone: 1.3, sp: [2, 4], life: 1.4, size: [0.04, 0.08], c0: 0x4a5a1c, g: 9.8, fi: 0, gnd: 3, ds: [0.1, 0.2] }),\n repeat(scaled(10), debris({ p: CENTRE, dir: UP, cone: 1.2, sp: [2, 5], size: [0.07, 0.05, 0.07], k: [0.6, 1.4], c: ite(mod(INDEX, 2), 0x6b7a52, 0x4a1a12), life: 9 })),\n decal(0, 0, 0, 1.6, 0x6a7a24, { grow: 0.6, life: 60, fluid: 'bile' }),\n ring([0, 0.02, 0], 4, 4, 8, 0x3a5a20, UP, 0.35),\n screen({ add: { shake: 0.15 } }),\n ),\n ],\n tick: [\n when(and(gt(TIME, 1), lt(TIME, 8)), [\n emit('gas', 'normal', 7, { p: [0, 0.4, 0], r: 3.4, flat: true, dir: UP, cone: 1.3, sp: [0.1, 0.35], drag: 0.4, life: [3, 5], size: [1.2, 2.2], grow: 1.5, c0: 0x6f8a3c, c1: 0x3b4a26, a: 0.22, fi: 0.3 }),\n emit('mote', 'add', 10, { p: [0, 0.6, 0], r: 3.6, flat: true, dir: UP, cone: 0.6, sp: [0.05, 0.25], life: [1.5, 2.5], size: [0.04, 0.07], c0: 0x5fe08a, a: 0.7, fi: 0.4 }),\n ]),\n ],\n}\n","import { add, and, gt, lt, mul, param, PARAM, pick, TIME, vnorm } from '../dsl'\nimport { after, call, decal, emit, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst BLOOD = param(0x8a0a0a, 0x5a0c10, 0x3a1c12, 0x1d1914)\nconst BLEED = [1, 0.7, 0.35]\n\n/** W449-473: blood by infection age: a mist at the entry, a jet at the exit, a pool that grows and darkens. */\nexport const BLOOD_HIT: EffectDef = {\n id: 'blood_hit',\n pageId: 'sangue',\n team: 'creatures',\n params: { id: 'age', options: ['fresh', 'recent', 'old', 'decrepit'], default: 0 },\n budget: { particles: 70, decals: 1 },\n loop: 3.2,\n view: { eye: [2.6, 1.9, 3.4], target: [0, 1, -0.6] },\n timeline: [\n after(0.3, call('blood_hit', [0, 1.3, 0.1], vnorm([0.15, 0.1, -1]), PARAM)),\n after(1.5, call('blood_hit', [0.1, 1.05, 0.1], vnorm([-0.2, 0.05, -1]), PARAM, 0.8)),\n when(lt(PARAM, 3), [after(0.7, decal(0, 0, -0.2, add(0.5, mul(0.7, pick(BLEED, PARAM))), BLOOD, { grow: add(2, mul(2, pick(BLEED, PARAM))), dark: 60, dry: 0x3b1d14 }))]),\n ],\n tick: [\n when(and(lt(PARAM, 2), gt(TIME, 0.3)), [\n emit('drip', 'normal', param(7, 3), {\n p: [0.05, 1.15, 0.17], box: [0.08, 0.05, 0], dir: [0, 1, 0], cone: 0.2, sp: [0, 0.2], life: 1.2, size: 0.03, c0: BLOOD, g: 9.8, fi: 0, gnd: 3, ds: [0.04, 0.08],\n }),\n ]),\n ],\n}\n","import { and, gt, lt, TIME } from '../dsl'\nimport { after, decal, emit, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst UP: readonly [number, number, number] = [0, 1, 0]\n\n/** W540-553: fifteen minutes in eight seconds: the pool dries, flies appear from the minute five and the body turns to dust. */\nexport const CORPSE_DECAY: EffectDef = {\n id: 'corpse_decay',\n pageId: 'corpo',\n team: 'creatures',\n budget: { particles: 40 },\n loop: 8.5,\n view: { eye: [2.4, 1.6, 2.6], target: [0, 0.3, -0.6] },\n timeline: [after(0.8, decal(0, 0, -0.9, 1.1, 0x5a0c10, { grow: 1.5, dark: 5, dry: 0x3b1d14 }))],\n tick: [\n when(and(gt(TIME, 2.5), lt(TIME, 7)), [\n emit('fly', 'normal', 6, { p: [0, 0.35, -0.8], r: 0.5, sp: [0.3, 0.8], life: [0.6, 1.2], size: 0.02, c0: 0x050505, a: 1, fi: 0.2 }),\n ]),\n when(and(gt(TIME, 6.8), lt(TIME, 7.6)), [\n emit('dust', 'normal', 40, { p: [0, 0.15, -0.8], box: [0.25, 0.05, 0.8], dir: UP, cone: 0.6, sp: [0.2, 0.6], drag: 1, life: [1, 1.8], size: [0.12, 0.22], grow: 3, c0: 0x2b2722, a: 0.5 }),\n ]),\n ],\n}\n","import { and, INDEX, ite, lt, not, pick, query, QUALITY, rnd, variable } from '../dsl'\nimport { behavior, repeat, set, spawn, when } from '../steps'\nimport type { EffectDef } from '../types'\n\n/** Which of the ten walkers run: the ones with red eyes (W192). */\nconst RUNNERS = [false, true, false, false, false, true, false, false, false, true]\nconst RUN_SPEED = 3.8\nconst WALK_SPEED = 0.7\nconst SPEED = ite(pick(RUNNERS, INDEX), RUN_SPEED, WALK_SPEED)\nconst walkTo = behavior('walkers', 'walk', { index: INDEX, to: [-12, 0, query('walkers', 'z', INDEX)], speed: SPEED })\n\n/** W562-571 (the page's `olhos`): ten zombies walk through fog from the east; the runners leave a short red trail behind their eyes. */\nexport const EYES_IN_FOG: EffectDef = {\n id: 'eyes_in_fog',\n pageId: 'olhos',\n team: 'creatures',\n budget: { particles: 20 },\n loop: 0,\n view: { eye: [-9, 1.7, 2], target: [6, 1.3, 2] },\n setup: [\n repeat(10, set('placeX', rnd(4, 34)), set('placeZ', rnd(-1.5, 5.5)), behavior('walkers', 'place', { index: INDEX, x: variable('placeX'), z: variable('placeZ'), facing: -Math.PI / 2 }), walkTo),\n ],\n timeline: [],\n tick: [\n repeat(\n 10,\n when(lt(query('walkers', 'x', INDEX), -8), [behavior('walkers', 'move_x', { index: INDEX, x: rnd(28, 36) }), walkTo], [when(not(query('walkers', 'is_walking', INDEX)), [walkTo])]),\n when(and(pick(RUNNERS, INDEX), lt(QUALITY, 2)), [\n spawn('streak', { p: query('walkers', 'point', INDEX, [0, 1.74, 0.2]), dir: [1, 0, 0], cone: 0, sp: RUN_SPEED, v: [-RUN_SPEED, 0, 0], life: 0.25, len: 0.06, c0: 0xe5392b, c1: 0x400000, a: 0.8 }),\n ]),\n ),\n ],\n}\n","import { ite, gt, lt, and, mul, sin, sub, vnorm, TIME } from '../dsl'\nimport { after, burst, debris, emit, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst SHOULDER: readonly [number, number, number] = [0.36, 1.46, 0.05]\nconst FRESH_BLOOD = 0x8a0a0a\n\n/** W487-500: the arm leaves as a loose piece and the shoulder spurts in pulses for half a second. */\nexport const DISMEMBER: EffectDef = {\n id: 'dismember',\n pageId: 'membro',\n team: 'creatures',\n budget: { particles: 60, debris: 1, decals: 4 },\n loop: 3.6,\n view: { eye: [3, 1.8, 2.2], target: [0.4, 1.1, 0] },\n timeline: [\n after(\n 0.4,\n debris({ p: SHOULDER, v: [2.2, 2.5, -0.6], sp: 0, size: [0.12, 0.12, 0.66], c: 0x6d7a5a, spin: 9, life: 12, bounce: 0.25 }),\n burst('normal', 12, { p: SHOULDER, r: 0.05, sp: [0.3, 1.2], drag: 3, life: [0.3, 0.6], size: [0.1, 0.16], grow: 3, c0: FRESH_BLOOD, a: 0.6 }),\n ),\n ],\n tick: [\n when(and(gt(TIME, 0.4), lt(TIME, 1.1)), [\n emit('jet', 'normal', mul(90, ite(gt(sin(mul(sub(TIME, 0.4), 28)), 0), 1, 0.2)), { p: SHOULDER, dir: vnorm([1, 0.35, 0.1]), cone: 0.25, sp: [1.5, 3], life: 1.4, size: [0.03, 0.055], c0: FRESH_BLOOD, g: 9.8, fi: 0, gnd: 3, ds: [0.05, 0.12] }),\n ]),\n ],\n}\n","import { ite, mod, INDEX, scaled, vnorm } from '../dsl'\nimport { after, burst, debris, decal, flash, repeat } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst FRESH_BLOOD = 0x8a0a0a\n\n/** W474-486: the head bursts, the eyes go out in the same frame and the body falls. */\nexport const HEADSHOT: EffectDef = {\n id: 'headshot',\n pageId: 'headshot',\n team: 'creatures',\n budget: { particles: 70, debris: 8, decals: 1 },\n loop: 3.2,\n view: { eye: [2.2, 1.8, 3], target: [0, 1.2, -0.5] },\n timeline: [\n after(\n 0.45,\n burst('normal', 20, { p: [0, 1.7, 0], r: 0.1, sp: [0.4, 1.8], drag: 3, life: [0.4, 0.75], size: [0.12, 0.2], grow: 3, c0: FRESH_BLOOD, a: 0.7 }),\n burst('normal', 35, { p: [0, 1.7, 0], dir: vnorm([0, 0.45, -1]), cone: 0.8, sp: [2, 5], life: 1.5, size: [0.035, 0.065], c0: FRESH_BLOOD, g: 9.8, fi: 0, gnd: 3, ds: [0.06, 0.14] }),\n repeat(scaled(8), debris({ p: [0, 1.7, 0], dir: vnorm([0, 0.6, -1]), cone: 0.9, sp: [1.5, 4], size: [0.06, 0.05, 0.06], k: [0.5, 1.2], c: ite(mod(INDEX, 2), 0xd9cfbf, 0x6d7a5a), life: 8 })),\n flash([0, 1.7, 0], 0xff3020, 1.2, 0.08, 4),\n ),\n after(1.4, decal(0, 0, -1.5, 0.9, FRESH_BLOOD, { grow: 2.4, dark: 60, dry: 0x3b1d14 })),\n ],\n}\n","import { and, gt, lt, TIME, vnorm } from '../dsl'\nimport { cue, emit, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst UP: readonly [number, number, number] = [0, 1, 0]\n\n/** W554-561: green motes while the body twitches (two random draws a frame for the arms), the eyes light up and a breath of steam on the first scream. */\nexport const REANIMATION: EffectDef = {\n id: 'reanimation',\n pageId: 'reanima',\n team: 'creatures',\n budget: { particles: 30 },\n loop: 6.5,\n view: { eye: [2.4, 1.4, 2.4], target: [0, 0.6, -0.4] },\n timeline: [],\n tick: [\n when(lt(TIME, 3.4), [\n when(gt(TIME, 1), [cue('zombie_arm_twitch', undefined, 2)]),\n when(gt(TIME, 0.6), [\n emit('mote', 'add', 8, { p: [0, 0.2, -0.8], box: [0.25, 0.05, 0.6], dir: UP, cone: 0.4, sp: [0.2, 0.5], life: [0.6, 1], size: [0.03, 0.05], c0: 0x9fc25a, a: 0.7, fi: 0.3 }),\n ]),\n ]),\n when(and(gt(TIME, 5), lt(TIME, 5.6)), [\n emit('br', 'normal', 20, { p: [0, 1.68, 0.2], dir: vnorm([0, 0.3, 1]), cone: 0.3, sp: [0.6, 1.1], drag: 1.5, life: 1.2, size: [0.05, 0.1], grow: 5, c0: 0xd8dde2, a: 0.2 }),\n ]),\n ],\n}\n","import { add, cos, div, INDEX, local, lt, mul, rand, sin, sub, TIME, variable, vnorm } from '../dsl'\nimport { after, emit, repeat, ring, screen, set, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst WHITE_RING = 0xe8e2d0\nconst PREPARATION_END = 1.7\nconst SHRIEK_END = 2.6\n\n/** W521-539: a steam of breath while the screamer prepares, then three pressure rings, a ring of dust and a burst of chromatic aberration. */\nexport const SCREAMER_SHRIEK: EffectDef = {\n id: 'screamer_shriek',\n pageId: 'grito',\n team: 'creatures',\n budget: { particles: 150 },\n loop: 5,\n view: { eye: [6, 3, 7], target: [0, 1, -1] },\n timeline: [\n after(\n PREPARATION_END,\n repeat(3, after(add(PREPARATION_END, mul(INDEX, 0.18)), ring([0, 0.03, 0], 0.3, 9, 0.9, WHITE_RING))),\n screen({ add: { chroma: 7, shake: 0.25 } }),\n ),\n ],\n tick: [\n when(lt(TIME, PREPARATION_END), [\n emit('vap', 'normal', 18, { p: [0, 1.68, 0.2], dir: vnorm([0, 0.5, 1]), cone: 0.3, sp: [0.6, 1.2], drag: 1.5, life: [0.8, 1.3], size: [0.05, 0.1], grow: 5, c0: 0xd8dde2, a: 0.2 }),\n ], [\n when(lt(TIME, SHRIEK_END), [\n set('shriekRadius', mul(div(sub(TIME, PREPARATION_END), 0.9), 8)),\n emit(\n 'dust', 'normal', 140,\n local('angle', mul(rand(), 6.283), {\n p: [mul(cos(variable('angle')), variable('shriekRadius')), 0.05, mul(sin(variable('angle')), variable('shriekRadius'))],\n dir: vnorm([cos(variable('angle')), 0.6, sin(variable('angle'))]),\n cone: 0.3, sp: [0.6, 1.6], drag: 2, life: [0.8, 1.4], size: [0.2, 0.35], grow: 3, c0: 0x5d574d, a: 0.35,\n }),\n ),\n ]),\n ]),\n ],\n}\n","import type { Vec3Tuple } from '../workshop/types'\n\n/**\n * A point of a prop's own space in the world, for a prop placed at `translation` and turned by `rotation` (x, y, z radians, applied in the order x, y,\n * z) as three.js composes it: how the page's `carW` and `tbW` find the bumper or a table leg.\n */\nexport function localToWorld(translation: Vec3Tuple, rotation: Vec3Tuple, local: Vec3Tuple): Vec3Tuple {\n const [x, y, z] = local\n const [a, c, e] = [Math.cos(rotation[0]), Math.cos(rotation[1]), Math.cos(rotation[2])]\n const [b, d, f] = [Math.sin(rotation[0]), Math.sin(rotation[1]), Math.sin(rotation[2])]\n const [ae, af, be, bf] = [a * e, a * f, b * e, b * f]\n return [\n c * e * x + -c * f * y + d * z + translation[0],\n (af + be * d) * x + (ae - bf * d) * y + -b * c * z + translation[1],\n (bf - ae * d) * x + (be + af * d) * y + a * c * z + translation[2],\n ]\n}\n","import { add, eq, FRESH, INDEX, ite, PARAM, pick, rnd, variable, vadd } from '../dsl'\nimport { localToWorld } from '../props'\nimport { after, behavior, call, camera, decal, repeat, set, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst MATERIALS = ['concrete', 'metal', 'wood', 'glass', 'flesh', 'water']\nconst CAR_ORIGIN = [5.6, 0, 0.8] as const\nconst CAR_ROTATION = [0, -0.4, 0] as const\nconst FACING_WALL = [0, 0, 1] as const\nconst UP = [0, 1, 0] as const\nconst GLASS = 3\nconst FLESH = 4\nconst WATER = 5\nconst SHOT_TIMES = [0, 0.55, 1.1]\nconst LAST_SHOT = 2\nconst BUMPER = localToWorld(CAR_ORIGIN, CAR_ROTATION, [0.96, 0.7, 0.4])\nconst CAR_SIDE = [Math.cos(CAR_ROTATION[1]), 0, -Math.sin(CAR_ROTATION[1])] as const\n/** Where each material is hit and the way the surface faces (W752-756). */\nconst POINTS = [[-2, 1.6, -4.45], BUMPER, [2, 1.3, -4.36], [-5, 1.9, -4.45], [0, 1.25, 0.16], [-1.5, 0.012, 2.2]]\nconst NORMALS = [FACING_WALL, CAR_SIDE, FACING_WALL, FACING_WALL, FACING_WALL, UP]\n/** The eye the page glides to for each material on the first run (W773-774); the car's is found from the car. */\nconst EYES = [[-2, 1.6, -1.8], localToWorld(CAR_ORIGIN, CAR_ROTATION, [3, 1.4, 1.5]), [2, 1.3, -1.6], [-5, 1.9, -1.8], [0, 1.2, 2.6], [-1.5, 0.8, 4.6]]\n/** The shop window as the glass texture sees it (`crackAt` takes pixels): 2.6 x 1.8 m on 256 x 180. */\nconst WINDOW = { centreX: -5, centreY: 1.9, width: 2.6, height: 1.8, pixelsX: 256, pixelsY: 180 }\n\nconst scatter = ite(eq(PARAM, WATER), [rnd(-0.6, 0.6), 0, rnd(-0.3, 0.3)], ite(eq(PARAM, FLESH), [rnd(-0.1, 0.1), rnd(-0.15, 0.15), 0], [rnd(-0.4, 0.4), rnd(-0.3, 0.3), 0]))\nconst hitX = { $: 'vx', a: [variable('hit')] }\nconst hitY = { $: 'vy', a: [variable('hit')] }\nconst pixelX = { $: 'mul', a: [{ $: 'add', a: [{ $: 'div', a: [{ $: 'sub', a: [hitX, WINDOW.centreX] }, WINDOW.width] }, 0.5] }, WINDOW.pixelsX] }\nconst pixelY = { $: 'mul', a: [{ $: 'sub', a: [1, { $: 'add', a: [{ $: 'div', a: [{ $: 'sub', a: [hitY, WINDOW.centreY] }, WINDOW.height] }, 0.5] }] }, WINDOW.pixelsY] }\n\n/** W762-775: three hits, 0.55 s apart, on the material the parameter names; the window keeps its cracks and the puddle takes a dark patch. */\nexport const IMPACT_BY_MATERIAL: EffectDef = {\n id: 'impact',\n pageId: 'impacto',\n team: 'weapons',\n params: { id: 'material', options: MATERIALS, default: 0 },\n budget: { particles: 40, decals: 1 },\n loop: 2.6,\n view: { eye: [0, 1.8, 4.5], target: [0, 1.2, -2] },\n props: ['cracks'],\n timeline: [\n when({ $: 'eq', a: [PARAM, WATER] }, [decal(-1.5, 0, 2.2, 3.2, 0x0d1218, { grow: 0, life: 60, ar: 0.55 })]),\n repeat(\n SHOT_TIMES.length,\n after(\n add(0.2, pick(SHOT_TIMES, INDEX)),\n set('hit', vadd(pick(POINTS, PARAM), scatter)),\n call('impact', pick(MATERIALS, PARAM), variable('hit'), pick(NORMALS, PARAM), 1, 0),\n when(eq(PARAM, GLASS), [behavior('cracks', 'crack', { x: pixelX, y: pixelY, big: eq(INDEX, LAST_SHOT) })]),\n ),\n ),\n when(FRESH, [camera(pick(EYES, PARAM), pick(POINTS, PARAM))]),\n ],\n}\n","import { and, DELTA, gt, INDEX, le, lt, mod, mul, add, pick, rnd, sub, TIME, variable } from '../dsl'\nimport { after, append, call, cue, decal, repeat, set, spawn, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst BLOOD = [0x8a0a0a, 0x5a0c10, 0x3a1c12, 0x1d1914]\n/** Where the five bodies stand along the line of the shot (W786). */\nconst BODY_Z = [2.2, 1, -0.2, -1.4, -2.6]\nconst MUZZLE_Z = 8.5\nconst SPEED = 12\nconst WALL_Z = -4.4\nconst FORWARD = [0, 0, -1] as const\nconst bulletAt = [0, 1.45, variable('bulletZ')]\n\n/** W776-785: a tracer of the Barrett crosses five bodies, each bleeding the colour of its age, then hits the wall. */\nexport const PENETRATION: EffectDef = {\n id: 'penetration',\n pageId: 'penetra',\n team: 'weapons',\n budget: { particles: 250 },\n loop: 4.2,\n view: { eye: [4.2, 2.2, 6.5], target: [0, 1.2, -0.5] },\n setup: [\n cue('zombie_visible', { index: -1, visible: false }),\n repeat(\n BODY_Z.length,\n set('bodyX', rnd(-0.05, 0.05)),\n set('bodyTurn', rnd(-0.3, 0.3)),\n append('bodiesX', variable('bodyX')),\n cue('zombie_place', { index: INDEX, x: variable('bodyX'), z: pick(BODY_Z, INDEX), turn: variable('bodyTurn') }),\n ),\n ],\n timeline: [set('bulletZ', MUZZLE_Z), set('bulletOn', true), call('muzzle', 'barrett', [0, 1.45, MUZZLE_Z], FORWARD)],\n tick: [\n when(variable('bulletOn'), [\n set('previousZ', variable('bulletZ')),\n set('bulletZ', sub(variable('bulletZ'), mul(SPEED, DELTA))),\n spawn('streak', { p: bulletAt, dir: FORWARD, cone: 0, sp: 12, life: 0.04, len: 0.07, c0: 0xffd090 }),\n spawn('add', { p: bulletAt, size: 0.09, life: 0.05, c0: 0xffc070, fi: 0 }),\n spawn('normal', { p: bulletAt, r: 0.02, sp: [0, 0.05], life: [1.2, 2], size: 0.05, grow: 3, c0: 0xc8ccd2, a: 0.12, fi: 0.1 }),\n repeat(\n BODY_Z.length,\n when(and(gt(variable('previousZ'), pick(BODY_Z, INDEX)), le(variable('bulletZ'), pick(BODY_Z, INDEX))), [\n call('blood_hit', [0, 1.45, sub(pick(BODY_Z, INDEX), 0.1)], [0, 0.05, -1], mod(INDEX, 4), 0.8),\n cue('zombie_fall', { index: INDEX, delay: 0.05, duration: 0.6 }),\n after(\n add(TIME, 0.6),\n decal(pick(variable('bodiesX'), INDEX), 0, sub(pick(BODY_Z, INDEX), 1.2), 0.6, pick(BLOOD, mod(INDEX, 4)), { grow: 2, dark: 60, dry: 0x3b1d14 }),\n ),\n ]),\n ),\n when(lt(variable('bulletZ'), WALL_Z), [set('bulletOn', false), call('impact', 'concrete', [0, 1.45, -4.45], [0, 0, 1], 2.2)]),\n ]),\n ],\n}\n","import { add, gt, INDEX, lt, and, mul, TIME } from '../dsl'\nimport { after, call, cue, debris, emit, repeat, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst TIP = [-0.8, 1.38, 3.4 - 0.78] as const\nconst PORT = [-0.76, 1.4, 3.32] as const\nconst FORWARD = [0, 0, -1] as const\nconst UP = [0, 1, 0] as const\nconst BURST_SHOTS = 12\n\n/** W787-797: twelve rounds of an AK with their brass, the empty magazine falling at 1.8 s and a thread of smoke from the hot barrel. */\nexport const CASINGS_AND_MAGAZINE: EffectDef = {\n id: 'casings_and_magazine',\n pageId: 'capsulas',\n team: 'weapons',\n budget: { particles: 200, debris: 13 },\n loop: 4.6,\n view: { eye: [0.9, 1.3, 4.6], target: [-0.6, 0.6, 3.1] },\n setup: [cue('gun_visible', { visible: true }), cue('zombie_visible', { index: -1, visible: false })],\n timeline: [\n repeat(\n BURST_SHOTS,\n after(add(0.2, mul(INDEX, 0.1)), call('muzzle', 'ak', TIP, FORWARD), call('casing', PORT, ''), cue('gun_recoil', { kick: 0.05 })),\n ),\n after(1.8, debris({ p: [-0.8, 1.15, 3.3], v: [0.1, -0.5, 0.1], sp: 0, size: [0.05, 0.2, 0.09], c: 0x2a2a2a, spin: 3, life: 20, bounce: 0.2 })),\n ],\n tick: [\n when(and(gt(TIME, 1.4), lt(TIME, 3.6)), [\n emit('hot', 'normal', 10, { p: TIP, dir: UP, cone: 0.25, sp: [0.2, 0.5], life: [1.4, 2.2], size: [0.03, 0.06], grow: 5, c0: 0x9a9a96, a: 0.18, g: -0.2, fi: 0.2 }),\n ]),\n ],\n}\n","import { add, and, div, DELTA, FRESH, ge, gt, ite, lerp, lt, max, min, mul, not, scaled, sin, sub, TIME, variable, vadd, vlerp, vwith } from '../dsl'\nimport { call, cue, debris, burst, decal, emit, fireLight, fireLightOff, flash, repeat, set, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst FROM = [-4, 1.6, 5] as const\nconst TARGET = [0, 0, -0.5] as const\nconst UP = [0, 1, 0] as const\nconst FLIGHT = 1\nconst ARC_HEIGHT = 2.2\nconst GLASS_GREEN = 0x3a6a3a\nconst OBJECT_ID_DRAWS = 12\nconst at = (offset: readonly [number, number, number]) => vadd(TARGET, offset)\n\n/** W798-818 (`lab: 1`): a bottle in an arc with a wick burning, glass and a splash of fire on landing, and a pool of fire that spreads for 6 s. */\nexport const MOLOTOV: EffectDef = {\n id: 'molotov',\n pageId: 'molotov',\n team: 'weapons',\n lab: 'molotov',\n budget: { particles: 300, lights: 1 },\n loop: 9.5,\n view: { eye: [5, 3, 6.5], target: [0, 0.6, -0.5] },\n // three.js draws 12 random numbers (the object ids of the bottle's geometry, material and mesh) the first time the page builds the bottle\n setup: [cue('zombie_visible', { index: -1, visible: false }), when(FRESH, [cue('prop_created', { prop: 'bottle' }, OBJECT_ID_DRAWS)])],\n timeline: [set('landed', false), cue('bottle_visible', { visible: true })],\n tick: [\n when(lt(TIME, FLIGHT), [\n set('bottle', vwith(vlerp(FROM, TARGET, TIME), 1, add(lerp(FROM[1], TARGET[1], TIME), mul(sin(mul(TIME, Math.PI)), ARC_HEIGHT)))),\n cue('bottle_move', { position: variable('bottle'), spin: mul(10, DELTA) }),\n emit('wick', 'add', 40, { p: vadd(variable('bottle'), [0, 0.12, 0]), r: 0.03, sp: [0.1, 0.3], life: [0.15, 0.3], size: [0.08, 0.14], grow: 0.5, c0: 0xffd27a, c1: 0xff5010, fi: 0, g: -1 }),\n emit('wsm', 'normal', 14, { p: variable('bottle'), size: [0.06, 0.1], grow: 4, life: [0.8, 1.2], c0: 0x3a3530, a: 0.3, sp: [0, 0.2] }),\n ], [\n when(not(variable('landed')), [\n set('landed', true),\n cue('bottle_visible', { visible: false }),\n repeat(scaled(12), debris({ p: at([0, 0.1, 0]), dir: UP, cone: 1.3, sp: [1, 3], size: [0.04, 0.01, 0.04], c: GLASS_GREEN, life: 12 })),\n burst('add', 45, { p: at([0, 0.15, 0]), dir: UP, cone: 1.35, sp: [2, 4], drag: 2, life: [0.3, 0.6], size: [0.3, 0.5], c0: 0xffd27a, c1: 0xa02008, fi: 0.05 }),\n flash(at([0, 0.6, 0]), 0xff8a30, 6, 0.3, 12, true),\n decal(TARGET[0], 0, TARGET[2], 4.2, 0x0d0b09, { grow: 0.8, life: 60 }),\n ]),\n ]),\n when(and(gt(TIME, FLIGHT), lt(TIME, 8.2)), [\n set('poolRadius', min(2.2, add(0.5, mul(sub(TIME, 1), 2.1)))),\n set('poolFade', ite(gt(TIME, 7), max(0.1, sub(1, div(sub(TIME, 7), 1.2))), 1)),\n call('fire_emit', 'mol', at([0, 0.05, 0]), variable('poolRadius'), mul(1.6, variable('poolFade')), { smoke: 16 }),\n fireLight(0, at([0, 1, 0]), mul(3.2, variable('poolFade')), TIME),\n ], [\n when(ge(TIME, 8.2), [\n fireLightOff(0),\n emit('emb', 'add', 6, { p: TARGET, r: 2, flat: true, size: [0.04, 0.07], life: [0.6, 1.2], c0: 0xff6a20, a: 0.8, fi: 0.3 }),\n ]),\n ]),\n ],\n}\n","import { add, atan2, DELTA, FRESH, INDEX, lt, mul, neg, not, pick, sub, TIME, variable } from '../dsl'\nimport { call, cue, repeat, set, when } from '../steps'\nimport type { EffectDef } from '../types'\n\n/** The five bodies the blast reaches (W826), where they stand and the way each faces (`atan2(-x, -z)`, toward the origin). */\nconst BODIES = [[1.6, 0.8], [-1.8, 1.2], [0.6, -2], [-1, -1.8], [2.4, -1.2]] as const\nconst BODY_X = BODIES.map(([x]) => x)\nconst BODY_Z = BODIES.map(([, z]) => z)\nconst FACING = BODIES.map(([x, z]) => Math.atan2(-x, -z))\nconst FUSE = 1.8\nconst GRAVITY = 9.8\nconst FLOOR = 0.07\nconst BOUNCE = -0.4\nconst SIDE_LOSS = 0.5\n\n/** W819-830 (`lab: 1`): a grenade thrown in an arc, bouncing on the asphalt, then the Workshop's blast and the five bodies turning away to fall. */\nexport const GRENADE: EffectDef = {\n id: 'grenade',\n pageId: 'granada',\n team: 'weapons',\n lab: 'm67',\n budget: { particles: 400, debris: 16, decals: 1 },\n loop: 6.5,\n view: { eye: [7, 3.4, 7.5], target: [0, 0.8, -0.2] },\n setup: [\n cue('zombie_visible', { index: -1, visible: false }),\n // three.js draws 12 random numbers (the object ids of the grenade's geometry, material and mesh) the first time the page builds it\n when(FRESH, [cue('prop_created', { prop: 'grenade' }, 12)]),\n repeat(BODIES.length, cue('zombie_place', { index: INDEX, x: pick(BODY_X, INDEX), z: pick(BODY_Z, INDEX), turn: pick(FACING, INDEX) })),\n ],\n timeline: [\n set('px', -4), set('py', 1.4), set('pz', 4.5),\n set('vx', 3.2), set('vy', 2.4), set('vz', -3.6),\n set('boom', false),\n cue('grenade_visible', { visible: true }),\n ],\n tick: [\n when(lt(TIME, FUSE), [\n set('vy', sub(variable('vy'), mul(GRAVITY, DELTA))),\n set('px', add(variable('px'), mul(variable('vx'), DELTA))),\n set('py', add(variable('py'), mul(variable('vy'), DELTA))),\n set('pz', add(variable('pz'), mul(variable('vz'), DELTA))),\n when(lt(variable('py'), FLOOR), [\n set('py', FLOOR),\n set('vy', mul(variable('vy'), BOUNCE)),\n set('vx', mul(variable('vx'), SIDE_LOSS)),\n set('vz', mul(variable('vz'), SIDE_LOSS)),\n ]),\n cue('grenade_move', { position: [variable('px'), variable('py'), variable('pz')] }),\n ], [\n when(not(variable('boom')), [\n set('boom', true),\n cue('grenade_visible', { visible: false }),\n call('explode', [variable('px'), 0, variable('pz')], 1),\n repeat(\n BODIES.length,\n cue('zombie_turn_and_fall', {\n index: INDEX,\n turn: atan2(neg(sub(pick(BODY_X, INDEX), variable('px'))), neg(sub(pick(BODY_Z, INDEX), variable('pz')))),\n delay: 0.02,\n duration: 0.45,\n }),\n ),\n ]),\n ]),\n ],\n}\n","import { add, and, cos, gt, ge, INDEX, ite, lt, mod, mul, rnd, scaled, sin, sub, TIME, variable, vadd, concat } from '../dsl'\nimport { after, append, burst, call, cue, debris, emit, fireLight, fireLightOff, repeat, set, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst BARREL = [-4.4, 0, 1.4] as const\nconst UP = [0, 1, 0] as const\nconst RED = 0x9b1c18\nconst YELLOW = 0xd8b02a\nconst POOLS = 3\nconst above = (height: number) => vadd(BARREL, [0, height, 0])\n\n/** W831-845 (`lab: 1`): the barrel goes up in a mushroom, its lid and red scraps fly, three pools of petrol burn for 5 s and a black column marks it from afar. */\nexport const EXPLOSIVE_BARREL: EffectDef = {\n id: 'explosive_barrel',\n pageId: 'barril',\n team: 'weapons',\n lab: 'barrel',\n budget: { particles: 450, lights: 1 },\n loop: 9,\n view: { eye: [2.4, 3, 7.5], target: [-4.4, 1.6, 1.4] },\n setup: [cue('zombie_visible', { index: -1, visible: false })],\n timeline: [\n set('pools', []),\n after(\n 0.4,\n cue('barrel_visible', { visible: false }),\n call('explode', BARREL, 1.3, RED),\n burst('add', 30, { p: above(0.8), r: 0.4, dir: UP, cone: 0.25, sp: [3, 5], drag: 1.2, life: [1, 1.6], size: [0.9, 1.4], grow: 1.4, c0: 0xffb050, c1: 0x3a0d05, g: -1, fi: 0.05 }),\n debris({ p: above(1), v: [0.6, 9, 0.3], sp: 0, size: [0.7, 0.05, 0.7], c: RED, spin: 8, life: 12 }),\n repeat(scaled(8), debris({ p: BARREL, dir: UP, cone: 1, sp: [3, 7], size: [0.25, 0.04, 0.18], c: ite(mod(INDEX, 2), RED, YELLOW), life: 12 })),\n repeat(\n POOLS,\n set('poolAngle', add(mul(INDEX, 2.1), rnd(0, 0.6))),\n append('pools', vadd(BARREL, [mul(cos(variable('poolAngle')), 1.4), 0.05, mul(sin(variable('poolAngle')), 1.4)])),\n ),\n ),\n ],\n tick: [\n when(and(variable('pools'), gt(TIME, 0.5), lt(TIME, 6)), [\n set('poolFade', ite(gt(TIME, 5), sub(1, sub(TIME, 5)), 1)),\n repeat(POOLS, call('fire_emit', concat('bp', INDEX), { $: 'at', a: [variable('pools'), INDEX] }, 0.45, mul(0.6, variable('poolFade')), { smoke: 6 })),\n fireLight(0, above(1), mul(2.6, variable('poolFade')), TIME),\n ]),\n when(and(gt(TIME, 0.6), lt(TIME, 8)), [\n emit('col', 'normal', 6, { p: above(1.5), r: 0.6, dir: UP, cone: 0.2, sp: [1.5, 2.5], v: [0.4, 0, 0], drag: 0.05, life: [4, 6], size: [1.2, 1.8], grow: 3, c0: 0x1a1817, c1: 0x0c0d10, a: 0.5, fi: 0.2 }),\n ]),\n when(ge(TIME, 6), [fireLightOff(0)]),\n ],\n}\n","import { add, and, eq, INDEX, ite, mod, mul, ne, param, PARAM, pick, rnd, round, variable } from '../dsl'\nimport { after, call, repeat, set, spawn, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst WEAPONS = ['glock', 'ak', 'r870', 'barrett']\nconst TIP = [-0.8, 1.38, 3.4 - 0.78] as const\nconst PORT = [-0.76, 1.4, 3.32] as const\nconst FORWARD = [0, 0, -1] as const\nconst NORMAL = [0, 0, 1] as const\nconst WALL_Z = -4.45\nconst SHOTS = [[0, 0.45, 0.9], [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0], [0]]\nconst SHOT_TIME = pick(pick(SHOTS, PARAM), INDEX)\nconst AK = 1\nconst SHOTGUN = 2\nconst BARRETT = 3\n\n/** W745-761: core, petals and smoke scaled by weapon (the Barrett also vents sideways and lifts dust), with the shot's casing and hit on the wall. */\nexport const MUZZLE_FLASH: EffectDef = {\n id: 'muzzle_flash',\n pageId: 'muzzle',\n team: 'weapons',\n params: { id: 'weapon', options: ['glock', 'ak', 'r870', 'barrett'], default: 1 },\n budget: { particles: 60, lights: 1 },\n loop: 2.4,\n view: { eye: [1.8, 1.8, 5.2], target: [-0.8, 1.3, 1.4] },\n timeline: [\n repeat(\n param(3, 7, 1, 1),\n after(\n add(0.2, SHOT_TIME),\n call('muzzle', pick(WEAPONS, PARAM), TIP, FORWARD),\n when(ne(PARAM, SHOTGUN), [call('casing', PORT, ite(eq(PARAM, BARRETT), '50', ''))]),\n set('target', [add(-0.8, rnd(-0.4, 0.4)), add(1.4, rnd(-0.3, 0.3)), WALL_Z]),\n when(\n eq(PARAM, SHOTGUN),\n [repeat(8, call('impact', 'concrete', [add(-0.8, rnd(-0.5, 0.5)), add(1.4, rnd(-0.4, 0.4)), WALL_Z], NORMAL, 0.35))],\n [call('impact', 'concrete', variable('target'), NORMAL, ite(eq(PARAM, BARRETT), 2, 1))],\n ),\n when(and(eq(PARAM, AK), eq(mod(round(mul(SHOT_TIME, 10)), 3), 0)), [spawn('streak', { p: TIP, dir: FORWARD, cone: 0, sp: 90, life: 0.09, len: 0.025, c0: 0xffa040, a: 1 })]),\n ),\n ),\n when(eq(PARAM, SHOTGUN), [after(0.75, call('casing', PORT, 'shell'))]),\n ],\n}\n","import { rnd, scaled } from '../dsl'\nimport { after, behavior, burst, call, cue, debris, repeat, screen } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst FACING = [0, 0, 1] as const\n\n/** W846-855 (`vidro`): two cracks open in the shop window, then the whole pane comes down as 60 to 80 shards that stay on the pavement. */\nexport const SHOPFRONT_GLASS: EffectDef = {\n id: 'shopfront_glass',\n pageId: 'vidro',\n team: 'destruction',\n budget: { debris: 80, decals: 1 },\n loop: 5,\n view: { eye: [-2.4, 1.8, 1.2], target: [-5, 1.4, -4] },\n timeline: [\n after(0.3, call('impact', 'glass', [-5.4, 2.2, -4.45], FACING), behavior('cracks', 'crack', { x: 97, y: 59, big: false })),\n after(1, call('impact', 'glass', [-4.5, 1.6, -4.45], FACING), behavior('cracks', 'crack', { x: 177, y: 108, big: true })),\n after(\n 1.8,\n cue('glass_pane_gone'),\n screen({ add: { shake: 0.08 } }),\n repeat(\n scaled(75),\n debris({ p: [-5, 1.9, -4.42], box: [1.25, 0.85, 0.02], v: [rnd(-0.4, 0.4), rnd(-0.5, 0.8), rnd(0.6, 2)], sp: 0, size: [rnd(0.05, 0.18), 0.006, rnd(0.05, 0.18)], c: 0xa8c8d8, spin: 14, life: 30, bounce: 0.15 }),\n ),\n burst('add', 30, { p: [-5, 1.9, -4.3], box: [1.2, 0.8, 0.05], size: [0.02, 0.05], life: [0.2, 0.6], c0: 0xffffff, a: 0.9, fi: 0.3 }),\n burst('normal', 20, { p: [-5, 1.3, -4.2], box: [1.2, 0.5, 0.1], sp: [0.2, 0.6], drag: 2, life: [0.8, 1.4], size: [0.12, 0.2], grow: 3, c0: 0xbfd6e2, a: 0.2 }),\n ),\n ],\n}\n","import type { Vec3Tuple } from '../workshop/types'\n\n/** Vector helpers written as three.js r128 does them, so a recipe's numbers match the page's. */\nexport function normalized(vector: Vec3Tuple): Vec3Tuple {\n const length = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]) || 1\n const inverse = 1 / length\n return [vector[0] * inverse, vector[1] * inverse, vector[2] * inverse]\n}\n\nexport function plus(vector: Vec3Tuple, x: number, y: number, z: number): Vec3Tuple {\n return [vector[0] + x, vector[1] + y, vector[2] + z]\n}\n\nexport function scaledBy(vector: Vec3Tuple, factor: number): Vec3Tuple {\n return [vector[0] * factor, vector[1] * factor, vector[2] * factor]\n}\n\nexport function plusScaled(vector: Vec3Tuple, direction: Vec3Tuple, factor: number): Vec3Tuple {\n return [vector[0] + direction[0] * factor, vector[1] + direction[1] * factor, vector[2] + direction[2] * factor]\n}\n\nexport function negated(vector: Vec3Tuple): Vec3Tuple {\n return [-vector[0], -vector[1], -vector[2]]\n}\n","import { add, and, div, DELTA, ge, INDEX, min, mul, not, rnd, scaled, sub, variable } from '../dsl'\nimport { after, behavior, burst, cue, debris, repeat, screen, set, when } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef, EffectStep } from '../types'\n\nconst FRAME_DUST = normalized([0, -0.3, 1])\nconst SPLINTER_DIRECTION = normalized([0, 0.3, 1])\nconst FALL_SECONDS = 0.4\n\n/** One blow on the door (`hit`, W779): the frame sheds dust and splinters; a heavy blow sheds twice as much. */\nconst blow = (heavy: boolean): EffectStep[] => [\n cue('door_shake', { strength: 0.25 }),\n burst('normal', heavy ? 24 : 12, { p: [2, 2.3, -4.38], box: [0.6, 0.05, 0.02], dir: FRAME_DUST, cone: 0.6, sp: [0.2, 0.8], drag: 1.5, life: [0.8, 1.4], size: [0.08, 0.16], grow: 3, c0: 0x7a7366, a: 0.45, g: 0.4 }),\n repeat(scaled(heavy ? 5 : 2), debris({ p: [add(2, rnd(-0.5, 0.5)), rnd(0.4, 2), -4.36], dir: SPLINTER_DIRECTION, cone: 0.6, sp: [1, 2.5], size: [0.015, 0.015, 0.1], c: 0x7a5530, life: 12 })),\n]\n\n/** W856-871 (`porta`): blows loosen dust from the frame, a plank cracks at 66 %, falls at 33 % and the door drops at the third stage. */\nexport const DOOR_STAGES: EffectDef = {\n id: 'door_stages',\n pageId: 'porta',\n team: 'destruction',\n budget: { particles: 120, debris: 6 },\n loop: 7.5,\n view: { eye: [4.2, 1.7, 1.4], target: [2, 1.1, -4] },\n setup: [behavior('walkers', 'place', { index: 0, x: 2, z: -5.4, facing: 0 })],\n timeline: [\n after(0.5, ...blow(false)),\n after(1.2, ...blow(false)),\n after(1.9, ...blow(true), cue('door_plank_crack', { stage: 1 })),\n after(2.7, ...blow(false)),\n after(\n 3.3,\n ...blow(true),\n cue('door_plank_fall', { stage: 2 }),\n repeat(3, debris({ p: [add(2, mul(sub(INDEX, 1), 0.4)), 1.1, -4.3], v: [rnd(-0.3, 0.3), 0.5, 1.2], sp: 0, size: [0.38, 0.68, 0.07], c: 0x5a3c22, spin: 4, life: 20, bounce: 0.15 })),\n ),\n after(4.4, set('fall', 0), set('falling', true), cue('door_fall', { stage: 3 })),\n after(4.6, behavior('walkers', 'walk', { index: 0, to: [2, 0, -2.2], speed: 0.9 })),\n ],\n tick: [\n when(variable('falling'), [\n set('fall', add(variable('fall'), DELTA)),\n set('progress', min(1, div(variable('fall'), FALL_SECONDS))),\n when(and(ge(variable('progress'), 1), not(variable('landed'))), [\n set('landed', true),\n screen({ add: { shake: 0.2 } }),\n burst('normal', 45, { p: [2, 0.15, -3.4], box: [0.7, 0.05, 1], dir: [0, 1, 0], cone: 1.4, sp: [0.6, 2], drag: 1.8, life: [1.5, 2.8], size: [0.25, 0.5], grow: 3, c0: 0x6a645a, a: 0.45 }),\n ]),\n ]),\n ],\n}\n","import { abs, add, and, cos, DELTA, gt, local, lt, mul, rand, rnd, scaled, sin, TIME, variable, vnorm } from '../dsl'\nimport { after, cue, debris, emit, repeat, screen, spawn, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst CENTRE = [-1.4, 3.35, -3.75] as const\n\n/** W872-886 (`desaba`): dust trickles from the awning for a second as a warning, then the slab drops in blocks and a cloud rolls along the street. */\nexport const BUILDING_COLLAPSE: EffectDef = {\n id: 'building_collapse',\n pageId: 'desaba',\n team: 'destruction',\n budget: { particles: 350, debris: 26 },\n loop: 8,\n view: { eye: [4, 2.4, 5], target: [-1.4, 1.4, -2.8] },\n setup: [cue('zombie_visible', { index: -1, visible: false })],\n timeline: [\n after(\n 1.5,\n cue('slab_gone'),\n screen({ add: { shake: 0.3 } }),\n repeat(6, debris({ p: CENTRE, box: [2, 0.05, 0.6], v: [rnd(-0.5, 0.5), rnd(-0.5, 0), rnd(0, 0.8)], sp: 0, size: [rnd(0.5, 1.1), 0.25, rnd(0.4, 0.8)], c: 0x6e6a63, spin: 3, life: 30, bounce: 0.1 })),\n repeat(scaled(20), debris({ p: CENTRE, box: [2.1, 0.1, 0.7], v: [rnd(-1, 1), rnd(-1, 1), rnd(0, 1.5)], sp: 0, size: [0.12, 0.1, 0.12], k: [0.5, 1.5], c: 0x5e5a54, life: 30 })),\n ),\n after(\n 2.15,\n screen({ add: { shake: 0.45 } }),\n repeat(\n scaled(70),\n spawn('normal', local('angle', rnd(-0.4, 3.5), {\n p: [add(-1.4, rnd(-2, 2)), 0.2, add(-3.4, rnd(-0.5, 0.5))],\n dir: vnorm([mul(cos(variable('angle')), 0.6), 0.25, abs(sin(variable('angle')))]),\n cone: 0.5, sp: [1, 3.2], drag: 1.1, life: [3, 6], size: [0.7, 1.2], grow: 3, c0: 0x7a7268, c1: 0x4a4640, a: 0.5, fi: 0.12,\n })),\n ),\n ),\n ],\n tick: [\n when(and(gt(TIME, 0.3), lt(TIME, 1.5)), [\n emit('trk', 'normal', 30, { p: [add(-1.4, rnd(-2, 2)), 3.2, -3.05], dir: [0, -1, 0], cone: 0.1, sp: [0.2, 0.5], life: [1, 1.6], size: [0.04, 0.08], grow: 2.5, c0: 0x8a8478, a: 0.5, g: 1 }),\n when(lt(rand(), mul(DELTA, 6)), [debris({ p: [add(-1.4, rnd(-2, 2)), 3.2, -3.05], sp: 0.2, size: [0.04, 0.04, 0.04], c: 0x6e6a63, life: 10 })]),\n ]),\n ],\n}\n","import { add, cos, INDEX, local, mul, rnd, scaled, sin, variable, vnorm } from '../dsl'\nimport { after, behavior, burst, call, cue, debris, repeat, screen, spawn } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef } from '../types'\n\nconst FIRING_FROM = [0.6, 1.3, 7] as const\nconst FIRING_DIRECTION = normalized([-2.8, -0.4, -4.4])\nconst WALL_CENTRE = [-2.2, 1, 2.6] as const\n\n/** W1039-1053 (`parede`): a burst of AK fire leaves holes that are stuck to the blocks, then the wall gives way and the holes fall with them. */\nexport const BLOCK_WALL: EffectDef = {\n id: 'block_wall',\n pageId: 'parede',\n team: 'destruction',\n budget: { particles: 300, debris: 16 },\n loop: 8,\n view: { eye: [0.4, 1.7, 6.6], target: [-2.2, 0.9, 2.4] },\n props: ['blocks'],\n setup: [cue('zombie_visible', { index: -1, visible: false }), cue('work_light', { intensity: 2.2 })],\n timeline: [\n repeat(\n 14,\n after(\n add(0.3, mul(INDEX, 0.11)),\n call('muzzle', 'ak', FIRING_FROM, FIRING_DIRECTION),\n behavior('blocks', 'hit', { x: rnd(-1.3, 1.3), y: rnd(0.4, 1.9), scale: 1 }),\n ),\n ),\n after(2.2, behavior('blocks', 'hit', { x: -0.2, y: 1.1, scale: 2.2 })),\n after(\n 3,\n screen({ add: { shake: 0.4 } }),\n behavior('blocks', 'collapse', { x: -0.2, y: 1.1 }),\n burst('normal', 30, { p: WALL_CENTRE, box: [1.4, 0.8, 0.2], sp: [0.3, 1.2], drag: 1.5, life: [1.5, 2.6], size: [0.4, 0.7], grow: 3, c0: 0x8a857a, c1: 0x4a4640, a: 0.45 }),\n repeat(scaled(16), debris({ p: WALL_CENTRE, box: [1.3, 0.8, 0.1], sp: [1, 3], size: [0.08, 0.07, 0.08], k: [0.5, 1.5], c: 0x6e6a63, life: 20 })),\n ),\n after(\n 3.6,\n screen({ add: { shake: 0.25 } }),\n repeat(\n scaled(60),\n spawn('normal', local('angle', rnd(0, 6.283), {\n p: [add(-2.2, rnd(-1.4, 1.4)), 0.2, add(2.4, rnd(-0.6, 0.4))],\n dir: vnorm([cos(variable('angle')), 0.3, sin(variable('angle'))]),\n cone: 0.4, sp: [0.8, 2.4], drag: 1.3, life: [2.5, 4.5], size: [0.6, 1], grow: 3, c0: 0x7a7268, c1: 0x4a4640, a: 0.45, fi: 0.12,\n })),\n ),\n ),\n ],\n}\n","import type { Vec3Tuple } from '../../workshop/types'\n\n/** The free spot of the Workshop's set where the wall, door, railing, fence, table and cylinders are put one at a time (W937, `SPOT`). */\nexport const SPOT: Vec3Tuple = [-2.2, 0, 2.6]\nexport const UP: Vec3Tuple = [0, 1, 0]\n","import { add, and, DELTA, div, floor, ge, INDEX, ite, min, mod, mul, not, pick, scaled, variable } from '../dsl'\nimport { after, behavior, burst, call, cue, debris, repeat, screen, set, when } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef } from '../types'\nimport { SPOT, UP } from './spot'\n\nconst HITS = [[0.4, 1.4], [0.75, 1.0], [0.3, 0.7], [0.6, 1.7], [0.5, 1.2]]\nconst HIT_X = HITS.map(([x]) => x)\nconst HIT_Y = HITS.map(([, y]) => y)\nconst SPARK_DIRECTION = normalized([0, 0.3, 1])\nconst CHIP_DIRECTION = normalized([0, 0.2, 1])\nconst HINGE_DIRECTION = normalized([1, 0.3, 1])\nconst SCRAPE_DIRECTION = normalized([0, 0.4, 1])\nconst FALL_SECONDS = 0.45\nconst SCRAPES = 5\n\n/** W960-985 (`portametal`): each blow dents the plate where it lands and throws sparks, paint chips and rust; the top hinge bursts and the door scrapes down. */\nexport const METAL_DOOR_EFFECT: EffectDef = {\n id: 'metal_door',\n pageId: 'portametal',\n team: 'destruction',\n budget: { particles: 160, debris: 20 },\n loop: 7.5,\n view: { eye: [0.6, 1.6, 6.2], target: [-2.2, 1.1, 2.6] },\n props: ['plate'],\n setup: [\n cue('zombie_visible', { index: -1, visible: false }),\n cue('work_light', { intensity: 2.2 }),\n behavior('walkers', 'place', { index: 3, x: SPOT[0] + 0.1, z: SPOT[2] - 0.7, facing: 0 }),\n ],\n timeline: [\n repeat(\n HITS.length,\n after(\n add(0.5, mul(INDEX, 0.75)),\n cue('zombie_swing', { index: 3, side: mod(INDEX, 2) }),\n after(\n add(0.5, mul(INDEX, 0.75), 0.15),\n behavior('plate', 'dent', { x: pick(HIT_X, INDEX), y: pick(HIT_Y, INDEX), radius: 0.42, depth: add(0.09, mul(INDEX, 0.025)) }),\n set('spot', [add(SPOT[0] - 0.56, pick(HIT_X, INDEX)), pick(HIT_Y, INDEX), SPOT[2] + 0.06]),\n call('spark_at', variable('spot'), SPARK_DIRECTION, 8),\n repeat(scaled(5), debris({ p: variable('spot'), dir: CHIP_DIRECTION, cone: 0.9, sp: [0.8, 2.2], size: [0.035, 0.004, 0.03], k: [0.6, 1.4], c: ite(mod(INDEX, 2), 0x51625a, 0x8a8f86), life: 20, spin: 16 })),\n call('rust_at', variable('spot'), 6),\n cue('door_dent', { level: min(3, add(1, floor(div(INDEX, 1.7)))) }),\n ),\n ),\n ),\n after(4.4, call('spark_at', [SPOT[0] - 0.6, 2.05, SPOT[2] + 0.05], HINGE_DIRECTION, 30, 1.2), screen({ add: { shake: 0.15 } }), set('fall', 0), set('falling', true), cue('door_hinge_burst')),\n after(5.2, behavior('walkers', 'walk', { index: 3, to: [SPOT[0], 0, SPOT[2] + 2.4], speed: 0.8 })),\n ],\n tick: [\n when(variable('falling'), [\n set('fall', add(variable('fall'), DELTA)),\n set('progress', min(1, div(variable('fall'), FALL_SECONDS))),\n when(and(ge(variable('progress'), 1), not(variable('landed'))), [\n set('landed', true),\n screen({ add: { shake: 0.3 } }),\n repeat(SCRAPES, call('spark_at', [add(SPOT[0] - 0.5, mul(INDEX, 0.27)), 0.03, SPOT[2] + 2.1], SCRAPE_DIRECTION, 8, 1)),\n burst('normal', 40, { p: [SPOT[0], 0.15, SPOT[2] + 1.2], box: [0.6, 0.05, 1], dir: UP, cone: 1.4, sp: [0.6, 2], drag: 1.8, life: [1.4, 2.4], size: [0.25, 0.5], grow: 3, c0: 0x6a645a, a: 0.45 }),\n ]),\n ]),\n ],\n}\n","import { add, and, DELTA, div, ge, gt, INDEX, lt, min, mul, not, pick, rnd, sub, TIME, variable } from '../dsl'\nimport { after, behavior, burst, call, cue, emit, repeat, screen, set, when } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef } from '../types'\nimport { SPOT, UP } from './spot'\n\n/** Where the four pushing bodies stand, relative to the railing (W987). */\nconst PUSHERS = [[-0.6, 0.45], [0.1, 0.5], [0.6, 0.42], [-0.1, 1]]\nconst BEND_LIMIT = 0.42\nconst BEND_STEP = 0.004\nconst FALL_SECONDS = 0.55\nconst WELD = normalized([0, 0.3, 1])\n\n/** W986-1003 (`grade`): the horde pushes, the bars bend and rust falls in a thread; the weld bursts in sparks twice and the railing topples. */\nexport const IRON_RAILING: EffectDef = {\n id: 'iron_railing',\n pageId: 'grade',\n team: 'destruction',\n budget: { particles: 200 },\n loop: 7.5,\n view: { eye: [1.4, 1.8, 6.4], target: [-2.2, 0.7, 2.4] },\n setup: [\n cue('zombie_visible', { index: -1, visible: false }),\n cue('work_light', { intensity: 2.2 }),\n set('bend', 0),\n ...PUSHERS.map(([x, dz], index) => behavior('walkers', 'place', { index, x: SPOT[0] + x!, z: SPOT[2] - dz!, facing: 0 })),\n ],\n timeline: [\n after(1.6, call('spark_at', [SPOT[0], 0.75, SPOT[2] + 0.05], WELD, 20, 1), screen({ add: { shake: 0.08 } }), cue('weld_burst')),\n after(3.2, call('spark_at', [SPOT[0] + 0.02, 1.2, SPOT[2] + 0.1], WELD, 20, 1), screen({ add: { shake: 0.1 } })),\n after(\n 4.3,\n set('fall', 0),\n set('falling', true),\n call('spark_at', [SPOT[0] - 1.5, 0.25, SPOT[2]], normalized([-0.3, 0.5, 1]), 14, 1),\n call('spark_at', [SPOT[0] + 1.5, 0.25, SPOT[2]], normalized([0.3, 0.5, 1]), 14, 1),\n cue('railing_gave_way'),\n ),\n after(\n 4.9,\n repeat(PUSHERS.length, behavior('walkers', 'walk', { index: INDEX, to: [add(SPOT[0], pick(PUSHERS.map(([x]) => x), INDEX)), 0, add(SPOT[2], 2.6, mul(INDEX, 0.3))], speed: 0.7 })),\n ),\n ],\n tick: [\n when(lt(TIME, 4.3), [\n set('target', min(BEND_LIMIT, mul(TIME, 0.1))),\n when(gt(sub(variable('target'), variable('bend')), BEND_STEP), [set('bend', variable('target')), cue('railing_bend', { amount: variable('bend') })]),\n emit('rust', 'normal', 10, { p: [add(SPOT[0], rnd(-0.6, 0.6)), rnd(0.3, 1.2), add(SPOT[2], mul(variable('bend'), 0.8))], sp: [0, 0.2], life: [0.8, 1.2], size: [0.015, 0.03], c0: 0x7a4a2a, a: 0.9, g: 6, fi: 0, gnd: 1 }),\n ]),\n when(variable('falling'), [\n set('fall', add(variable('fall'), DELTA)),\n set('progress', min(1, div(variable('fall'), FALL_SECONDS))),\n when(and(ge(variable('progress'), 1), not(variable('landed'))), [\n set('landed', true),\n screen({ add: { shake: 0.25 } }),\n repeat(4, call('spark_at', [add(SPOT[0] - 1.2, mul(INDEX, 0.8)), 0.04, SPOT[2] + 1.2], normalized([0, 0.5, 1]), 6, 1)),\n burst('normal', 30, { p: [SPOT[0], 0.12, SPOT[2] + 0.8], box: [1.4, 0.05, 0.5], dir: UP, cone: 1.4, sp: [0.5, 1.6], drag: 1.8, life: [1.2, 2.2], size: [0.2, 0.4], grow: 3, c0: 0x6a645a, a: 0.4 }),\n ]),\n ]),\n ],\n}\n","import { add, and, DELTA, div, ge, INDEX, min, mul, not, pick, rnd, scaled, sub, variable, vadd, mod } from '../dsl'\nimport { after, behavior, burst, cue, debris, repeat, screen, set, when } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef } from '../types'\nimport { SPOT, UP } from './spot'\n\n/** The order the planks break in and where each stands along the fence (W1005-1006). */\nconst ORDER = [3, 4, 8, 5, 9, 2, 7]\nconst PLANK_X = Array.from({ length: 12 }, (_, index) => -1.43 + index * 0.26)\nconst BODIES = [[-0.8, 0.6], [0.5, 0.7], [0, 1.2]]\nconst SPLINTER_DIRECTION = normalized([0, 0.4, 1])\nconst FALL_SECONDS = 0.5\nconst PLANK_HEIGHT = 1.3\n\n/** W1004-1020 (`cerca`): the planks snap one by one at a random height, the stump stays on the rail and the rest flies; the section finally tips over. */\nexport const WOODEN_FENCE: EffectDef = {\n id: 'wooden_fence',\n pageId: 'cerca',\n team: 'destruction',\n budget: { particles: 150, debris: 12 },\n loop: 7.5,\n view: { eye: [1, 1.6, 6], target: [-2.2, 0.7, 2.4] },\n setup: [\n cue('zombie_visible', { index: -1, visible: false }),\n cue('work_light', { intensity: 2.2 }),\n ...BODIES.map(([x, dz], index) => behavior('walkers', 'place', { index, x: SPOT[0] + x!, z: SPOT[2] - dz!, facing: 0 })),\n ],\n timeline: [\n repeat(\n ORDER.length,\n after(\n add(0.4, mul(INDEX, 0.5)),\n set('cut', rnd(0.32, 0.6)),\n set('top', sub(PLANK_HEIGHT, variable('cut'))),\n set('break', [add(SPOT[0], pick(PLANK_X, pick(ORDER, INDEX))), variable('cut'), SPOT[2]]),\n cue('zombie_swing', { index: mod(INDEX, 3), side: mod(INDEX, 2) }),\n cue('plank_snap', { plank: pick(ORDER, INDEX), cut: variable('cut') }),\n debris({ p: vadd(variable('break'), [0, div(variable('top'), 2), 0]), v: [rnd(-0.8, 0.8), rnd(1.5, 3), rnd(1.5, 3)], sp: 0, size: [0.17, variable('top'), 0.035], c: 0x8a6a44, spin: 7, life: 20, bounce: 0.2 }),\n repeat(scaled(6), debris({ p: variable('break'), dir: SPLINTER_DIRECTION, cone: 0.9, sp: [1, 3], size: [0.012, 0.012, 0.1], k: [0.6, 1.4], c: 0xb08a5a, life: 20 })),\n burst('normal', 8, { p: variable('break'), dir: [0, 0, 1], cone: 0.8, sp: [0.4, 1.4], drag: 2, life: [0.8, 1.4], size: [0.1, 0.18], grow: 3, c0: 0x7a6048, a: 0.4 }),\n ),\n ),\n after(\n 4.2,\n set('fall', 0),\n set('falling', true),\n repeat(scaled(10), debris({ p: [add(SPOT[0], rnd(-1.4, 1.4)), 1, SPOT[2]], dir: SPLINTER_DIRECTION, cone: 0.9, sp: [1, 3], size: [0.012, 0.012, 0.1], c: 0xb08a5a, life: 20 })),\n cue('fence_rail_cracked'),\n ),\n after(4.8, repeat(BODIES.length, behavior('walkers', 'walk', { index: INDEX, to: [add(SPOT[0], pick(BODIES.map(([x]) => x), INDEX)), 0, SPOT[2] + 2.5], speed: 0.75 }))),\n ],\n tick: [\n when(variable('falling'), [\n set('fall', add(variable('fall'), DELTA)),\n set('progress', min(1, div(variable('fall'), FALL_SECONDS))),\n when(and(ge(variable('progress'), 1), not(variable('landed'))), [\n set('landed', true),\n screen({ add: { shake: 0.2 } }),\n burst('normal', 35, { p: [SPOT[0], 0.12, SPOT[2] + 0.8], box: [1.5, 0.05, 0.5], dir: UP, cone: 1.4, sp: [0.5, 1.6], drag: 1.8, life: [1.2, 2.2], size: [0.25, 0.45], grow: 3, c0: 0x6a5a48, a: 0.4 }),\n ]),\n ]),\n ],\n}\n","import { add, INDEX, mul, rnd, variable, vadd } from '../dsl'\nimport { after, call, cue, debris, burst, repeat, screen, set } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef } from '../types'\nimport { SPOT, UP } from './spot'\n\nconst at = (x: number, y: number, z: number): readonly [number, number, number] => [SPOT[0] + x, SPOT[1] + y, SPOT[2] + z]\n/**\n * Where each part of the table is in the world. The page asks its scene graph for it (`tbW`), and the capture does not render, so the table's tilt never\n * reaches the world matrix it reads: the parts are always where the table stands level (W1009, recorded at 2.2 s, 3.8 s and 5.3 s).\n */\nconst FRONT_LEG = at(-0.52, 0.2, 0.32)\nconst TOP_CORNER = at(0.3, 0.78, 0.2)\nconst TOP_PIECE = at(-0.3, 0.78, -0.2)\nconst FAR_CORNER = at(0.3, 0.78, -0.2)\nconst HOLE_OBJECT_ID_DRAWS = 12\nconst HOLES = 9\nconst GLOCK_FROM = [-0.4, 1, 6] as const\nconst SHOTGUN_FROM = [0.2, 1.1, 4.6] as const\nconst toward = (target: readonly [number, number, number], from: readonly [number, number, number]) => normalized([target[0] - from[0], target[1] - from[1], target[2] - from[2]])\n\n/** W1008-1020 (`pontos`): a 9 mm in the leg, a machete on a corner, a bat on the top and a 12-gauge up close: each breaks only the part it hit. */\nexport const HIT_POINT_BREAKAGE: EffectDef = {\n id: 'hit_point_breakage',\n pageId: 'pontos',\n team: 'destruction',\n budget: { particles: 120, debris: 20 },\n loop: 7.5,\n view: { eye: [0.2, 1.6, 5.4], target: [-2.2, 0.6, 2.6] },\n setup: [cue('zombie_visible', { index: -1, visible: false }), cue('work_light', { intensity: 2.2 })],\n timeline: [\n after(\n 0.6,\n call('muzzle', 'glock', GLOCK_FROM, toward(FRONT_LEG, GLOCK_FROM)),\n call('chips', FRONT_LEG, normalized([0, 0.2, 1]), 6),\n cue('table_part_detached', { part: 'front_leg' }),\n debris({ p: FRONT_LEG, v: [0.4, 0.6, 1.4], sp: 0, size: [0.07, 0.4, 0.07], c: 0x7a5530, spin: 6, life: 20, bounce: 0.25 }),\n cue('table_tilt'),\n ),\n after(\n 2.2,\n repeat(8, call('chips', vadd(TOP_CORNER, [add(-0.3, mul(INDEX, 0.08)), 0.03, 0]), normalized([0, 0.6, 1]), 1, 0.4)),\n cue('table_part_detached', { part: 'top_corner' }),\n debris({ p: TOP_CORNER, v: [1.2, 1.5, 1], sp: 0, size: [0.6, 0.06, 0.4], c: 0x7a5530, spin: 6, life: 20, bounce: 0.25 }),\n ),\n after(\n 3.8,\n screen({ add: { shake: 0.12 } }),\n call('chips', TOP_PIECE, UP, 10, 1.3),\n cue('table_part_gone', { part: 'top_piece' }),\n repeat(4, debris({ p: [add(TOP_PIECE[0], rnd(-0.2, 0.2)), TOP_PIECE[1], add(TOP_PIECE[2], rnd(-0.1, 0.1))], dir: UP, cone: 1.2, sp: [1, 2.5], size: [rnd(0.15, 0.3), 0.06, rnd(0.12, 0.25)], c: 0x6b4a2a, spin: 8, life: 20, bounce: 0.2 })),\n burst('normal', 20, { p: TOP_PIECE, r: 0.2, sp: [0.3, 1], drag: 2, life: [1, 1.8], size: [0.2, 0.35], grow: 3, c0: 0x7a6048, a: 0.4 }),\n ),\n after(\n 5.3,\n call('muzzle', 'r870', SHOTGUN_FROM, toward(FAR_CORNER, SHOTGUN_FROM)),\n repeat(HOLES, set('holeX', rnd(-0.15, 0.15)), set('holeZ', rnd(-0.1, 0.1)), cue('table_hole', { x: variable('holeX'), z: variable('holeZ') }, HOLE_OBJECT_ID_DRAWS)),\n call('chips', FAR_CORNER, UP, 10, 1),\n ),\n ],\n}\n","import { abs, clamp, DELTA, div, eq, gt, lt, min, mul, not, PARAM, query, sub, TIME, variable, vx, vz } from '../dsl'\nimport { after, behavior, call, cue, emit, set, screen, when } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef } from '../types'\nimport { SPOT, UP } from './spot'\n\nconst OXYGEN = 1\nconst PUNCTURE_AT = 0.6\nconst JET_SECONDS = 3.4\nconst CLOUD_UNTIL = 7\nconst SHOT_FROM = [0.4, 1.2, 6.4] as const\nconst SHOT_DIRECTION = normalized([-0.5, -0.15, -1])\nconst SCRAPE_DIRECTION = normalized([1, 0.4, 0])\nconst jetStrength = variable('jet')\nconst isOxygen = eq(PARAM, OXYGEN)\n\n/** W1021-1038 (`pressao`): a shot into an extinguisher (a white cloud and a spin) or into an oxygen bottle (the valve breaks and it flies off as a rocket). */\nexport const PRESSURE_CYLINDERS: EffectDef = {\n id: 'pressure_cylinders',\n pageId: 'pressao',\n team: 'destruction',\n params: { id: 'cylinder', options: ['extinguisher', 'oxygen'], default: 0 },\n budget: { particles: 250 },\n loop: 7,\n view: { eye: [1.6, 1.5, 6.4], target: [-1.6, 0.4, 2.4] },\n props: ['cylinder'],\n setup: [cue('zombie_visible', { index: -1, visible: false }), cue('work_light', { intensity: 2.2 }), behavior('cylinder', 'choose', { oxygen: isOxygen }), cue('cylinder_visible', { oxygen: isOxygen })],\n timeline: [\n after(\n PUNCTURE_AT,\n call('muzzle', 'glock', SHOT_FROM, SHOT_DIRECTION),\n screen({ add: { shake: 0.1 } }),\n behavior('cylinder', 'puncture'),\n when(isOxygen, [call('spark_at', [SPOT[0], 1.3, SPOT[2]], UP, 20, 1.2)]),\n ),\n ],\n tick: [\n when(query('cylinder', 'live'), [\n set('jet', clamp(sub(1, div(sub(TIME, PUNCTURE_AT), JET_SECONDS)), 0, 1)),\n behavior('cylinder', 'step', { dt: DELTA, t: TIME }),\n when(not(isOxygen), [\n when(gt(jetStrength, 0), [\n emit('jet', 'normal', 60, { p: query('cylinder', 'tip'), dir: query('cylinder', 'direction'), cone: 0.3, sp: [3, 6], drag: 2.5, life: [2, 3.5], size: [0.15, 0.3], grow: 6, c0: 0xeeeeea, c1: 0xbfc3c8, a: 0.45, g: -0.05, fi: 0.05 }),\n ]),\n when(lt(TIME, CLOUD_UNTIL), [\n emit('cloud', 'normal', 4, { p: [SPOT[0], 0.6, SPOT[2]], r: 2.5, flat: true, sp: [0.05, 0.2], life: [3, 5], size: [1.5, 2.5], grow: 1.3, c0: 0xe2e2de, a: mul(0.18, min(1, sub(TIME, PUNCTURE_AT))), fi: 0.3 }),\n ]),\n ], [\n when(gt(jetStrength, 0), [\n emit('jet', 'normal', 50, { p: query('cylinder', 'back'), dir: [1, 0, 0], cone: 0.25, sp: [2, 4], drag: 3, life: [0.5, 0.9], size: [0.1, 0.2], grow: 4, c0: 0xdde6ee, a: 0.2 }),\n ]),\n when(gt(abs(query('cylinder', 'speed_x')), 0.5), [\n emit('scr', 'streak', 120, { p: [vx(query('cylinder', 'position')), 0.03, vz(query('cylinder', 'position'))], dir: SCRAPE_DIRECTION, cone: 0.6, sp: [2, 6], v: [mul(query('cylinder', 'speed_x'), 0.5), 0, 0], life: [0.2, 0.45], len: 0.02, c0: 0xfff0c0, c1: 0xff5010, g: 9.8, gnd: 2 }),\n ]),\n ]),\n ]),\n ],\n}\n","import { add, concat, eq, INDEX, pick, TIME, vadd } from '../dsl'\nimport { call, cue, fireLight, repeat, when } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef } from '../types'\n\n/** The two burning windows of the shop front (W73). */\nconst WINDOWS = [[4.6, 5, -4.48], [-1.6, 5, -4.48]]\n\n/** W1054-1068 (`incendio`): fire out of the top of each window, licking up the facade, embers and smoke carried by the wind, and a light that paints the street. */\nexport const BUILDING_FIRE: EffectDef = {\n id: 'building_fire',\n pageId: 'incendio',\n team: 'world',\n budget: { particles: 600, lights: 2 },\n loop: 0,\n view: { eye: [3, 2.4, 6.5], target: [1.5, 4, -4] },\n setup: [cue('zombie_visible', { index: -1, visible: false })],\n timeline: [],\n tick: [\n repeat(\n WINDOWS.length,\n call('fire_emit', concat('w', INDEX), vadd(pick(WINDOWS, INDEX), [0, 0.55, 0.12]), 0.55, 1.1, { dir: normalized([0, 1, 0.35]), wind: [0.9, 0, 0], smoke: 22, smokeAlpha: 0.5 }),\n when(eq(INDEX, 0), [fireLight(0, vadd(pick(WINDOWS, INDEX), [0, 0, 1.4]), 3, add(TIME, INDEX))], [fireLight(1, vadd(pick(WINDOWS, INDEX), [0, 0, 1.4]), 3, add(TIME, INDEX))]),\n ),\n ],\n}\n","import { INDEX, pick, rnd, variable } from '../dsl'\nimport { behavior, cue, emit, repeat, set } from '../steps'\nimport type { EffectDef } from '../types'\n\n/** Where the three zombies stand in the fog (W1084). */\nconst SPOTS_X = [-3, 1, 5]\nconst SPOTS_Z = [-1, -2.4, 0.4]\n\n/**\n * W1073-1080 (the page's `neblina`): a low layer of mist that drifts slowly, fine ash falling in a spiral and the lamp's cone of light, with three\n * zombies standing in the fog (each turned at random). The fog density (0.075) and the cone are cues for whoever owns the scene.\n */\nexport const FOG_AND_ASH: EffectDef = {\n id: 'fog_and_ash',\n pageId: 'neblina',\n team: 'world',\n budget: { particles: 120, lights: 1 },\n loop: 0,\n view: { eye: [3, 1.8, 7.5], target: [-3, 1.2, -1] },\n setup: [\n cue('fog', { density: 0.075 }),\n cue('lamp_cone', { visible: true }),\n cue('zombie_visible', { index: -1, visible: false }),\n repeat(\n SPOTS_X.length,\n set('turn', rnd(-0.6, 0.6)),\n behavior('walkers', 'place', { index: INDEX, x: pick(SPOTS_X, INDEX), z: pick(SPOTS_Z, INDEX), facing: variable('turn') }),\n ),\n ],\n timeline: [],\n tick: [\n emit('fog', 'normal', 5, { p: [rnd(-14, 10), rnd(0.2, 1.1), rnd(-3, 8)], v: [rnd(0.15, 0.35), 0, rnd(-0.05, 0.05)], sp: 0, life: [9, 13], size: [3, 5], grow: 1.4, c0: 0x7d8aa0, a: 0.13, fi: 0.35 }),\n emit('ash', 'normal', 14, { p: [rnd(-10, 8), rnd(4, 6), rnd(-3, 7)], v: [rnd(0.1, 0.4), -0.35, rnd(-0.1, 0.1)], sp: [0.05, 0.15], life: [8, 12], size: [0.025, 0.04], c0: 0xa09a90, a: 0.75, fi: 0.1, gnd: 1 }),\n ],\n}\n","import type { WorkshopEngine } from './engine'\nimport type { Vec3Tuple } from './types'\n\n/** The most drops the field keeps (`RAINMAX`, W312). */\nexport const RAIN_CAPACITY = 4000\n/** Metres per second a drop falls (W322: `15 * dt`). */\nconst FALL_SPEED = 15\n/** How far the wind slants a drop per metre it falls (W322: `.07`), and the slant of the streak itself (W321: `.04`). */\nconst WIND_PER_FALL = 0.07\nconst STREAK_SLANT = 0.04\nconst STREAK_LENGTH = 0.6\n/** Where a drop starts, in a box of the field's own: x -15..15, z -6..16, height 0..16 at first and 14..17 when it comes back (W321, W328). */\nconst BOX_X: readonly [number, number] = [-15, 15]\nconst BOX_Z: readonly [number, number] = [-6, 16]\nconst START_HEIGHT: readonly [number, number] = [0, 16]\nconst REENTRY_HEIGHT: readonly [number, number] = [14, 17]\n/** A drop that lands makes a splash 35% of the time, within `12 / (1 + quality)` metres of the camera (W324). */\nconst SPLASH_CHANCE = 0.35\nconst SPLASH_RANGE = 12\nconst SPRITE_CHANCE = 0.5\nconst UP: Vec3Tuple = [0, 1, 0]\n\n/** An ellipse of the ground a puddle covers: centre x, centre z and the two half-axes (W332 `PUD`). */\nexport type Puddle = readonly [x: number, z: number, radiusX: number, radiusZ: number]\n\n/** The Workshop's four puddles (W332): the rings of the rain show only in them. */\nexport const WORKSHOP_PUDDLES: readonly Puddle[] = [\n [-1.5, 2.2, 1.6, 0.9],\n [3.0, 3.6, 1.1, 0.7],\n [-6, 3.2, 1.3, 0.8],\n [1.4, -2.2, 0.9, 0.5],\n]\n\nexport const inPuddle = (puddles: readonly Puddle[], x: number, z: number): boolean => puddles.some((p) => ((x - p[0]) / p[2]) ** 2 + ((z - p[1]) / p[3]) ** 2 < 1)\n\n/**\n * The Workshop's rain (W313-331): up to 4000 streaks (two points each, in a Float32Array as the page keeps them) that fall in a box, slanted by the\n * wind. A streak that reaches the ground may leave a splash (a sprite of the `normal` pool, and a ring where there is a puddle) when it is near the\n * camera, and comes back at the top. It draws its random numbers in the page's order, so the weather and the Workshop see the same rain.\n */\nexport class RainField {\n /** Two points per streak: x, y, z of the top and x, y, z of the bottom. */\n readonly positions = new Float32Array(RAIN_CAPACITY * 6)\n /** Streaks alive. */\n count = 0\n /** The slant (the wind along x in the Workshop's units). */\n wind = 1\n on = false\n /** Where the box is centred (the Workshop leaves it at the origin; a game follows the camera). */\n centerX = 0\n centerZ = 0\n puddles: readonly Puddle[] = WORKSHOP_PUDDLES\n /** Replaces the puddle test (a game knows its own wet spots). */\n puddleAt: ((x: number, z: number) => boolean) | undefined\n\n constructor(private readonly engine: WorkshopEngine) {}\n\n /** `initRain` (W321): `count` streaks (thinned by the quality), each drawn at a random place in the box. */\n init(count: number, wind: number): void {\n const { random } = this.engine\n this.count = Math.min(RAIN_CAPACITY, Math.round(count * this.engine.context.quality.mult))\n this.wind = wind\n this.on = true\n for (let index = 0; index < this.count; index++) {\n const offset = index * 6\n const x = random.between(BOX_X[0], BOX_X[1])\n const y = random.between(START_HEIGHT[0], START_HEIGHT[1])\n const z = random.between(BOX_Z[0], BOX_Z[1])\n this.set(offset, x, y, z)\n }\n }\n\n /**\n * What a game does with the field every frame: the box follows the camera (`centerX`, `centerZ`), the streaks thin or thicken to `count` (a new streak\n * starts at a random place of the box, as `init` draws it) and the wind slants them. The golden never calls it.\n */\n drive(count: number, wind: number, centerX: number, centerZ: number): void {\n const { random } = this.engine\n const target = Math.max(0, Math.min(RAIN_CAPACITY, Math.round(count * this.engine.context.quality.mult)))\n this.wind = wind\n this.centerX = centerX\n this.centerZ = centerZ\n this.on = target > 0\n for (let index = this.count; index < target; index++) {\n this.set(index * 6, random.between(BOX_X[0], BOX_X[1]) + centerX, random.between(START_HEIGHT[0], START_HEIGHT[1]), random.between(BOX_Z[0], BOX_Z[1]) + centerZ)\n }\n this.count = target\n }\n\n /** `updateRain` (W322-329). `camera` is where the eye is; `splash` lets the drops that land leave a splash. */\n update(dt: number, splash: boolean, camera: Vec3Tuple): void {\n if (!this.on) return\n const { random, context } = this.engine\n const positions = this.positions\n const fall = FALL_SPEED * dt\n const slant = this.wind * fall * WIND_PER_FALL\n const reach = SPLASH_RANGE / (1 + context.quality.level)\n for (let index = 0; index < this.count; index++) {\n const o = index * 6\n positions[o]! += slant\n positions[o + 3]! += slant\n positions[o + 1]! -= fall\n positions[o + 4]! -= fall\n const ground = context.groundAt(positions[o]!, positions[o + 2]!)\n if (positions[o + 4]! < ground) {\n const x = positions[o]!\n const z = positions[o + 2]!\n if (splash && random.next() < SPLASH_CHANCE) {\n const distance = Math.hypot(x - camera[0], z - camera[2])\n if (distance < reach) {\n if (random.next() < SPRITE_CHANCE) {\n this.engine.spawn('normal', { p: [x, ground + 0.01, z], dir: UP, cone: 0.6, sp: [0.6, 1.3], life: [0.18, 0.3], size: 0.035, c0: 0xb8c8dc, a: 0.6, g: 9.8, fi: 0 })\n }\n if (this.puddleAt ? this.puddleAt(x, z) : inPuddle(this.puddles, x, z)) {\n this.engine.ring([[x, ground + 0.012, z], 0.01, random.between(0.12, 0.25), random.between(0.4, 0.6), 0x6a7a90])\n }\n }\n }\n const nx = random.between(BOX_X[0], BOX_X[1]) + this.centerX\n const ny = random.between(REENTRY_HEIGHT[0], REENTRY_HEIGHT[1])\n const nz = random.between(BOX_Z[0], BOX_Z[1]) + this.centerZ\n this.set(o, nx, ny, nz)\n }\n }\n }\n\n /** `rain.visible = false; RN.on = false` (W440): the field stops; the streaks stay where they were. */\n clear(): void {\n this.on = false\n }\n\n private set(offset: number, x: number, y: number, z: number): void {\n const positions = this.positions\n positions[offset] = x\n positions[offset + 1] = y\n positions[offset + 2] = z\n positions[offset + 3] = x + this.wind * STREAK_SLANT\n positions[offset + 4] = y - STREAK_LENGTH\n positions[offset + 5] = z\n }\n}\n","import { add, DELTA, eq, ge, gt, ite, lt, mul, PARAM, rnd, TIME, variable } from '../dsl'\nimport { behavior, cue, decal, emit, screen, set, when } from '../steps'\nimport type { EffectDef } from '../types'\nimport { WORKSHOP_PUDDLES } from '../../workshop/rain'\n\n/** How many streaks, and how much the wind slants them, for the garoa, the chuva and the temporal (W1070). */\nconst DROPS = [900, 2200, 4000]\nconst SLANT = [0.3, 1.2, 4.5]\nconst STORM = 2\n/** The double flash (W1072): at full for 0.08 s, 0.2 for 0.08 s, 0.8 for 0.1 s, then dark; it ends at 0.4 s. */\nconst FLASH_STEPS = ite(lt(variable('flash'), 0.08), 1, ite(lt(variable('flash'), 0.16), 0.2, ite(lt(variable('flash'), 0.26), 0.8, 0)))\n\n/**\n * W1063-1072 (the page's `chuva`): up to 4000 streaks in a box by the camera, slanted by the wind; splashes and rings only within 12 m and only on the\n * four dark puddles; in the storm, sheets of water pass and a double flash lights the scene for 0.2 s every 3.5 to 6 s. The flash is a cue (`lightning`) for\n * whoever owns the lights; the screen goes white by itself.\n */\nexport const RAIN_AND_PUDDLES: EffectDef = {\n id: 'rain_and_puddles',\n pageId: 'chuva',\n team: 'world',\n params: { id: 'intensity', options: ['drizzle', 'rain', 'storm'], default: 1 },\n budget: { particles: 4000, decals: 4 },\n loop: 0,\n view: { eye: [2, 2, 7], target: [-1, 0.6, 1] },\n setup: [\n cue('lamp_cone', { visible: true }),\n ...WORKSHOP_PUDDLES.map(([x, z, radiusX, radiusZ]) => decal(x, 0, z, radiusX * 2, 0x0c1016, { grow: 0, life: 1e6, ar: radiusZ / radiusX })),\n ],\n timeline: [\n behavior('rain', 'init', { count: ite(eq(PARAM, 0), DROPS[0], ite(eq(PARAM, 1), DROPS[1], DROPS[2])), wind: ite(eq(PARAM, 0), SLANT[0], ite(eq(PARAM, 1), SLANT[1], SLANT[2])) }),\n set('nextStrike', rnd(2, 4)),\n set('flash', -1),\n ],\n tick: [\n behavior('rain', 'update', { dt: DELTA, splash: gt(PARAM, 0) }),\n when(eq(PARAM, STORM), [\n when(gt(TIME, variable('nextStrike')), [set('nextStrike', add(TIME, rnd(3.5, 6))), set('flash', 0)]),\n when(ge(variable('flash'), 0), [\n set('flash', add(variable('flash'), DELTA)),\n set('light', FLASH_STEPS),\n cue('lightning', { intensity: variable('light') }),\n screen({ max: { white: mul(variable('light'), 0.12) } }),\n when(gt(variable('flash'), 0.4), [set('flash', -1), cue('lightning', { intensity: 0 })]),\n ]),\n emit('sheet', 'normal', 2.5, { p: [-12, 2.5, rnd(-1, 8)], box: [0, 2, 0], v: [7, -0.5, 0], sp: 0, life: [3, 4], size: [3, 5], grow: 1.3, c0: 0x8a9ab0, a: 0.06, fi: 0.3 }),\n ]),\n ],\n}\n","import { localToWorld } from '../props'\nimport type { Vec3Tuple } from '../../workshop/types'\nimport { normalized } from '../vector-math'\n\n/** The car of the Workshop's set as the page leaves it between effects (`CAR0`, W104): at (5.6, 0, 0.8), turned -0.4 rad. */\nexport const CAR_ORIGIN: Vec3Tuple = [5.6, 0, 0.8]\nexport const CAR_ROTATION: Vec3Tuple = [0, -0.4, 0]\n/** A point of the car's own space in the world (`carW`). */\nexport const carPoint = (x: number, y: number, z: number): Vec3Tuple => localToWorld(CAR_ORIGIN, CAR_ROTATION, [x, y, z])\nexport const carDirection = (from: Vec3Tuple, to: Vec3Tuple): Vec3Tuple => normalized([to[0] - from[0], to[1] - from[1], to[2] - from[2]])\n","import { add, cos, DELTA, INDEX, local, mul, not, pick, query, rand, rnd, sin, sqrt, variable, vadd, vscale, vwith, vy } from '../dsl'\nimport { behavior, cue, emit, repeat, set, when } from '../steps'\nimport type { EffectDef } from '../types'\nimport { carDirection, carPoint } from './car'\n\n/** Where the beam starts (between the two lamps, W1176) and which way it points: the car's own +z. */\nconst BEAM_FROM = carPoint(0, 0.74, 2.15)\nconst BEAM_DIRECTION = carDirection(BEAM_FROM, carPoint(0, 0.74, 3.15))\nconst BEAM_SIDE = [BEAM_DIRECTION[2], 0, -BEAM_DIRECTION[0]] as const\n/** The six zombies that come into the light walk toward a point 2.6 m in front of the car (W1168). */\nconst WALKERS = 6\nconst WALK_SPEED = 0.7\nconst DROPS = 1800\nconst SLANT = 0.8\n\nconst startWalking = [\n set('spot', query('car', 'point', rnd(-2, 2), 0, rnd(8, 18))),\n set('goal', query('car', 'point', rnd(-0.6, 0.6), 0, 2.6)),\n behavior('walkers', 'place', { index: INDEX, x: pick(variable('spot'), 0), z: pick(variable('spot'), 2), facing: 0 }),\n behavior('walkers', 'walk', { index: INDEX, to: variable('goal'), speed: WALK_SPEED }),\n]\n\n/**\n * W1161-1170 (the page's `farol`): two beams in the air, rain that glows only inside the cone (up to 600 lit drops a second, thrown into the beam and\n * falling), and zombies that walk into the light. The lamps, the cones and the fog (0.05) are cues for whoever owns the scene.\n */\nexport const HEADLIGHTS_IN_RAIN: EffectDef = {\n id: 'headlights_in_rain',\n pageId: 'farol',\n team: 'vehicles',\n budget: { particles: 600, lights: 2 },\n loop: 0,\n view: { eye: [-2.5, 2, 5.5], target: [4, 1, 6] },\n setup: [\n cue('fog', { density: 0.05 }),\n cue('headlights', { on: true, intensity: 2.4, color: 0xfff1d0 }),\n behavior('rain', 'init', { count: DROPS, wind: SLANT }),\n repeat(WALKERS, ...startWalking),\n ],\n timeline: [],\n tick: [\n behavior('rain', 'update', { dt: DELTA, splash: true }),\n emit(\n 'lit',\n 'streak',\n 600,\n local(\n 'depth',\n rnd(1, 13),\n local(\n 'spread',\n mul(variable('depth'), 0.33, sqrt(rand())),\n local(\n 'turn',\n rnd(0, 6.283),\n local('base', vadd(BEAM_FROM, vscale(BEAM_DIRECTION, variable('depth')), vscale(BEAM_SIDE, mul(cos(variable('turn')), variable('spread')))), {\n p: vwith(variable('base'), 1, add(vy(variable('base')), mul(sin(variable('turn')), variable('spread'), 0.6))),\n v: [0.8, -14, 0],\n sp: 0,\n life: 0.06,\n len: 0.035,\n c0: 0xcfdcf0,\n a: 0.9,\n }),\n ),\n ),\n ),\n ),\n repeat(WALKERS, when(not(query('walkers', 'is_walking', INDEX)), startWalking)),\n ],\n}\n","import { add, DELTA, lt, mul, rand, rnd, sub, TIME, variable, vx, vz } from '../dsl'\nimport { cue, decal, emit, fireLight, set, when } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef } from '../types'\n\nconst YAW = -Math.PI / 2\nconst COS = Math.cos(YAW)\nconst SIN = Math.sin(YAW)\nconst START_X = 10\nconst SPEED = 2.4\n/** `carW(0.6, 0.06, 2.15)` for a car turned by `YAW`, as the matrix multiplies it (the y component is replaced by 0.04). */\nconst contactX = (carX: number | object) => add(COS * 0.6, 0 * 0.06, SIN * 2.15, carX)\nconst contactZ = add(-SIN * 0.6, 0 * 0.06, COS * 2.15, 1.5)\nconst BACKWARD = normalized([1, 0.35, 0])\n\n/** W1090-1100 (`faisca`): a bumper dragging on the asphalt throws a river of sparks that bounce and go from white to orange, with a short light and dark scratches. */\nexport const SCRAPING_SPARKS: EffectDef = {\n id: 'scraping_sparks',\n pageId: 'faisca',\n team: 'vehicles',\n budget: { particles: 200, lights: 1 },\n loop: 4.4,\n view: { eye: [2, 1.6, 5.5], target: [3, 0.3, 1.5] },\n setup: [cue('zombie_visible', { index: -1, visible: false }), cue('car_pose', { x: START_X, z: 1.5, yaw: YAW })],\n timeline: [],\n tick: [\n set('carX', sub(START_X, mul(TIME, SPEED))),\n set('contact', [contactX(variable('carX')), 0.04, contactZ]),\n cue('car_pose', { x: variable('carX'), z: 1.5, yaw: YAW }),\n emit('sp', 'streak', 200, { p: variable('contact'), dir: BACKWARD, cone: 0.55, sp: [3, 8], v: [-2.4, 0, 0], life: [0.2, 0.5], len: 0.02, c0: 0xfff0c0, c1: 0xff5010, g: 9.8, gnd: 2 }),\n emit('gl', 'add', 40, { p: variable('contact'), r: 0.05, size: [0.08, 0.16], life: 0.05, c0: 0xffd8a0, fi: 0 }),\n when(lt(rand(), mul(DELTA, 14)), [decal(vx(variable('contact')), 0, add(vz(variable('contact')), rnd(-0.05, 0.05)), 0.12, 0x0a0a0a, { grow: 0, life: 60, ar: 0.25 })]),\n fireLight(0, [vx(variable('contact')), 0.34, vz(variable('contact'))], 1.6, mul(TIME, 3)),\n ],\n}\n","import { DELTA, eq, gt, ite, lt, mul, PARAM, rand, TIME } from '../dsl'\nimport { burst, call, emit, fireLight, fireLightOff, when } from '../steps'\nimport type { EffectDef } from '../types'\nimport { carDirection, carPoint } from './car'\n\nconst HOOD = carPoint(0, 1.02, 1.4)\nconst EXHAUST = carPoint(0.5, 0.3, -2.2)\nconst EXHAUST_DIRECTION = carDirection(EXHAUST, carPoint(0.5, 0.3, -3))\nconst SPARK_FROM = carPoint(0, 0.7, 2.1)\nconst SPARK_DIRECTION = carDirection(SPARK_FROM, carPoint(0, 0.9, 3))\nconst WORN = 1\nconst BAD = 2\nconst CRITICAL = 3\n\n/** W1101-1112 (`motor`): exhaust only at 100, a thread of white steam at 60, steady grey smoke and the odd spark at 30, thick black smoke with fire at 10. */\nexport const ENGINE_SMOKE: EffectDef = {\n id: 'engine_smoke',\n pageId: 'motor',\n team: 'vehicles',\n params: { id: 'condition', options: ['100', '60', '30', '10'], default: 2 },\n budget: { particles: 120, lights: 1 },\n loop: 0,\n view: { eye: [9.5, 2.6, 5.2], target: [5.2, 0.9, 1.5] },\n timeline: [],\n tick: [\n emit('ex', 'normal', ite(eq(PARAM, CRITICAL), 6, 3), { p: EXHAUST, dir: EXHAUST_DIRECTION, cone: 0.3, sp: [0.6, 1.2], drag: 1.2, life: [1, 1.8], size: [0.06, 0.12], grow: 5, c0: ite(gt(PARAM, 1), 0x55524e, 0x9a9a96), a: 0.18, g: -0.2 }),\n when(eq(PARAM, WORN), [\n emit('st', 'normal', 14, { p: HOOD, box: [0.4, 0, 0.3], dir: [0, 1, 0], cone: 0.35, sp: [0.5, 1], drag: 0.8, life: [1.4, 2.4], size: [0.15, 0.25], grow: 4, c0: 0xc8ccd0, a: 0.22, g: -0.3 }),\n ]),\n when(eq(PARAM, BAD), [\n emit('st', 'normal', 22, { p: HOOD, box: [0.4, 0, 0.3], dir: [0, 1, 0], cone: 0.35, sp: [0.6, 1.2], drag: 0.6, life: [2, 3], size: [0.2, 0.35], grow: 4, c0: 0x6a6a68, c1: 0x3a3a3c, a: 0.4, g: -0.35 }),\n when(lt(rand(), mul(DELTA, 0.8)), [burst('streak', 8, { p: SPARK_FROM, dir: SPARK_DIRECTION, cone: 0.6, sp: [2, 4], life: 0.3, len: 0.02, c0: 0xffd080, c1: 0xff4000, g: 9.8 })]),\n ]),\n when(eq(PARAM, CRITICAL), [\n emit('st', 'normal', 34, { p: HOOD, box: [0.5, 0, 0.4], dir: [0, 1, 0], cone: 0.3, sp: [0.8, 1.6], drag: 0.4, life: [2.5, 3.8], size: [0.3, 0.5], grow: 4.5, c0: 0x1c1b1b, c1: 0x0e0e10, a: 0.6, g: -0.4 }),\n call('fire_emit', 'hood', carPoint(0, 0.8, 2.0), 0.3, 0.45, { noSmoke: true }),\n fireLight(0, carPoint(0, 1.2, 2.6), 2.2, TIME),\n ], [fireLightOff(0)]),\n ],\n}\n","import { rnd, scaled, query, vadd } from '../dsl'\nimport { behavior, call, cue, debris, repeat } from '../steps'\nimport type { EffectStep } from '../types'\nimport { carPoint } from './car'\n\nconst UP = [0, 1, 0] as const\n\n/** The velocity the page gives a door: the car's own space pushed outward by `s * 4`, minus the centre, with the height set to 3 (W1124). */\nconst doorVelocity = (side: number): readonly [number, number, number] => {\n const out = carPoint(side * 4, 3, 0.3)\n const centre = carPoint(0, 0, 0.3)\n return [out[0] - centre[0], 3, out[2] - centre[2]]\n}\n\n/** `vehicleBoom` (W1118-1128): the blast at the car, the car jumping, glass in the air, then the bonnet, both doors and a wheel flying off. */\nexport const VEHICLE_BOOM: readonly EffectStep[] = [\n call('explode', vadd(query('car', 'position'), [0, 0.6, 0]), 1.8, 0x2a1a14),\n cue('car_burnt'),\n behavior('car', 'launch', { rise: 4.5, spin: 0.9 }),\n repeat(scaled(30), debris({ p: vadd(query('car', 'position'), [0, 1.3, 0]), dir: UP, cone: 1.3, sp: [2, 6], size: [0.06, 0.006, 0.06], c: 0xa8c8d8, life: 20 })),\n debris({ p: carPoint(0, 1, 1.4), v: [rnd(-1, 1), 9, rnd(-1, 1)], sp: 0, size: [1.6, 0.06, 1.2], c: 0x3a1d18, spin: 6, life: 20, bounce: 0.2 }),\n ...[-1, 1].map((side) => debris({ p: carPoint(side * 0.95, 0.7, 0.3), v: doorVelocity(side), sp: 0, size: [0.06, 0.7, 1.1], c: 0x3a1d18, spin: 5, life: 20, bounce: 0.2 })),\n debris({ p: carPoint(0.95, 0.38, 1.35), v: [3, 4, 2], sp: 0, size: [0.3, 0.76, 0.76], c: 0x111111, spin: 8, life: 20, bounce: 0.4 }),\n]\n","import { add, and, clamp, DELTA, div, ge, gt, lt, mul, query, sub, TIME, variable, vadd, vlerp } from '../dsl'\nimport { after, behavior, burst, call, cue, decal, emit, fireLight, set, when } from '../steps'\nimport type { EffectDef } from '../types'\nimport { VEHICLE_BOOM } from './boom'\nimport { carDirection, carPoint } from './car'\n\nconst TANK = carPoint(-0.6, 0.25, -1.3)\nconst TANK_FLOOR = [TANK[0], 0, TANK[2]] as const\nconst POOL = [TANK_FLOOR[0] - 1.2, 0, TANK_FLOOR[2] - 0.6] as const\nconst BOOM_AT = 7.2\nconst HOLE = carPoint(-0.96, 0.6, -1.2)\n\n/** W1113-1132 (`vazamento`): petrol drips into a pool, a shot into the metal throws a spark, the fire runs along the pool to the tank and the car blows up. */\nexport const FUEL_LEAK: EffectDef = {\n id: 'fuel_leak',\n pageId: 'vazamento',\n team: 'vehicles',\n budget: { particles: 400, lights: 1 },\n loop: 11,\n view: { eye: [10, 2.4, 4.2], target: [5.6, 0.4, 0] },\n setup: [cue('zombie_visible', { index: -1, visible: false })],\n timeline: [\n decal(POOL[0], 0, POOL[2], 2.2, 0x1f2230, { grow: 3, life: 60, ar: 0.6, fluid: 'oil' }),\n after(\n 3.4,\n call('impact', 'metal', HOLE, carDirection(HOLE, carPoint(-2, 0.6, -1.2)), 1.5),\n burst('streak', 14, { p: [POOL[0], 0.05, POOL[2]], cone: 1.4, sp: [1, 3], life: 0.3, len: 0.02, c0: 0xffd080, c1: 0xff4000, g: 9.8 }),\n ),\n after(BOOM_AT, ...VEHICLE_BOOM),\n ],\n tick: [\n when(lt(TIME, BOOM_AT), [\n emit('drip', 'normal', 12, { p: TANK, box: [0.05, 0, 0.05], sp: 0, life: 1, size: 0.03, c0: 0x5a5a7a, a: 0.9, g: 9.8, fi: 0, gnd: 1 }),\n ]),\n when(and(gt(TIME, 3.5), lt(TIME, BOOM_AT)), [\n set('spread', clamp(div(sub(TIME, 3.5), 2.5), 0, 1)),\n set('front', vlerp(POOL, TANK_FLOOR, mul(variable('spread'), 0.8))),\n call('fire_emit', 'vz', variable('front'), add(0.6, mul(variable('spread'), 0.4)), 1.1, { smoke: 14 }),\n call('fire_emit', 'vz2', POOL, 0.7, 0.8, { smoke: 6 }),\n fireLight(0, vadd(variable('front'), [0, 1, 0]), 3, TIME),\n ]),\n when(ge(TIME, BOOM_AT), [\n call('fire_emit', 'wr', vadd(query('car', 'position'), [0, 0.9, 0]), 0.8, 1.4, { smoke: 18, smokeAlpha: 0.55 }),\n fireLight(0, vadd(query('car', 'position'), [0, 1.6, 0]), 3.4, TIME),\n behavior('car', 'step', { dt: DELTA }),\n ]),\n ],\n}\n\n/**\n * The same leak without the fire and the explosion: the pool of petrol spreading under the car and the drips from the hole, for as long as it plays. The steps\n * are the page's own (`decal` of the pool, `emit` of the drips); the spark of the shot and everything from `BOOM_AT` on (the fire running to the tank, the\n * blast) are left out. A normal bullet only does this; when the leak catches fire the vehicle asks for the full `fuel_leak` sequence (`vehicle_leak_ignited`).\n */\nexport const FUEL_LEAK_ONLY: EffectDef = {\n id: 'fuel_leak_only',\n pageId: 'vazamento',\n team: 'vehicles',\n budget: { particles: 200, lights: 0 },\n loop: 11,\n view: FUEL_LEAK.view,\n setup: FUEL_LEAK.setup,\n timeline: [decal(POOL[0], 0, POOL[2], 2.2, 0x1f2230, { grow: 3, life: 60, ar: 0.6, fluid: 'oil' })],\n tick: [emit('drip', 'normal', 12, { p: TANK, box: [0.05, 0, 0.05], sp: 0, life: 1, size: 0.03, c0: 0x5a5a7a, a: 0.9, g: 9.8, fi: 0, gnd: 1 })],\n}\n","import { DELTA, gt, query, TIME, vadd } from '../dsl'\nimport { after, behavior, call, cue, fireLight, when } from '../steps'\nimport type { EffectDef } from '../types'\nimport { VEHICLE_BOOM } from './boom'\n\n/** W1133-1139 (`explosao`, `lab: 1`): a big blast (scale 1.8 of the grenade), the car jumps and lands crooked, bonnet, doors and a wheel fly, and the wreck burns. */\nexport const CAR_EXPLOSION: EffectDef = {\n id: 'car_explosion',\n pageId: 'explosao',\n team: 'vehicles',\n lab: 'car_fuel_tank',\n budget: { particles: 600, debris: 34, lights: 1 },\n loop: 10,\n view: { eye: [12, 4.2, 8], target: [5.6, 1, 0.8] },\n props: ['car'],\n setup: [cue('zombie_visible', { index: -1, visible: false })],\n timeline: [after(0.4, ...VEHICLE_BOOM)],\n tick: [\n when(gt(TIME, 0.5), [\n call('fire_emit', 'wr', vadd(query('car', 'position'), [0, 0.9, 0]), 0.8, 1.4, { smoke: 18, smokeAlpha: 0.55 }),\n fireLight(0, vadd(query('car', 'position'), [0, 1.6, 0]), 3.4, TIME),\n ]),\n behavior('car', 'step', { dt: DELTA }),\n ],\n}\n","import { add, eq, gt, INDEX, mod, mul, or, pick } from '../dsl'\nimport { after, behavior, burst, camera, cue, screen, repeat, when } from '../steps'\nimport type { EffectDef } from '../types'\nimport { carPoint } from './car'\n\nconst HITS = [[90, 40], [170, 60], [120, 75], [60, 50]]\nconst HIT_X = HITS.map(([x]) => x)\nconst HIT_Y = HITS.map(([, y]) => y)\nconst ZOMBIE = carPoint(0, 0, 3.1)\nconst CAR_YAW = -0.4\n\n/** W1140-1152 (`parabrisa`): from inside the car, blows shake the camera and add a star crack; at the second and fourth a smear of blood with a handprint runs down. */\nexport const WINDSHIELD: EffectDef = {\n id: 'windshield',\n pageId: 'parabrisa',\n team: 'vehicles',\n budget: { particles: 6, decals: 1 },\n loop: 5.8,\n setup: [\n cue('zombie_visible', { index: -1, visible: false }),\n behavior('walkers', 'place', { index: 0, x: ZOMBIE[0], z: ZOMBIE[2], facing: CAR_YAW + Math.PI }),\n camera(carPoint(-0.3, 1.35, -0.2), carPoint(0, 1.2, 2.2), true),\n ],\n timeline: [\n repeat(\n HITS.length,\n after(\n add(0.6, mul(INDEX, 0.85)),\n cue('zombie_swing', { index: 0, side: mod(INDEX, 2) }),\n after(\n add(0.6, mul(INDEX, 0.85), 0.15),\n behavior('cracks', 'crack', { x: pick(HIT_X, INDEX), y: pick(HIT_Y, INDEX), big: gt(INDEX, 1) }),\n when(or(eq(INDEX, 1), eq(INDEX, 3)), [behavior('cracks', 'blood_streaks', { x: pick(HIT_X, INDEX), y: pick(HIT_Y, INDEX) })]),\n screen({ add: { shake: 0.18 } }),\n burst('add', 6, { p: carPoint(-0.1, 1.25, 0.75), box: [0.4, 0.1, 0.05], size: [0.01, 0.03], life: [0.2, 0.5], c0: 0xffffff, a: 0.8, fi: 0.2 }),\n cue('windshield_stage', { stage: add(INDEX, 1) }),\n ),\n ),\n ),\n ],\n}\n","import { add, and, DELTA, gt, lt, mod, mul, rnd, TIME } from '../dsl'\nimport { after, burst, cue, lens, repeat, screen, when } from '../steps'\nimport type { EffectDef, EffectStep } from '../types'\n\nconst FRESH_BLOOD = 0x8a0a0a\nconst BEAT_RATE = 1.9\n\n/** A blow from one side (`swing`, W1178): a red edge on that side, a shake and splashes on the lens, 0.18 s after the swing starts. */\nconst blow = (side: 0 | 1): EffectStep[] => [\n cue('zombie_swing', { index: -1, side }),\n after(\n add(TIME, 0.18),\n screen({ set: side === 0 ? { dirL: 1 } : { dirR: 1 }, add: { shake: 0.3 } }),\n repeat(4, lens(side === 0 ? rnd(0.05, 0.3) : rnd(0.7, 0.95), rnd(0.3, 0.8), rnd(0.02, 0.05), '110,8,8', 0.8)),\n burst('normal', 12, { p: [0, 1.4, 0.9], dir: [0, 0.2, 1], cone: 0.6, sp: [1, 2], life: 1, size: [0.03, 0.05], c0: FRESH_BLOOD, g: 9.8, fi: 0, gnd: 3, ds: [0.05, 0.1] }),\n ),\n]\n\n/** W1169-1185 (`dano`): two blows, from the left and from the right, then the bite: a red flash, the colour drains, the heartbeat pulses and big blood runs down the lens. */\nexport const DAMAGE_AND_BITE: EffectDef = {\n id: 'damage_and_bite',\n pageId: 'dano',\n team: 'screen',\n budget: {},\n loop: 5.6,\n view: { eye: [0, 1.6, 1.7], target: [0, 1.5, 0] },\n timeline: [\n after(0.3, ...blow(0)),\n after(1.6, ...blow(1)),\n after(3, cue('zombie_lunge', { z: 0.45 })),\n after(\n 3.15,\n screen({ set: { red: 1, desat: 0.9 }, add: { shake: 0.6 } }),\n cue('bite', { infection: 1 }),\n repeat(7, lens(rnd(0.2, 0.8), rnd(0.2, 0.7), rnd(0.04, 0.1), '100,6,6', 1)),\n ),\n ],\n tick: [\n when(and(gt(TIME, 3.2), lt(TIME, 5.4)), [when(lt(mod(mul(TIME, BEAT_RATE), 1), mul(DELTA, BEAT_RATE)), [screen({ set: { beat: 1 } })])]),\n ],\n}\n","import { add, CAMERA_POSITION, cameraForward, DELTA, eq, gt, INDEX, lt, max, mod, mul, PARAM, rand, rnd, sin, TIME, variable, vadd } from '../dsl'\nimport { burst, cue, post, repeat, screen, set, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst MALAISE = 0\nconst FEVER = 1\nconst DELIRIUM = 2\nconst LAST_HOURS = 3\nconst GHOSTS = [[-3, -1.8], [3.5, -2.4], [-6, 1.5]]\n\n/** W1186-1205 (`infeccao`): malaise (washed colour, a breathing vignette), fever (cold edge, waves of blur, a cough that shakes the camera), delirium, the last hours. */\nexport const INFECTION_PHASES: EffectDef = {\n id: 'infection_phases',\n pageId: 'infeccao',\n team: 'screen',\n params: { id: 'phase', options: ['malaise', 'fever', 'delirium', 'last_hours'], default: 1 },\n budget: { particles: 10 },\n loop: 0,\n view: { eye: [2, 1.7, 7], target: [0, 1.2, -1] },\n setup: [\n cue('zombie_visible', { index: -1, visible: false }),\n ...GHOSTS.map(([x, z], index) => cue('ghost_place', { index, x, z, turn: rnd(-0.4, 0.4) })),\n ],\n timeline: [set('nextCough', 2)],\n tick: [\n when(eq(PARAM, MALAISE), [post({ desat: 0.25, tunnel: add(0.15, mul(0.12, sin(mul(TIME, 1.2)))), blur: mul(0.3, max(0, sin(mul(TIME, 0.7)))) })]),\n when(eq(PARAM, FEVER), [\n post({ cold: add(0.8, mul(0.15, sin(mul(TIME, 2)))), blur: add(0.6, mul(0.6, sin(mul(TIME, 1.6)))), desat: 0.3 }),\n when(gt(TIME, variable('nextCough')), [\n set('nextCough', add(TIME, rnd(3, 5))),\n screen({ add: { shake: 0.25 } }),\n burst('normal', 10, { p: vadd(CAMERA_POSITION, cameraForward([0, -0.15, -0.6])), dir: cameraForward([0, 0, -1]), cone: 0.4, sp: [0.6, 1.2], drag: 1.5, life: 1.2, size: [0.05, 0.1], grow: 5, c0: 0xdfe6ee, a: 0.2 }),\n ]),\n ]),\n when(eq(PARAM, DELIRIUM), [\n post({ chroma: add(2, mul(3, add(0.5, mul(0.5, sin(mul(TIME, 1.7)))))), hue: mul(22, sin(mul(TIME, 0.6))), tunnel: 0.3, desat: 0.15 }),\n repeat(GHOSTS.length, cue('ghost_visible', { index: INDEX, visible: gt(sin(add(mul(TIME, add(1.3, mul(INDEX, 0.7))), mul(INDEX, 2))), 0.6) })),\n when(lt(rand(), mul(DELTA, 0.3)), [screen({ set: { white: 0.12 } })]),\n ]),\n when(eq(PARAM, LAST_HOURS), [\n post({ desat: 0.85, tintAlpha: 0.22, tintColor: '70,110,40', tunnel: 0.55, blur: 1.2 }),\n when(lt(mod(mul(TIME, 2.2), 1), mul(DELTA, 2.2)), [screen({ set: { beat: 1 }, add: { shake: 0.05 } })]),\n ]),\n ],\n}\n","import { add, CAMERA_POSITION, clamp, DELTA, div, eq, INDEX, ite, lt, min, mod, mul, query, rnd, sub, TIME, variable, vdist } from '../dsl'\nimport { behavior, cue, post, repeat, screen, set, when } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst WALKERS = 6\nconst NEAR = 9\nconst RANGE = 7\nconst RESTING_BPM = 70\nconst BPM_RISE = 80\nconst walkerPoint = [query('walkers', 'x', INDEX), 0, query('walkers', 'z', INDEX)]\n\n/** W1206-1217 (`panico`): the aim trembles, the tunnel closes as the nearest zombie arrives and the heartbeat quickens from 70 to 150 bpm. */\nexport const PANIC: EffectDef = {\n id: 'panic',\n pageId: 'panico',\n team: 'screen',\n budget: {},\n loop: 8,\n view: { eye: [0, 1.65, 6], target: [0, 1.3, -2] },\n setup: [\n cue('zombie_visible', { index: -1, visible: false }),\n repeat(\n WALKERS,\n set('startX', rnd(-4, 4)),\n set('startZ', sub(rnd(-4, -2), mul(INDEX, 0.6))),\n behavior('walkers', 'place', { index: INDEX, x: variable('startX'), z: variable('startZ'), facing: 0 }),\n behavior('walkers', 'walk', { index: INDEX, to: [rnd(-1.2, 1.2), 0, 5], speed: ite(eq(mod(INDEX, 3), 1), 1.6, 0.7) }),\n ),\n ],\n timeline: [],\n tick: [\n set('nearest', 99),\n repeat(WALKERS, set('nearest', min(variable('nearest'), vdist(walkerPoint, CAMERA_POSITION)))),\n set('fear', clamp(div(sub(NEAR, variable('nearest')), RANGE), 0, 1)),\n set('bpm', add(RESTING_BPM, mul(BPM_RISE, variable('fear')))),\n when(lt(mod(div(mul(TIME, variable('bpm')), 60), 1), div(mul(DELTA, variable('bpm')), 60)), [screen({ set: { beat: add(0.4, mul(0.6, variable('fear'))) } })]),\n post({ tunnel: add(0.1, mul(0.65, variable('fear'))), desat: mul(0.45, variable('fear')) }),\n set('tremor', add(0.012, mul(0.035, variable('fear')))),\n ],\n}\n","import { rnd } from '../dsl'\nimport { after, burst, cue, lens, repeat, screen } from '../steps'\nimport type { EffectDef } from '../types'\n\n/** W1218-1226 (`cegueira`): a green cloud bursts between the bloater and the camera, the screen goes green and blurred and clears in 5 s; two coughs shake the camera. */\nexport const BLOATER_BLINDNESS: EffectDef = {\n id: 'bloater_blindness',\n pageId: 'cegueira',\n team: 'screen',\n budget: { particles: 30 },\n loop: 7.5,\n view: { eye: [0, 1.6, 2.6], target: [0, 1.3, 0] },\n setup: [cue('bloater_pose', { eyes: 0x5fe08a, skin: 0x6b7a52, belly: [1.4, 1.25, 1.6] })],\n timeline: [\n after(\n 0.7,\n cue('zombie_visible', { index: -1, visible: false }),\n screen({ set: { gas: 1 }, add: { shake: 0.3 } }),\n burst('normal', 30, { p: [0, 1.3, 0.8], r: 0.6, sp: [0.5, 2], drag: 2, life: [3, 5], size: [0.8, 1.6], grow: 1.6, c0: 0x6f8a3c, c1: 0x3b4a26, a: 0.3, fi: 0.05 }),\n repeat(8, lens(rnd(0.15, 0.85), rnd(0.15, 0.7), rnd(0.03, 0.08), '110,150,50', 0.9)),\n ),\n after(2, screen({ add: { shake: 0.2 } })),\n after(3.4, screen({ add: { shake: 0.2 } })),\n ],\n}\n","import { TIME } from '../dsl'\nimport { call, cue, fireLight } from '../steps'\nimport type { EffectDef } from '../types'\n\nconst FIRE = [-1.4, 0.1, 3.0] as const\n\n/** W1227-1233 (`fogueira`): a low flame, embers that spiral up, thin smoke and a warm light that trembles and paints the survivor. */\nexport const CAMPFIRE: EffectDef = {\n id: 'campfire',\n pageId: 'fogueira',\n team: 'screen',\n budget: { particles: 120, lights: 1 },\n loop: 0,\n view: { eye: [1.4, 1.6, 6.4], target: [-1.4, 0.6, 3] },\n setup: [cue('zombie_visible', { index: -1, visible: false }), cue('survivor_pose', { x: -2.6, z: 3.6, turn: 2.2 })],\n timeline: [],\n tick: [\n call('fire_emit', 'camp', FIRE, 0.25, 0.55, { smoke: 8, smokeColor: 0x4a4642, smokeAlpha: 0.25 }),\n fireLight(0, [FIRE[0], FIRE[1] + 0.8, FIRE[2]], 2.4, TIME),\n ],\n}\n","import { add, gt, TIME, variable } from '../dsl'\nimport { after, burst, cue, set, when } from '../steps'\nimport { normalized } from '../vector-math'\nimport type { EffectDef, EffectStep } from '../types'\n\nconst FACING = 0.5\n/** The survivor's mouth and the way it breathes out, with the survivor at (-1.6, 0, 1.8) turned by `FACING` (W1236-1238). */\nconst MOUTH = [Math.sin(FACING) * 0.2 + -1.6, 1.68, Math.cos(FACING) * 0.2 + 1.8] as const\nconst BREATH = normalized([Math.sin(FACING), -0.15, Math.cos(FACING)])\nconst BREATH_EVERY = 2.4\nconst COUGH_EVERY = 6.5\n\n/** A puff of breath (`puff`, W1239): `strength` scales how many and how fast. */\nconst puff = (strength: number): EffectStep =>\n burst('normal', 8 * strength, { p: MOUTH, dir: BREATH, cone: 0.35, sp: [0.5, 1.1 * strength], drag: 1.6, life: [1.2, 1.8], size: [0.05, 0.09], grow: 6, c0: 0xdfe6ee, a: 0.22, g: -0.1 })\n\n/** W1234-1243 (`tosse`): a short breath every 2.4 s; in a cough, three quick bigger ones and a jolt of the body; the torch shows its beam in the fog. */\nexport const COUGH_AND_BREATH: EffectDef = {\n id: 'cough_and_breath',\n pageId: 'tosse',\n team: 'screen',\n budget: { particles: 40, lights: 1 },\n loop: 0,\n view: { eye: [2.4, 1.8, 6.5], target: [-1.4, 1.3, 1.6] },\n setup: [cue('zombie_visible', { index: -1, visible: false }), cue('survivor_pose', { x: -1.6, z: 1.8, turn: FACING }), cue('torch', { on: true })],\n timeline: [set('nextBreath', 0.5), set('nextCough', 3)],\n tick: [\n when(gt(TIME, variable('nextBreath')), [set('nextBreath', add(TIME, BREATH_EVERY)), puff(1)]),\n when(gt(TIME, variable('nextCough')), [\n set('nextCough', add(TIME, COUGH_EVERY)),\n ...[0, 0.25, 0.5].map((delay) => after(add(TIME, delay), puff(1.6), cue('survivor_jolt', { tilt: 0.25 }))),\n ]),\n ],\n}\n","import { cos, local, mul, rnd, scaled, sin, variable, vnorm } from './dsl'\nimport { burst, call, debris, repeat, screen, spawn } from './steps'\nimport type { EffectDef } from './types'\n\nconst FACING = [0, 0, 1] as const\nconst UP = [0, 1, 0] as const\nconst ORIGIN = [0, 0, 0] as const\nconst FORWARD = [0, 0, -1] as const\nconst IMPACT_MATERIALS = ['concrete', 'metal', 'wood', 'glass', 'flesh', 'water'] as const\n\n/** What a hit leaves on one material, the way the Workshop's `impact` helper does (W374-391), played once at the hit. */\nconst impactEffect = (material: (typeof IMPACT_MATERIALS)[number]): EffectDef => ({\n id: `impact_${material}`,\n pageId: `event:impact_${material}`,\n team: 'weapons',\n budget: { particles: 40, decals: 1 },\n loop: 2,\n timeline: [call('impact', material, ORIGIN, FACING, 1, 0)],\n})\n\n/** The flash of one weapon at its muzzle (W417 `muzzle`), played once: the recipe's own per-weapon sizes, petals, light and smoke. */\nconst muzzleEffect = (weapon: 'glock' | 'ak' | 'r870' | 'barrett'): EffectDef => ({\n id: `muzzle_${weapon}`,\n pageId: `event:muzzle_${weapon}`,\n team: 'weapons',\n budget: { particles: 60, lights: 1 },\n loop: 2.4,\n timeline: [call('muzzle', weapon, ORIGIN, FORWARD)],\n})\n\n/** A case thrown out of the port (W430 `casing`): brass, a red shell or a .50 case. */\nconst casingEffect = (id: string, type: '' | 'shell' | '50'): EffectDef => ({\n id,\n pageId: `event:${id}`,\n team: 'weapons',\n budget: { debris: 1 },\n loop: 2,\n timeline: [call('casing', ORIGIN, type)],\n})\n\n/**\n * The effects the destruction events play, as slices of the Workshop's recipes: the same bursts, with the recipe's own numbers, centred on the point the event\n * happens (`data/event-effects.json` says which event plays which). They are not Workshop pages, so they have no golden of their own; each cites the lines it was cut from.\n */\nexport const EVENT_EFFECTS: readonly EffectDef[] = [\n ...IMPACT_MATERIALS.map(impactEffect),\n ...(['glock', 'ak', 'r870', 'barrett'] as const).map(muzzleEffect),\n casingEffect('casing_brass', ''),\n casingEffect('casing_shell', 'shell'),\n casingEffect('casing_50', '50'),\n {\n // door_stages (W856-871): the dust a blow shakes loose from the frame and the splinters.\n id: 'stage_crack',\n pageId: 'event:stage_crack',\n team: 'destruction',\n budget: { particles: 12, debris: 5 },\n loop: 2,\n timeline: [\n burst('normal', 12, { p: ORIGIN, box: [0.6, 0.05, 0.02], dir: [0, -0.287, 0.958], cone: 0.6, sp: [0.2, 0.8], drag: 1.5, life: [0.8, 1.4], size: [0.08, 0.16], grow: 3, c0: 0x7a7366, a: 0.45, g: 0.4 }),\n repeat(scaled(2), debris({ p: [rnd(-0.5, 0.5), rnd(0, 1.6), 0], dir: [0, 0.287, 0.958], cone: 0.6, sp: [1, 2.5], size: [0.015, 0.015, 0.1], c: 0x7a5530, life: 12 })),\n ],\n },\n {\n // block_wall (W872-886, 3.6 s): the ring of dust that rolls out when the wall gives way.\n id: 'breach_dust',\n pageId: 'event:breach_dust',\n team: 'destruction',\n budget: { particles: 90, debris: 16 },\n loop: 5,\n timeline: [\n screen({ add: { shake: 0.25 } }),\n burst('normal', 30, { p: ORIGIN, box: [1.4, 0.8, 0.2], sp: [0.3, 1.2], drag: 1.5, life: [1.5, 2.6], size: [0.4, 0.7], grow: 3, c0: 0x8a857a, c1: 0x4a4640, a: 0.45 }),\n repeat(scaled(16), debris({ p: ORIGIN, box: [1.3, 0.8, 0.1], sp: [1, 3], size: [0.08, 0.07, 0.08], k: [0.5, 1.5], c: 0x6e6a63, life: 20 })),\n repeat(\n scaled(60),\n spawn('normal', local('angle', rnd(0, 6.283), {\n p: [rnd(-1.4, 1.4), 0.2, rnd(-0.6, 0.4)],\n dir: vnorm([cos(variable('angle')), 0.3, sin(variable('angle'))]),\n cone: 0.4, sp: [0.8, 2.4], drag: 1.3, life: [2.5, 4.5], size: [0.6, 1], grow: 3, c0: 0x7a7268, c1: 0x4a4640, a: 0.45, fi: 0.12,\n })),\n ),\n ],\n },\n {\n // building_collapse (W872-886): the slab dropping in blocks and the cloud that rolls along the street.\n id: 'collapse_dust',\n pageId: 'event:collapse_dust',\n team: 'destruction',\n budget: { particles: 350, debris: 26 },\n loop: 8,\n timeline: [\n screen({ add: { shake: 0.45 } }),\n repeat(6, debris({ p: ORIGIN, box: [2, 0.05, 0.6], v: [rnd(-0.5, 0.5), rnd(-0.5, 0), rnd(0, 0.8)], sp: 0, size: [rnd(0.5, 1.1), 0.25, rnd(0.4, 0.8)], c: 0x6e6a63, spin: 3, life: 30, bounce: 0.1 })),\n repeat(scaled(20), debris({ p: ORIGIN, box: [2.1, 0.1, 0.7], v: [rnd(-1, 1), rnd(-1, 1), rnd(0, 1.5)], sp: 0, size: [0.12, 0.1, 0.12], k: [0.5, 1.5], c: 0x5e5a54, life: 30 })),\n repeat(\n scaled(70),\n spawn('normal', local('angle', rnd(-0.4, 3.5), {\n p: [rnd(-2, 2), 0.2, rnd(-0.5, 0.5)],\n dir: vnorm([mul(cos(variable('angle')), 0.6), 0.25, sin(variable('angle'))]),\n cone: 0.5, sp: [1, 3.2], drag: 1.1, life: [3, 6], size: [0.7, 1.2], grow: 3, c0: 0x7a7268, c1: 0x4a4640, a: 0.5, fi: 0.12,\n })),\n ),\n ],\n },\n {\n // shopfront_glass (W846-855): the pane coming down as shards, the flash of the glints and the pale dust.\n id: 'glass_burst',\n pageId: 'event:glass_burst',\n team: 'destruction',\n budget: { debris: 80, particles: 50 },\n loop: 5,\n timeline: [\n screen({ add: { shake: 0.08 } }),\n repeat(\n scaled(75),\n debris({ p: ORIGIN, box: [1.25, 0.85, 0.02], v: [rnd(-0.4, 0.4), rnd(-0.5, 0.8), rnd(0.6, 2)], sp: 0, size: [rnd(0.05, 0.18), 0.006, rnd(0.05, 0.18)], c: 0xa8c8d8, spin: 14, life: 30, bounce: 0.15 }),\n ),\n burst('add', 30, { p: ORIGIN, box: [1.2, 0.8, 0.05], size: [0.02, 0.05], life: [0.2, 0.6], c0: 0xffffff, a: 0.9, fi: 0.3 }),\n burst('normal', 20, { p: [0, -0.6, 0.1], box: [1.2, 0.5, 0.1], sp: [0.2, 0.6], drag: 2, life: [0.8, 1.4], size: [0.12, 0.2], grow: 3, c0: 0xbfd6e2, a: 0.2 }),\n ],\n },\n {\n // The chips macro the page uses on wood (W849) plus rust (`rustAt`), for rebar laid bare in concrete.\n id: 'rebar_expose',\n pageId: 'event:rebar_expose',\n team: 'destruction',\n budget: { particles: 20, debris: 8 },\n loop: 2,\n timeline: [call('chips', ORIGIN, UP, 8, 0.9), call('rust_at', ORIGIN, 6)],\n },\n]\n","import { add, cos, DIRECTION, div, eq, floor, gt, INDEX, ite, lt, min, mul, ne, neg, pick, rand, sin, sub, variable, vx, vz } from '../dsl'\nimport { append, flash, mesh, repeat, set, when } from '../steps'\nimport type { EffectDef, EffectStep } from '../types'\n\n/**\n * The weapon in the player's hands (the Arsenal gallery's first-person effects: the muzzle flash and smoke of `createEffects`, the backblast of a launched rocket, the rocket's\n * trail and the flame jet of a flamethrower) as recipes of the Workshop engine, drawn as the gallery draws them: as solids (cones, spheres, icosahedra) with the gallery's sizes,\n * segment counts, materials, timing and growth, not as sprites. They are written in the weapon's own axes, the gallery's: x along the barrel, y up, z to the right. A player\n * says how big (`values`), where the weapon points (`direction`, the barrel in the world) and how it is turned (`orientation`, the quaternion, pitch and roll included), and\n * seeds the numbers the effect draws, which it draws in the order the gallery drew them.\n */\nconst PI = Math.PI\n/** The gallery's `smoke`: a sphere of 10 by 8 segments, a grey that rises, slows and grows by 2.1 a second while it fades. */\nconst SMOKE = { color: 0x8a8a86, width: 10, height: 8, gravity: -0.08, drag: 2.2, growth: 2.1 }\n/** Default muzzle smoke numbers (the `smokeless` profile of `@adatechnology/game-weapons`). */\nconst DEFAULT_SMOKE = { puffs: 2, radius: 0.01, opacity: 0.14, life: 0.7, drift: 0.2, rise: 0.06, growth: 2.1, flash: 1 } as const\n/** Default random spreads on top of drift, rise and sideways speed; the `gallery` profile widens them to reproduce the Arsenal gallery's smoke. */\nconst DEFAULT_SPREAD = { drift: 0.1, rise: 0.04, side: 0.06 } as const\n/** The flash lasts 55 ms and its group grows from 0.6 to 1.5 times its size (`0.6 + min(1, age / 0.055) * 0.9`). */\nconst FLASH_SECONDS = 0.055\nconst FLASH_RAMP = [0.6, 0.9] as const\nconst FLASH_OPACITY = 0.95\n/** The gallery's palette for flame bits: orange, yellow and dark red, and a pale additive core. */\nconst FLAME_COLORS = [0xff7a22, 0xffc040, 0xc8361a, 0xfff0b0]\nconst FLAME_ADDITIVE = 3\nconst ORIGIN = [0, 0, 0] as const\n\nconst SIZE = variable('size')\nconst FLASH_SCALE = variable('flash')\nconst fallback = (name: string, defaultValue: number): unknown => ite(ne(variable(name), undefined), variable(name), defaultValue)\nconst wide = (width: number): unknown => mul(sub(rand(), 0.5), width)\n\n/** The gallery's `smoke(position, velocity, size, life, opacity)`; the keys are in the order the gallery drew its numbers. `world` is a velocity that is not turned with the weapon. */\nconst puff = (point: unknown, velocity: unknown, world: unknown, radius: unknown, life: unknown, opacity: unknown, growth: unknown = SMOKE.growth): EffectStep =>\n mesh({\n shape: 'sphere',\n p: point,\n v: velocity,\n wv: world,\n r: radius,\n seg: SMOKE.width,\n seg2: SMOKE.height,\n life,\n c0: SMOKE.color,\n a: opacity,\n fade: true,\n lit: true,\n g: SMOKE.gravity,\n drag: SMOKE.drag,\n grow: growth,\n })\n\n/** The gallery's `flameBit`: an icosahedron of one of four colours, tumbling, thrown, rising, growing and slowing. The numbers are drawn in the gallery's order, the colour after the life. */\nconst flameBit = (point: unknown, velocity: unknown, world: unknown, radius: unknown, life: unknown): EffectStep[] => [\n set('bitPoint', point),\n set('bitVelocity', velocity),\n set('bitWorld', world),\n set('bitRadius', radius),\n set('bitLife', life),\n set('bitColour', floor(mul(rand(), FLAME_COLORS.length))),\n mesh({\n shape: 'icosahedron',\n p: variable('bitPoint'),\n v: variable('bitVelocity'),\n wv: variable('bitWorld'),\n r: variable('bitRadius'),\n seg: 1,\n rot: [mul(rand(), 3), mul(rand(), 3), 0],\n spin: [3, 4, 2],\n life: variable('bitLife'),\n c0: pick(FLAME_COLORS, variable('bitColour')),\n add: eq(variable('bitColour'), FLAME_ADDITIVE),\n a: 0.9,\n fade: true,\n g: -1.4,\n drag: 1.4,\n grow: 2.2,\n }),\n]\n\n/** One piece of the flash: additive, 55 ms, in the group that grows and is rolled by `roll`. */\nconst flashPiece = (shape: 'cone' | 'cone_open' | 'sphere', radius: unknown, height: unknown, width: number, at: unknown, rot: unknown, color: number): EffectStep =>\n mesh({ shape, r: radius, ...(height === undefined ? {} : { h: height }), seg: width, seg2: shape === 'sphere' ? 8 : 1, p: ORIGIN, at, rot, roll: variable('roll'), ramp: FLASH_RAMP, life: FLASH_SECONDS, c0: color, a: FLASH_OPACITY, add: true })\n\n/**\n * `muzzleFlash` of the gallery (size `k`): an open cone, four small cones and a sphere, additive for 55 ms and growing, a short orange light that holds its strength, and six\n * puffs of smoke. It draws the four angles of the small cones, the roll of the group, then the velocity and life of each puff.\n */\nconst MUZZLE_FLASH: EffectDef = {\n id: 'viewmodel_muzzle_flash',\n pageId: 'event:viewmodel_muzzle_flash',\n team: 'weapons',\n budget: { particles: 14, lights: 1 },\n loop: 0.1,\n setup: [\n set('size', fallback('size', 1)),\n set('puffs', fallback('puffs', DEFAULT_SMOKE.puffs)),\n set('radius', fallback('radius', DEFAULT_SMOKE.radius)),\n set('opacity', fallback('opacity', DEFAULT_SMOKE.opacity)),\n set('life', fallback('life', DEFAULT_SMOKE.life)),\n set('drift', fallback('drift', DEFAULT_SMOKE.drift)),\n set('rise', fallback('rise', DEFAULT_SMOKE.rise)),\n set('growth', fallback('growth', DEFAULT_SMOKE.growth)),\n set('flash', fallback('flash', DEFAULT_SMOKE.flash)),\n set('driftSpread', fallback('driftSpread', DEFAULT_SPREAD.drift)),\n set('riseSpread', fallback('riseSpread', DEFAULT_SPREAD.rise)),\n set('sideSpread', fallback('sideSpread', DEFAULT_SPREAD.side)),\n set('lifeSpread', ite(ne(variable('lifeSpread'), undefined), variable('lifeSpread'), mul(variable('life'), 0.3))),\n ],\n timeline: [\n set('angles', []),\n repeat(4, append('angles', add(div(mul(INDEX, PI), 2), mul(rand(), 0.6)))),\n set('roll', mul(rand(), PI)),\n when(gt(FLASH_SCALE, 0), [\n flash(ORIGIN, 0xffb060, mul(4, SIZE, FLASH_SCALE), FLASH_SECONDS, mul(2, SIZE, FLASH_SCALE), true, true),\n flashPiece('cone_open', mul(0.013, SIZE, FLASH_SCALE), mul(0.085, SIZE, FLASH_SCALE), 10, [mul(0.042, SIZE, FLASH_SCALE), 0, 0], [0, 0, -PI / 2], 0xffb347),\n repeat(\n 4,\n set('angle', pick(variable('angles'), INDEX)),\n flashPiece(\n 'cone',\n mul(0.005, SIZE, FLASH_SCALE),\n mul(0.04, SIZE, FLASH_SCALE),\n 6,\n [mul(0.012, SIZE, FLASH_SCALE), mul(cos(variable('angle')), 0.016, SIZE, FLASH_SCALE), mul(sin(variable('angle')), 0.016, SIZE, FLASH_SCALE)],\n [variable('angle'), 0, -PI / 2],\n 0xffb347,\n ),\n ),\n flashPiece('sphere', mul(0.016, SIZE, FLASH_SCALE), undefined, 12, [mul(0.008, SIZE, FLASH_SCALE), 0, 0], [0, 0, 0], 0xfff1c0),\n ]),\n repeat(\n variable('puffs'),\n puff(\n ORIGIN,\n [add(variable('drift'), mul(rand(), variable('driftSpread'))), add(variable('rise'), mul(rand(), variable('riseSpread'))), mul(sub(rand(), 0.5), variable('sideSpread'))],\n [0, 0, 0],\n mul(variable('radius'), SIZE),\n add(variable('life'), mul(rand(), variable('lifeSpread'))),\n variable('opacity'),\n variable('growth'),\n ),\n ),\n ],\n}\n\n/**\n * One puff of smoke the gallery's `smoke(position, velocity, size, life, opacity)` throws: a hot barrel's, a rocket's trail, the smoke of a flame on the ground. The numbers come as\n * `values` (`vx`, `vy`, `vz` in the world, `radius`, `life`, `opacity`), so the weapon keeps its own random draws.\n */\nconst SMOKE_PUFF: EffectDef = {\n id: 'weapon_smoke_puff',\n pageId: 'event:weapon_smoke_puff',\n team: 'weapons',\n budget: { particles: 1 },\n loop: 0.1,\n timeline: [puff(ORIGIN, undefined, [variable('vx'), variable('vy'), variable('vz')], variable('radius'), variable('life'), variable('opacity'))],\n}\n\n/** A rocket leaving the tube, from the rear of it: 26 puffs thrown back (-x) and 14 bits of fire. */\nconst BACKBLAST: EffectDef = {\n id: 'launcher_backblast',\n pageId: 'event:launcher_backblast',\n team: 'weapons',\n budget: { particles: 40 },\n loop: 0.1,\n timeline: [\n repeat(26, puff(ORIGIN, [neg(add(1.5, mul(rand(), 3))), 0, 0], [wide(1.4), wide(0.8), wide(1.4)], add(0.03, mul(rand(), 0.03)), add(1.8, mul(rand(), 1.4)), 0.5)),\n repeat(14, ...flameBit(ORIGIN, [neg(add(2, mul(rand(), 2))), 0, 0], [wide(1), wide(0.6), wide(1)], 0.02, 0.25)),\n ],\n}\n\n/**\n * A frame of a flamethrower's jet: `floor(420 dt scale)` bits of fire thrown along the barrel and up (`dt` and `scale` are the values), and a puff of smoke downwind half the time.\n * It is played every frame the nozzle burns. It needs the `direction` of the barrel for the wind of the smoke.\n */\nconst FLAME_JET: EffectDef = {\n id: 'flame_jet',\n pageId: 'event:flame_jet',\n team: 'weapons',\n budget: { particles: 30 },\n loop: 0.1,\n timeline: [\n repeat(\n floor(mul(420, variable('dt'), variable('scale'))),\n set('speed', mul(add(2.2, mul(rand(), 1.1)), variable('scale'))),\n ...flameBit(\n [mul(rand(), variable('speed'), min(variable('dt'), 0.3)), 0, 0],\n [variable('speed'), 0, 0],\n [wide(0.35), mul(sub(rand(), 0.3), 0.3), wide(0.35)],\n add(0.02, mul(rand(), 0.022)),\n add(0.4, mul(rand(), 0.35)),\n ),\n ),\n when(lt(rand(), 0.5), [\n puff([add(mul(1.1, variable('scale')), mul(rand(), 0.4)), 0, 0], undefined, [mul(vx(DIRECTION), 0.6), 0.35, mul(vz(DIRECTION), 0.6)], 0.03, 1.6, 0.4),\n ]),\n ],\n}\n\nexport const VIEWMODEL_EFFECTS: readonly EffectDef[] = [MUZZLE_FLASH, SMOKE_PUFF, BACKBLAST, FLAME_JET]\n","import { CAR_ORIGIN } from './car'\nimport { ENGINE_SMOKE } from './engine-smoke'\nimport { FUEL_LEAK, FUEL_LEAK_ONLY } from './fuel-leak'\nimport type { EffectDef } from '../types'\n\n/**\n * The Garagem's recipes written around a car, to be played at a vehicle: the Workshop's `engine_smoke` and `fuel_leak` keep the car where the page stood\n * it (`CAR_ORIGIN`), so each alias names that spot as its `anchor` and the effect lands on the point it is played at. The values are the recipes' own;\n * only the id, the condition they start in and (for the exhaust) how long one play lasts change. Ids a vehicle module asks for: `vehicle_fire`, `vehicle_leak`, `vehicle_leak_ignited`, `vehicle_exhaust`.\n * The other vehicle requests use point-based entries that already exist: the explosion catalogue's `car_fuel_tank`, `glass_burst` and `impact_metal`.\n */\nconst aliasOf = (base: EffectDef, id: string, changes: Partial<EffectDef>): EffectDef => ({ ...base, id, pageId: `vehicle:${id}`, anchor: CAR_ORIGIN, ...changes })\n\nexport const VEHICLE_EFFECTS: readonly EffectDef[] = [\n /** The engine on fire: `engine_smoke` at its worst condition (black smoke, flames in the hood, a light). */\n aliasOf(ENGINE_SMOKE, 'vehicle_fire', { params: { id: 'condition', options: ['100', '60', '30', '10'], default: 3 }, loop: 12 }),\n /** The exhaust of a car under way: `engine_smoke` in the best condition, for half a second so a moving car asks again. */\n aliasOf(ENGINE_SMOKE, 'vehicle_exhaust', { params: { id: 'condition', options: ['100', '60', '30', '10'], default: 0 }, loop: 0.5 }),\n /** A hole in the tank: the page's petrol leak without the fire and the blast (the pool and the drips). A normal bullet does only this. */\n aliasOf(FUEL_LEAK_ONLY, 'vehicle_leak', {}),\n /** The leak caught fire (an incendiary or tracer round, a spark, a fire near): the page's whole sequence, the fire running along the pool to the tank and the blast. */\n aliasOf(FUEL_LEAK, 'vehicle_leak_ignited', {}),\n]\n","import { BLOATER_CLOUD } from './creatures/bloater-cloud'\nimport { BLOOD_HIT } from './creatures/blood-hit'\nimport { CORPSE_DECAY } from './creatures/corpse-decay'\nimport { EYES_IN_FOG } from './creatures/eyes-in-fog'\nimport { DISMEMBER } from './creatures/dismember'\nimport { HEADSHOT } from './creatures/headshot'\nimport { REANIMATION } from './creatures/reanimation'\nimport { SCREAMER_SHRIEK } from './creatures/screamer-shriek'\nimport { IMPACT_BY_MATERIAL } from './weapons/impact-by-material'\nimport { PENETRATION } from './weapons/penetration'\nimport { CASINGS_AND_MAGAZINE } from './weapons/casings-and-magazine'\nimport { MOLOTOV } from './weapons/molotov'\nimport { GRENADE } from './weapons/grenade'\nimport { EXPLOSIVE_BARREL } from './weapons/explosive-barrel'\nimport { MUZZLE_FLASH } from './weapons/muzzle-flash'\nimport { SHOPFRONT_GLASS } from './destruction/shopfront-glass'\nimport { DOOR_STAGES } from './destruction/door-stages'\nimport { BUILDING_COLLAPSE } from './destruction/building-collapse'\nimport { BLOCK_WALL } from './destruction/block-wall'\nimport { METAL_DOOR_EFFECT } from './destruction/metal-door'\nimport { IRON_RAILING } from './destruction/iron-railing'\nimport { WOODEN_FENCE } from './destruction/wooden-fence'\nimport { HIT_POINT_BREAKAGE } from './destruction/hit-point-breakage'\nimport { PRESSURE_CYLINDERS } from './destruction/pressure-cylinders'\nimport { BUILDING_FIRE } from './world/building-fire'\nimport { FOG_AND_ASH } from './world/fog-and-ash'\nimport { RAIN_AND_PUDDLES } from './world/rain'\nimport { HEADLIGHTS_IN_RAIN } from './vehicles/headlights'\nimport { SCRAPING_SPARKS } from './vehicles/scraping-sparks'\nimport { ENGINE_SMOKE } from './vehicles/engine-smoke'\nimport { FUEL_LEAK } from './vehicles/fuel-leak'\nimport { CAR_EXPLOSION } from './vehicles/car-explosion'\nimport { WINDSHIELD } from './vehicles/windshield'\nimport { DAMAGE_AND_BITE } from './screen/damage-and-bite'\nimport { INFECTION_PHASES } from './screen/infection-phases'\nimport { PANIC } from './screen/panic'\nimport { BLOATER_BLINDNESS } from './screen/bloater-blindness'\nimport { CAMPFIRE } from './screen/campfire'\nimport { COUGH_AND_BREATH } from './screen/cough-and-breath'\nimport { EVENT_EFFECTS } from './events'\nimport { VIEWMODEL_EFFECTS } from './weapons/viewmodel'\nimport { VEHICLE_EFFECTS } from './vehicles/game-effects'\nimport type { EffectDef } from './types'\n\nconst ALL_EFFECTS: readonly EffectDef[] = [BLOOD_HIT, HEADSHOT, DISMEMBER, BLOATER_CLOUD, SCREAMER_SHRIEK, CORPSE_DECAY, REANIMATION, EYES_IN_FOG, MUZZLE_FLASH, IMPACT_BY_MATERIAL, PENETRATION, CASINGS_AND_MAGAZINE, MOLOTOV, GRENADE, EXPLOSIVE_BARREL, SHOPFRONT_GLASS, DOOR_STAGES, BUILDING_COLLAPSE, BLOCK_WALL, METAL_DOOR_EFFECT, IRON_RAILING, WOODEN_FENCE, HIT_POINT_BREAKAGE, PRESSURE_CYLINDERS, BUILDING_FIRE, RAIN_AND_PUDDLES, FOG_AND_ASH, SCRAPING_SPARKS, ENGINE_SMOKE, FUEL_LEAK, CAR_EXPLOSION, WINDSHIELD, HEADLIGHTS_IN_RAIN, DAMAGE_AND_BITE, INFECTION_PHASES, PANIC, BLOATER_BLINDNESS, CAMPFIRE, COUGH_AND_BREATH]\n\n/** Every effect of the Workshop by id. A game adds, changes or removes entries with `patchEffects`. */\nexport const WORKSHOP_EFFECTS: Readonly<Record<string, EffectDef>> = Object.fromEntries(ALL_EFFECTS.map((effect) => [effect.id, effect]))\n\n/** The Workshop's recipes plus the one-shot effects the destruction events play (`event-effects.json`) and the ones of the weapon in the player's hands and the vehicle ones (`vehicle_fire`, `vehicle_exhaust`, `vehicle_leak`, written around a car): what a world plays by default. */\nexport const GAME_EFFECTS: Readonly<Record<string, EffectDef>> = { ...WORKSHOP_EFFECTS, ...Object.fromEntries([...EVENT_EFFECTS, ...VIEWMODEL_EFFECTS, ...VEHICLE_EFFECTS].map((effect) => [effect.id, effect])) }\n\n/** A copy of the recipes with some changed (a partial replaces the fields it names), added or removed (`null`); a game tunes the Workshop's effects without touching the core. */\nexport function patchEffects(base: Readonly<Record<string, EffectDef>>, changes: Readonly<Record<string, Partial<EffectDef> | EffectDef | null>>): Record<string, EffectDef> {\n const result: Record<string, EffectDef> = { ...base }\n for (const [id, change] of Object.entries(changes)) {\n if (change === null) delete result[id]\n else result[id] = { ...(base[id] ?? {}), ...change, id } as EffectDef\n }\n return result\n}\n","const LEHMER_MULTIPLIER = 16807\nconst LEHMER_MODULUS = 2147483647\nconst LCG_MULTIPLIER = 1664525\nconst LCG_INCREMENT = 1013904223\nconst LCG_RANGE = 4294967296\nconst HASH_MIX = 0x9e3779b1\n\nexport interface MutableVec3 {\n x: number\n y: number\n z: number\n}\n\n/** A seeded stream of numbers in [0, 1). The Lab's generator and the Fluid Standard's are both here, behind the same three calls. */\nexport interface RandomStream {\n /** The next number (the Lab's `rnd`). */\n next(): number\n /** The Lab's `rr(min, max)`: one draw, scaled. */\n between(min: number, max: number): number\n /** The Lab's `rdir`: a random unit vector, two draws (`u`, then the angle), written into `out`. */\n direction(out: MutableVec3): MutableVec3\n readonly state: number\n}\n\nfunction normalizeLehmerSeed(seed: number): number {\n const whole = Math.floor(Math.abs(seed)) % (LEHMER_MODULUS - 1)\n return whole === 0 ? 1 : whole\n}\n\n/** Park-Miller 16807 (the Lab's L16-18). A seed in 1..2147483646 is used as is, so a stream replays the page's. */\nexport function lehmer(seed: number): RandomStream {\n let state = normalizeLehmerSeed(seed)\n const next = (): number => {\n state = (state * LEHMER_MULTIPLIER) % LEHMER_MODULUS\n return (state - 1) / (LEHMER_MODULUS - 1)\n }\n return {\n next,\n between: (min, max) => min + (max - min) * next(),\n direction(out) {\n const up = next() * 2 - 1\n const angle = next() * Math.PI * 2\n const ring = Math.sqrt(1 - up * up)\n out.x = ring * Math.cos(angle)\n out.y = up\n out.z = ring * Math.sin(angle)\n return out\n },\n get state() {\n return state\n },\n }\n}\n\n/** The Fluid Standard's generator (F17-18): a 32-bit linear congruential one. */\nexport function lcg(seed: number): RandomStream {\n let state = seed >>> 0\n const next = (): number => {\n state = (Math.imul(state, LCG_MULTIPLIER) + LCG_INCREMENT) >>> 0\n return state / LCG_RANGE\n }\n return {\n next,\n between: (min, max) => min + (max - min) * next(),\n direction(out) {\n const up = next() * 2 - 1\n const angle = next() * Math.PI * 2\n const ring = Math.sqrt(1 - up * up)\n out.x = ring * Math.cos(angle)\n out.y = up\n out.z = ring * Math.sin(angle)\n return out\n },\n get state() {\n return state\n },\n }\n}\n\nfunction mixEventSeed(eventSeed: number): number {\n let mixed = Math.imul((eventSeed | 0) ^ HASH_MIX, 0x85ebca6b)\n mixed ^= mixed >>> 13\n mixed = Math.imul(mixed, 0xc2b2ae35)\n mixed ^= mixed >>> 16\n return (mixed >>> 0) % (LEHMER_MODULUS - 1) + 1\n}\n\n/** The stream of one event: the same event seed draws the same effect on every machine, whatever else is playing (`randomMode: 'per_event'`). */\nexport function streamFor(eventSeed: number): RandomStream {\n return lehmer(mixEventSeed(eventSeed))\n}\n","import type { ImpactMaterial } from './blood'\n\n/** What each material raises when hit: dust colour, chip colour and the number of sparks (W374-381). Zero means none. */\nconst TABLE: Readonly<Record<ImpactMaterial, { readonly dust: number; readonly chip: number; readonly spark: number }>> = {\n concrete: { dust: 0x8a857a, chip: 0x6e6a63, spark: 2 },\n metal: { dust: 0, chip: 0, spark: 18 },\n wood: { dust: 0x6a5038, chip: 0x7a5530, spark: 0 },\n glass: { dust: 0xbfd6e2, chip: 0xa8c8d8, spark: 0 },\n flesh: { dust: 0, chip: 0, spark: 0 },\n water: { dust: 0, chip: 0, spark: 0 },\n}\n\nexport function sparkSpec(material: ImpactMaterial): { readonly dust: number; readonly chip: number; readonly spark: number } {\n return TABLE[material]\n}\n","import type { WorkshopEngine } from '../../workshop/engine'\nimport type { Vec3Tuple } from '../../workshop/types'\nimport { negated, plus, plusScaled } from '../vector-math'\nimport { sparkSpec } from './impact-table'\n\n/** The blood colour of each infection age (W116) and how much each bleeds (W392). */\nexport const BLOOD_COLORS = [0x8a0a0a, 0x5a0c10, 0x3a1c12, 0x1d1914] as const\nconst BLEED_BY_AGE = [1, 0.7, 0.35, 0.1] as const\n\n/** The dust and dry flakes a decrepit body gives instead of blood. */\nfunction drySplash(engine: WorkshopEngine, point: Vec3Tuple, exitDirection: Vec3Tuple, scale: number): void {\n engine.burst('normal', 16 * scale, { p: point, dir: exitDirection, cone: 1, sp: [0.5, 1.6], drag: 2, life: [0.9, 1.6], size: [0.1, 0.2], grow: 4, c0: 0x2e2822, c1: 0x15130f, a: 0.6, g: -0.15 })\n for (let index = 0; index < engine.scaled(5); index++) engine.debrisPiece({ p: point, dir: exitDirection, cone: 1, sp: [0.5, 2], size: [0.03, 0.008, 0.03], c: 0x1c1916, life: 6 })\n engine.decal([point[0], 0, point[2] + exitDirection[2] * 0.6, 0.25, 0x1a1714, { grow: 0.4, ar: 0.7 }])\n}\n\n/** `bloodHit` (W392-398): a mist at the entry, a jet out of the exit and a cloud, by infection age. */\nexport function bloodHit(engine: WorkshopEngine, point: Vec3Tuple, exitDirection: Vec3Tuple, age: number, scale = 1): void {\n if (age === 3) return drySplash(engine, point, exitDirection, scale)\n const amount = BLEED_BY_AGE[age]!\n const color = BLOOD_COLORS[age]!\n engine.burst('normal', 10 * amount * scale, { p: point, dir: negated(exitDirection), cone: 0.9, sp: [0.2, 0.9], drag: 3, life: [0.25, 0.5], size: [0.08, 0.14], grow: 3, c0: color, a: 0.65 })\n engine.burst('normal', 45 * amount * scale, { p: point, dir: exitDirection, cone: 0.45, sp: [2.5, 6], life: [1, 1.6], size: [0.035, 0.07], c0: color, a: 0.95, g: 9.8, fi: 0, gnd: 3, ds: [0.06, 0.16] })\n engine.burst('normal', 14 * amount * scale, { p: point, dir: exitDirection, cone: 0.5, sp: [1, 2.5], drag: 2.5, life: [0.4, 0.8], size: [0.1, 0.18], grow: 3, c0: color, a: 0.45 })\n}\n\nconst MATERIAL_NAMES = ['concrete', 'metal', 'wood', 'glass', 'flesh', 'water'] as const\nexport type ImpactMaterial = (typeof MATERIAL_NAMES)[number]\n\nfunction waterImpact(engine: WorkshopEngine, point: Vec3Tuple, scale: number): void {\n engine.burst('normal', 22 * scale, { p: point, dir: [0, 1, 0], cone: 0.5, sp: [1.5, 3.5], life: [0.4, 0.7], size: [0.03, 0.06], c0: 0xc8d6e6, a: 0.7, g: 9.8, fi: 0, gnd: 1 })\n engine.ring([plus(point, 0, 0.012, 0), 0.02, 0.6, 0.7, 0x8090a8])\n engine.ring([plus(point, 0, 0.013, 0), 0.02, 0.35, 0.5, 0x8090a8])\n}\n\n/** `impact` (W374-391): what a bullet raises on each material. */\nexport function impact(engine: WorkshopEngine, material: ImpactMaterial, point: Vec3Tuple, normal: Vec3Tuple, scale = 1, age = 0): void {\n if (material === 'flesh') return bloodHit(engine, point, negated(normal), age, scale)\n if (material === 'water') return waterImpact(engine, point, scale)\n const spec = sparkSpec(material)\n if (spec.dust) engine.burst('normal', 14 * scale, { p: point, dir: normal, cone: 0.6, sp: [0.6, 2.4], drag: 2.5, life: [0.6, 1.2], size: [0.08, 0.16], grow: 4, c0: spec.dust, c1: 0x3a3936, a: 0.55, g: -0.1 })\n if (spec.chip) {\n const size = material === 'wood' ? ([0.015, 0.015, 0.09] as const) : material === 'glass' ? ([0.03, 0.004, 0.03] as const) : ([0.03, 0.03, 0.03] as const)\n for (let index = 0; index < engine.scaled(material === 'wood' ? 8 : 6) * scale; index++) {\n engine.debrisPiece({ p: point, dir: normal, cone: 0.8, sp: [1.5, 4], size, k: [0.6, 1.4], c: spec.chip, life: 6 })\n }\n }\n if (spec.spark && engine.quality < 2) {\n engine.burst('streak', spec.spark * scale, { p: point, dir: normal, cone: material === 'metal' ? 0.9 : 0.5, sp: [3, 9], life: [0.15, 0.45], len: 0.025, c0: 0xfff0c0, c1: 0xff5a10, g: 9.8, gnd: 2 })\n engine.burst('add', material === 'metal' ? 3 : 1, { p: point, size: [0.12, 0.2], life: 0.06, c0: 0xffe0a0, fi: 0 })\n if (material === 'metal') engine.flash([plusScaled(point, normal, 0.2), 0xffb060, 2, 0.06, 5])\n }\n if (material === 'glass') engine.burst('add', 5, { p: point, r: 0.15, size: [0.02, 0.05], life: [0.2, 0.5], c0: 0xffffff, a: 0.9, fi: 0.3 })\n if (engine.quality < 2) {\n const size = material === 'glass' ? 0.07 : material === 'metal' ? 0.05 : 0.06\n engine.decal([point[0], point[1], point[2], size, material === 'metal' ? 0x2a2522 : 0x0e0d0c, { n: normal, grow: 0, life: 60 }])\n }\n}\n\nexport function impactMaterialAt(index: number): ImpactMaterial {\n return MATERIAL_NAMES[index]!\n}\n","import { impact } from '../builders/blood'\nimport type { Behavior } from '../behavior'\n\nconst COLUMNS = 6\nconst ROWS = 5\nconst ORIGIN = [-2.2, 0, 2.6] as const\nconst BLOCK_SIZE = [0.5, 0.44, 0.25] as const\nconst OBJECT_ID_DRAWS = 4\nconst FLOOR = 0.13\nconst QUARTER_TURN = 1.5708\n\ninterface Motion {\n vx: number\n vy: number\n vz: number\n wx: number\n wy: number\n wz: number\n rest: boolean\n}\n\nexport interface WallBlock {\n readonly home: readonly [number, number]\n x: number\n y: number\n rotation: [number, number, number]\n /** Bullet holes stuck to this block (local to the block). */\n holes: { x: number; y: number; scale: number; chipTurn: number; chipScale: number }[]\n motion: Motion | undefined\n}\n\nconst clamp = (value: number, low: number, high: number): number => (value < low ? low : value > high ? high : value)\n\nfunction createBlocks(): WallBlock[] {\n const blocks: WallBlock[] = []\n for (let row = 0; row < ROWS; row++) {\n for (let column = 0; column < COLUMNS; column++) {\n const home = [(column - 2.5) * BLOCK_SIZE[0] + (row % 2 ? 0.12 : 0), 0.22 + row * BLOCK_SIZE[1]] as const\n blocks.push({ home, x: home[0], y: home[1], rotation: [0, 0, 0], holes: [], motion: undefined })\n }\n }\n return blocks\n}\n\n/**\n * The free-standing wall of 30 blocks of the page (W926-935, W1039-1046): a bullet leaves a hole and a chip stuck to the nearest block (`bwHit`, which\n * also costs the page the ids three.js gives each new mesh), and when it gives way the blocks fall with the velocities the page draws for them and\n * rest on their sides. The holes travel with the blocks, which is the point of the effect.\n */\nexport function createBlocksBehavior(): Behavior {\n let blocks = createBlocks()\n return {\n reset() {\n blocks = createBlocks()\n },\n act(host, action, args) {\n const random = host.engine.random\n if (action === 'hit') {\n const { x, y, scale } = args as { readonly x: number; readonly y: number; readonly scale: number }\n let best: WallBlock | undefined\n let bestDistance = 9\n for (const block of blocks) {\n const distance = Math.hypot(block.x - x, block.y - y)\n if (distance < bestDistance) {\n bestDistance = distance\n best = block\n }\n }\n if (!best) return\n const offsetX = clamp(x - best.x, -0.2, 0.2)\n const offsetY = clamp(y - best.y, -0.17, 0.17)\n for (let draw = 0; draw < OBJECT_ID_DRAWS; draw++) random.next()\n const chipTurn = random.between(0, 6)\n const chipScale = random.between(0.8, 1.3) * scale\n for (let draw = 0; draw < OBJECT_ID_DRAWS; draw++) random.next()\n best.holes.push({ x: offsetX, y: offsetY, scale, chipTurn, chipScale })\n impact(host.engine, 'concrete', [ORIGIN[0] + x, y, ORIGIN[2] + 0.13], [0, 0, 1], 0.8)\n } else if (action === 'collapse') {\n const { x, y } = args as { readonly x: number; readonly y: number }\n for (const block of blocks) {\n const dx = block.x - x\n const dy = block.y - y\n block.motion = {\n vx: dx * random.between(0.8, 2),\n vy: random.between(0.5, 2.5) + dy * 0.5,\n vz: random.between(-2.2, -0.4),\n wx: random.between(-4, 4),\n wy: random.between(-3, 3),\n wz: random.between(-4, 4),\n rest: false,\n }\n }\n }\n },\n query(_host, name) {\n if (name === 'blocks') return blocks\n throw new Error(`blocks: unknown query ${name}`)\n },\n update(_host, dt) {\n for (const block of blocks) {\n const motion = block.motion\n if (!motion || motion.rest) continue\n motion.vy -= 9.8 * dt\n block.x += motion.vx * dt\n block.y += motion.vy * dt\n block.rotation[0] += motion.wx * dt\n block.rotation[1] += motion.wy * dt\n block.rotation[2] += motion.wz * dt\n if (block.y < FLOOR) {\n block.y = FLOOR\n motion.vy *= -0.25\n motion.vx *= 0.5\n motion.vz *= 0.5\n motion.wx *= 0.5\n motion.wy *= 0.5\n motion.wz *= 0.5\n if (Math.abs(motion.vy) < 0.4 && Math.hypot(motion.vx, motion.vz) < 0.3) {\n motion.rest = true\n block.rotation[0] = Math.round(block.rotation[0] / QUARTER_TURN) * QUARTER_TURN\n block.rotation[2] = Math.round(block.rotation[2] / QUARTER_TURN) * QUARTER_TURN\n }\n }\n }\n },\n }\n}\n","import type { Behavior } from '../behavior'\nimport { carPoint } from '../vehicles/car'\nimport type { Vec3Tuple } from '../../workshop/types'\n\nconst ORIGIN: Vec3Tuple = [5.6, 0, 0.8]\nconst MAX_TILT = 0.25\n\n/**\n * The car of the page after it blew up (`carTick`, W1131): it leaps, spins and lands crooked. Its height is what the burning wreck's fire is emitted\n * from, so the model keeps it; the body is drawn by whoever owns the car.\n */\nexport function createCarBehavior(): Behavior {\n let y = 0\n let rise: number | undefined\n let spin = 0\n let roll = 0\n return {\n reset() {\n y = 0\n rise = undefined\n spin = 0\n roll = 0\n },\n act(_host, action, args) {\n if (action === 'launch') {\n const launch = args as { readonly rise: number; readonly spin: number }\n rise = launch.rise\n spin = launch.spin\n } else if (action === 'step') {\n if (rise === undefined) return\n const dt = (args as { readonly dt: number }).dt\n rise -= 9.8 * dt\n y = Math.max(0, y + rise * dt)\n roll += spin * dt\n spin *= Math.exp(-dt * 2)\n if (y === 0 && rise < 0) {\n rise = undefined\n roll = Math.min(MAX_TILT, Math.max(-MAX_TILT, roll))\n }\n }\n },\n query(_host, name, args) {\n if (name === 'position') return [ORIGIN[0], y, ORIGIN[2]]\n // `carW(x, y, z)` (W104): a point of the car's own space in the world, with the car where it is now\n if (name === 'point') {\n const [x, py, z] = args as readonly [number, number, number]\n const point = carPoint(x, py, z)\n return [point[0], point[1] + y, point[2]]\n }\n if (name === 'roll') return roll\n throw new Error(`car: unknown query ${name}`)\n },\n update() {},\n }\n}\n","import type { Behavior } from '../behavior'\n\n/** A smear of blood that runs down the windshield from a hit (`wsMarks`, W1156). */\nexport interface BloodStreak {\n readonly x: number\n readonly y: number\n readonly dx: number\n readonly dy: number\n readonly width: number\n}\n\nexport interface Crack {\n readonly x: number\n readonly y: number\n /** Each ray is a list of `[dx, dy]` segments that start at the impact. */\n readonly rays: readonly (readonly (readonly [number, number])[])[]\n readonly rings: readonly number[]\n}\n\nconst FULL_TURN_ROUNDED = 6.28\nconst SHORT_RAYS = 7\nconst LONG_RAYS = 12\nconst STREAKS_PER_HIT = 5\n\n/** `crackAt` (W62-63): the star of cracks a hit leaves in glass. Its draws depend on the lengths it draws, so the model keeps the same loop. */\nexport function createCracksBehavior(): Behavior {\n let cracks: Crack[] = []\n let streaks: BloodStreak[][] = []\n return {\n reset() {\n cracks = []\n streaks = []\n },\n act(host, action, args) {\n if (action === 'blood_streaks') {\n const { x, y } = args as { readonly x: number; readonly y: number }\n const random = host.engine.random\n const group: BloodStreak[] = []\n for (let index = 0; index < STREAKS_PER_HIT; index++) {\n group.push({ x: x - 18 + index * 8, y: y - 10, dx: random.between(-3, 3), dy: random.between(18, 40), width: random.between(5, 9) })\n }\n streaks.push(group)\n return\n }\n if (action !== 'crack') return\n const { x, y, big } = args as { readonly x: number; readonly y: number; readonly big: boolean }\n const random = host.engine.random\n const count = big ? LONG_RAYS : SHORT_RAYS\n const rays: [number, number][][] = []\n for (let ray = 0; ray < count; ray++) {\n let angle = (ray / count) * FULL_TURN_ROUNDED + random.between(-0.3, 0.3)\n const segments: [number, number][] = []\n let remaining = big ? random.between(40, 110) : random.between(15, 45)\n while (remaining > 0) {\n const length = random.between(6, 14)\n angle += random.between(-0.35, 0.35)\n segments.push([Math.cos(angle) * length, Math.sin(angle) * length])\n remaining -= length\n }\n rays.push(segments)\n }\n cracks.push({ x, y, rays, rings: big ? [12, 26] : [8] })\n },\n query(_host, name) {\n if (name === 'cracks') return cracks\n if (name === 'streaks') return streaks\n throw new Error(`cracks: unknown query ${name}`)\n },\n update() {},\n }\n}\n","import type { WorkshopEngine } from '../../workshop/engine'\nimport type { Vec3Tuple } from '../../workshop/types'\n\n/** Wood chips and a puff of dust (`chips`, W849). */\nexport function chips(engine: WorkshopEngine, point: Vec3Tuple, direction: Vec3Tuple, count: number, spread?: number): void {\n for (let index = 0; index < engine.scaled(count); index++) {\n engine.debrisPiece({ p: point, dir: direction, cone: spread || 0.9, sp: [1, 3], size: [0.012, 0.012, 0.09], k: [0.6, 1.4], c: 0xb08a5a, life: 20 })\n }\n engine.burst('normal', count, { p: point, dir: direction, cone: 0.8, sp: [0.3, 1.2], drag: 2, life: [0.8, 1.4], size: [0.08, 0.15], grow: 3, c0: 0x7a6048, a: 0.4 })\n}\n\n/** Sparks off metal (`sparkAt`, W899). */\nexport function sparkAt(engine: WorkshopEngine, point: Vec3Tuple, direction: Vec3Tuple, count: number, spread?: number): void {\n engine.burst('streak', count, { p: point, dir: direction, cone: spread || 0.8, sp: [2.5, 7], life: [0.2, 0.5], len: 0.025, c0: 0xfff0c0, c1: 0xff5010, g: 9.8, gnd: 2 })\n engine.burst('add', 2, { p: point, size: [0.1, 0.18], life: 0.06, c0: 0xffd8a0, fi: 0 })\n}\n\n/** Rust flakes and a little dust (`rustAt`, W900). */\nexport function rustAt(engine: WorkshopEngine, point: Vec3Tuple, count: number): void {\n engine.burst('normal', count, { p: point, r: 0.1, sp: [0.1, 0.5], life: [1, 1.6], size: [0.02, 0.035], c0: 0x7a4a2a, a: 0.9, g: 6, fi: 0, gnd: 1 })\n engine.burst('normal', Math.ceil(count / 2), { p: point, r: 0.08, sp: [0.2, 0.5], drag: 2, life: [0.8, 1.3], size: [0.08, 0.14], grow: 3, c0: 0x6a4a34, a: 0.3 })\n}\n","import type { Behavior, BehaviorHost } from '../behavior'\nimport { sparkAt } from '../builders/shared'\nimport { normalized } from '../vector-math'\nimport type { Vec3Tuple } from '../../workshop/types'\n\nconst SPOT: Vec3Tuple = [-2.2, 0, 2.6]\nconst WALL_X = -8.2\nconst QUARTER_TURN = Math.PI / 2\n\nconst lerp = (from: number, to: number, amount: number): number => from + (to - from) * amount\nconst clamp = (value: number, low: number, high: number): number => (value < low ? low : value > high ? high : value)\n\n/**\n * The pressure cylinder of the page (W1021-1038): an extinguisher that spins on the floor pushed by its own jet, or an oxygen bottle that becomes a\n * rocket and hits the wall. `step` moves it the way the page's tick does, drawing the sparks of the wall hit; the recipe then asks where the jet comes\n * out (`tip`, `back`, `direction`) to emit it.\n */\nexport function createCylinderBehavior(): Behavior {\n let position: Vec3Tuple = SPOT\n let velocity: Vec3Tuple = [0, 0, 0]\n let rotationZ = 0\n let angle = 0\n let spin = 0\n let live = false\n let oxygen = false\n let direction: Vec3Tuple = [1, 0, 0]\n\n const clear = (): void => {\n position = SPOT\n velocity = [0, 0, 0]\n rotationZ = 0\n angle = 0\n spin = 0\n live = false\n direction = [1, 0, 0]\n }\n\n return {\n reset: clear,\n act(host: BehaviorHost, action: string, args: unknown) {\n if (action === 'choose') {\n clear()\n oxygen = (args as { readonly oxygen: boolean }).oxygen\n } else if (action === 'puncture') {\n live = true\n if (oxygen) velocity = [-5, 0, -0.3]\n else spin = 9\n } else if (action === 'step') {\n if (!live) return\n const { dt, t } = args as { readonly dt: number; readonly t: number }\n const k = clamp(1 - (t - 0.6) / 3.4, 0, 1)\n if (!oxygen) {\n rotationZ = lerp(rotationZ, QUARTER_TURN, dt * 8)\n const y = lerp(position[1], 0.09, dt * 8)\n angle += spin * k * dt\n direction = [Math.cos(angle), 0, -Math.sin(angle)]\n position = [position[0] + direction[0] * (-0.6 * k * dt), y + direction[1] * (-0.6 * k * dt), position[2] + direction[2] * (-0.6 * k * dt)]\n return\n }\n let [vx, vy, vz] = velocity\n if (k > 0) vx -= 4 * k * dt\n const damping = Math.exp(-dt * (k > 0 ? 0.3 : 3))\n vx *= damping\n vy *= damping\n vz *= damping\n velocity = [vx, vy, vz]\n rotationZ = lerp(rotationZ, -QUARTER_TURN, dt * 10)\n position = [position[0] + vx * dt, lerp(position[1], 0.12, dt * 10) + vy * dt, position[2] + vz * dt]\n if (position[0] < WALL_X) {\n position = [WALL_X, position[1], position[2]]\n velocity = [velocity[0] * -0.3, velocity[1], velocity[2]]\n host.engine.screen.shake += 0.08\n sparkAt(host.engine, position, normalized([1, 0.5, 0]), 16, 1)\n }\n }\n },\n query(_host, name) {\n switch (name) {\n case 'live': return live\n case 'oxygen': return oxygen\n case 'position': return position\n case 'direction': return direction\n case 'speed_x': return velocity[0]\n case 'rotation_z': return rotationZ\n case 'tip': return [position[0] + direction[0] * 0.3, 0.1, position[2] + direction[2] * 0.3]\n case 'back': return [position[0] + 1.1, position[1], position[2]]\n default: throw new Error(`cylinder: unknown query ${name}`)\n }\n },\n update() {},\n }\n}\n","import type { Behavior } from '../behavior'\n\n/** The geometry of a box plate as three.js lays out its vertices: `BoxGeometry(width, height, depth, columns, rows, 1)` moved by `(shiftX, shiftY)`. */\nexport interface PlateGeometry {\n readonly width: number\n readonly height: number\n readonly columns: number\n readonly rows: number\n readonly shiftX: number\n readonly shiftY: number\n}\n\n/** The metal door of the page: 1.1 x 2.2 m, 14 x 24 segments, hinged at its left edge (W960). */\nexport const METAL_DOOR: PlateGeometry = { width: 1.1, height: 2.2, columns: 14, rows: 24, shiftX: 0.56, shiftY: 1.1 }\n\n/** Every vertex of the plate seen from the front (x, y), the front and back faces and the four thin edges, each vertex once per face it belongs to. */\nexport function plateVertices(geometry: PlateGeometry): readonly (readonly [number, number])[] {\n const { width, height, columns, rows, shiftX, shiftY } = geometry\n const xs = Array.from({ length: columns + 1 }, (_, column) => (column * width) / columns - width / 2)\n const ys = Array.from({ length: rows + 1 }, (_, row) => (row * height) / rows - height / 2)\n const vertices: [number, number][] = []\n // the sides (px, nx): two vertices, front and back, for each row; then top and bottom (py, ny): the same for each column; then the two big faces\n for (const x of [width / 2, -width / 2]) for (const y of ys) for (let depth = 0; depth < 2; depth++) vertices.push([x, y])\n for (const y of [height / 2, -height / 2]) for (const x of xs) for (let depth = 0; depth < 2; depth++) vertices.push([x, y])\n for (let face = 0; face < 2; face++) for (const y of ys) for (const x of xs) vertices.push([x, y])\n return vertices.map(([x, y]) => [x + shiftX, y + shiftY] as const)\n}\n\ninterface DentArgs {\n readonly x: number\n readonly y: number\n readonly radius: number\n readonly depth: number\n}\n\n/**\n * `dentAt` (W1002): pushes the vertices within `radius` of a hit in and draws one random number for each, so how many it draws depends on how the plate\n * is cut. The model counts them; the deformed mesh belongs to whoever draws the door.\n */\nlet doorVertices: ReturnType<typeof plateVertices> | undefined\n\nexport function createPlateBehavior(geometry: PlateGeometry = METAL_DOOR): Behavior {\n const vertices = geometry === METAL_DOOR ? (doorVertices ??= plateVertices(geometry)) : plateVertices(geometry)\n let dents: { x: number; y: number; radius: number; depth: number; vertices: number }[] = []\n return {\n reset() {\n dents = []\n },\n act(host, action, args) {\n if (action !== 'dent') return\n const { x, y, radius, depth } = args as DentArgs\n let touched = 0\n for (const [vertexX, vertexY] of vertices) {\n if (Math.hypot(vertexX - x, vertexY - y) < radius) {\n host.engine.random.next()\n touched++\n }\n }\n dents.push({ x, y, radius, depth, vertices: touched })\n },\n query(_host, name) {\n if (name === 'dents') return dents\n throw new Error(`plate: unknown query ${name}`)\n },\n update() {},\n }\n}\n","import type { Behavior, BehaviorHost } from '../behavior'\n\ninterface InitArgs { readonly count: number; readonly wind: number }\ninterface UpdateArgs { readonly dt: number; readonly splash: boolean }\n\n/**\n * The page's rain (`initRain` and `updateRain`, W321-331) as a prop an effect starts and ticks: `init` fills the box with streaks, `update` moves them and\n * leaves the splashes. The field itself lives in the engine (`engine.rain`), where the renderer reads it; the weather package drives the same field.\n */\nexport function createRainBehavior(): Behavior {\n return {\n reset() {},\n act(host: BehaviorHost, action: string, args: unknown) {\n const { engine, camera } = host\n if (action === 'init') {\n const { count, wind } = args as InitArgs\n engine.rain.init(count, wind)\n } else if (action === 'update') {\n const { dt, splash } = args as UpdateArgs\n engine.rain.update(dt, splash, camera.position)\n }\n },\n query(host, name) {\n if (name === 'count') return host.engine.rain.count\n if (name === 'on') return host.engine.rain.on\n throw new Error(`rain: unknown query ${name}`)\n },\n update() {},\n }\n}\n","import type { Behavior } from '../behavior'\n\nconst WALKER_COUNT = 10\nconst ARRIVAL_DISTANCE = 0.05\nconst SWAY_AMPLITUDE = 0.04\nconst BOB_AMPLITUDE = 0.04\nconst STRIDE_FACTOR = 4.5\n\n/**\n * The `ph` of each walker, the offset of its sway and stride. The page drew them at load (`Math.random() * 6`) after three.js had drawn a few hundred\n * numbers for object ids, so they cannot be replayed; the three that matter (the runners with red eyes, whose trails start at the eyes) were fitted to\n * the recording until every position matched to four decimals. The others only turn a zombie nobody reads.\n */\nconst PHASES = [0, 0, 3.59629, 0, 0, 0, 2.946505, 0, 0, 0, 2.44693] as const\n\ninterface Walker {\n x: number\n z: number\n facing: number\n bob: number\n sway: number\n walk: { toX: number; toZ: number; speed: number } | undefined\n}\n\nfunction createWalkers(): Walker[] {\n return Array.from({ length: WALKER_COUNT }, () => ({ x: 0, z: 0, facing: 0, bob: 0, sway: 0, walk: undefined }))\n}\n\ninterface PlaceArgs { readonly index: number; readonly x: number; readonly z: number; readonly facing: number }\ninterface WalkArgs { readonly index: number; readonly to: readonly [number, number, number]; readonly speed: number }\ninterface MoveArgs { readonly index: number; readonly x: number }\n\n/**\n * The ten walking zombies of the page (W151, W190-197): they walk to a point at a speed, face where they go, bob with each stride and sway. An effect\n * reads where their eyes are (`point`) to start a trail from them. Phases are the page's, so the sway and bob repeat to the last digit.\n */\nexport function createWalkersBehavior(phases: readonly number[] = PHASES): Behavior {\n let walkers = createWalkers()\n return {\n reset() {\n walkers = createWalkers()\n },\n act(_host, action, args) {\n if (action === 'place') {\n const { index, x, z, facing } = args as PlaceArgs\n Object.assign(walkers[index]!, { x, z, facing })\n } else if (action === 'walk') {\n const { index, to, speed } = args as WalkArgs\n walkers[index]!.walk = { toX: to[0], toZ: to[2], speed }\n } else if (action === 'move_x') {\n const { index, x } = args as MoveArgs\n walkers[index]!.x = x\n }\n },\n query(_host, name, args) {\n const walker = walkers[args[0] as number]!\n if (name === 'x') return walker.x\n if (name === 'z') return walker.z\n if (name === 'is_walking') return walker.walk !== undefined\n if (name !== 'point') throw new Error(`walkers: unknown query ${name}`)\n const [localX, localY, localZ] = args[1] as readonly [number, number, number]\n const swayedX = Math.cos(walker.sway) * localX - Math.sin(walker.sway) * localY\n const swayedY = Math.sin(walker.sway) * localX + Math.cos(walker.sway) * localY + walker.bob\n return [Math.cos(walker.facing) * swayedX + Math.sin(walker.facing) * localZ + walker.x, swayedY, -Math.sin(walker.facing) * swayedX + Math.cos(walker.facing) * localZ + walker.z]\n },\n update(_host, dt, elapsed) {\n walkers.forEach((walker, index) => {\n walker.sway = Math.sin(elapsed * 1.3 + phases[index + 1]!) * SWAY_AMPLITUDE\n const walk = walker.walk\n if (!walk) return\n const dx = walk.toX - walker.x\n const dz = walk.toZ - walker.z\n const distance = Math.hypot(dx, dz)\n if (distance <= ARRIVAL_DISTANCE) {\n walker.walk = undefined\n return\n }\n const step = Math.min(distance, walk.speed * dt)\n walker.x += (dx / distance) * step\n walker.z += (dz / distance) * step\n walker.facing = Math.atan2(dx, dz)\n walker.bob = Math.abs(Math.cos(elapsed * walk.speed * STRIDE_FACTOR + phases[index + 1]!)) * BOB_AMPLITUDE\n })\n },\n}\n}\n","import type { Behavior } from './behavior'\nimport { createBlocksBehavior } from './behaviors/blocks'\nimport { createCarBehavior } from './behaviors/car'\nimport { createCracksBehavior } from './behaviors/cracks'\nimport { createCylinderBehavior } from './behaviors/cylinder'\nimport { createPlateBehavior } from './behaviors/plate'\nimport { createRainBehavior } from './behaviors/rain'\nimport { createWalkersBehavior } from './behaviors/walkers'\n\n/** How to build each prop the Workshop animates by hand, by id (see `Behavior`). A game adds its own the same way. */\nexport const WORKSHOP_BEHAVIORS: Readonly<Record<string, () => Behavior>> = {\n walkers: () => createWalkersBehavior(),\n cracks: () => createCracksBehavior(),\n blocks: () => createBlocksBehavior(),\n plate: () => createPlateBehavior(),\n cylinder: () => createCylinderBehavior(),\n car: () => createCarBehavior(),\n rain: () => createRainBehavior(),\n}\n\n/** A fresh set of the props the Workshop animates by hand, by id; one per runner, since they keep state. */\nexport function createWorkshopBehaviors(factories: Readonly<Record<string, () => Behavior>> = WORKSHOP_BEHAVIORS): Record<string, Behavior> {\n return Object.fromEntries(Object.entries(factories).map(([id, make]) => [id, make()]))\n}\n","export interface Schedule {\n /** Runs `action` once `now` reaches `time`. */\n at(time: number, action: () => void): void\n /** Runs what is due at `now`, from the last one added to the first (the Lab's L1085 and the Workshop's W1280 walk the list backwards, and so does this). */\n update(now: number): void\n clear(): void\n readonly pending: number\n}\n\ninterface Entry {\n time: number\n action: () => void\n}\n\nconst NOOP = (): void => {}\n\nexport function createSchedule(): Schedule {\n const entries: Entry[] = []\n const spare: Entry[] = []\n return {\n at(time, action) {\n const entry = spare.pop() ?? { time: 0, action: NOOP }\n entry.time = time\n entry.action = action\n entries.push(entry)\n },\n update(now) {\n for (let index = entries.length - 1; index >= 0; index--) {\n const entry = entries[index]\n if (!entry || entry.time > now) continue\n const action = entry.action\n entries.splice(index, 1)\n entry.action = NOOP\n spare.push(entry)\n action()\n }\n },\n clear() {\n for (const entry of entries) {\n entry.action = NOOP\n spare.push(entry)\n }\n entries.length = 0\n },\n get pending() {\n return entries.length\n },\n }\n}\n","import type { RandomStream } from '../random'\nimport { normalized } from './vector-math'\n\n/** A value an effect computes: a number, a flag, a name or a vector. */\nexport type EffectValue = number | boolean | string | readonly number[]\n\n/** A computed field of a recipe. Plain data: `{ $: 'add', a: [1, { $: 't' }] }`. Built with the helpers in `dsl.ts`. */\nexport interface Expr {\n readonly $: string\n readonly a?: readonly unknown[]\n}\n\nexport interface ExprContext {\n readonly random: RandomStream\n readonly param: number\n readonly time: number\n readonly dt: number\n readonly index: number\n readonly variables: ReadonlyMap<string, unknown>\n readonly scaled: (count: number) => number\n readonly quality: number\n /** True on the first run after the effect was chosen, false on its repeats (the page's `fresh`). */\n readonly fresh: boolean\n /** Which way the played effect points and which surface it hit, in the world (`play`'s `direction` and `normal`). */\n readonly direction: readonly number[]\n readonly normal: readonly number[]\n readonly query: (behavior: string, name: string, args: readonly unknown[]) => unknown\n readonly camera: { readonly position: readonly number[]; readonly forward: (local: readonly number[]) => readonly number[] }\n}\n\nexport function isExpr(value: unknown): value is Expr {\n return typeof value === 'object' && value !== null && !Array.isArray(value) && '$' in value\n}\n\ntype Vector = readonly number[]\nconst asNumber = (value: unknown): number => value as number\nconst asVector = (value: unknown): Vector => value as Vector\nconst EASE_LIMIT = 1\n\nfunction ease(value: number): number {\n return value < 0 ? 0 : value > EASE_LIMIT ? EASE_LIMIT : value * value * (3 - 2 * value)\n}\n\nfunction reduceVectors(operands: readonly unknown[], combine: (accumulated: number, next: number) => number): Vector {\n const [first, ...rest] = operands.map(asVector)\n return rest.reduce<Vector>((total, next) => [combine(total[0]!, next[0]!), combine(total[1]!, next[1]!), combine(total[2]!, next[2]!)], first!)\n}\n\nfunction evaluateCall(name: string, args: readonly unknown[]): unknown {\n const [first, second, third] = args\n switch (name) {\n case 'concat': return args.map(String).join('')\n case 'add': return args.reduce<number>((total, next) => total + asNumber(next), 0)\n case 'sub': return asNumber(first) - asNumber(second)\n case 'mul': return args.reduce<number>((total, next) => total * asNumber(next), 1)\n case 'div': return asNumber(first) / asNumber(second)\n case 'mod': return asNumber(first) % asNumber(second)\n case 'neg': return -asNumber(first)\n case 'abs': return Math.abs(asNumber(first))\n case 'sqrt': return Math.sqrt(asNumber(first))\n case 'exp': return Math.exp(asNumber(first))\n case 'sin': return Math.sin(asNumber(first))\n case 'cos': return Math.cos(asNumber(first))\n case 'atan2': return Math.atan2(asNumber(first), asNumber(second))\n case 'min': return Math.min(...args.map(asNumber))\n case 'max': return Math.max(...args.map(asNumber))\n case 'floor': return Math.floor(asNumber(first))\n case 'ceil': return Math.ceil(asNumber(first))\n case 'round': return Math.round(asNumber(first))\n case 'clamp': return asNumber(first) < asNumber(second) ? asNumber(second) : asNumber(first) > asNumber(third) ? asNumber(third) : asNumber(first)\n case 'lerp': return asNumber(first) + (asNumber(second) - asNumber(first)) * asNumber(third)\n case 'ease': return ease(asNumber(first))\n case 'lt': return asNumber(first) < asNumber(second)\n case 'gt': return asNumber(first) > asNumber(second)\n case 'le': return asNumber(first) <= asNumber(second)\n case 'ge': return asNumber(first) >= asNumber(second)\n case 'eq': return first === second\n case 'ne': return first !== second\n case 'not': return !first\n case 'vadd': return reduceVectors(args, (total, next) => total + next)\n case 'vsub': return reduceVectors(args, (total, next) => total - next)\n case 'vscale': return asVector(first).map((component) => component * asNumber(second))\n case 'vnorm': return normalized(asVector(first) as readonly [number, number, number])\n case 'vneg': return asVector(first).map((component) => -component)\n case 'vlerp': return asVector(first).map((component, axis) => component + (asVector(second)[axis]! - component) * asNumber(third))\n case 'vx': return asVector(first)[0]\n case 'vy': return asVector(first)[1]\n case 'vz': return asVector(first)[2]\n case 'vdist': {\n const dx = asVector(first)[0]! - asVector(second)[0]!\n const dy = asVector(first)[1]! - asVector(second)[1]!\n const dz = asVector(first)[2]! - asVector(second)[2]!\n return Math.sqrt(dx * dx + dy * dy + dz * dz)\n }\n case 'with': return asVector(first).map((component, axis) => (axis === asNumber(second) ? asNumber(third) : component))\n default: throw new Error(`unknown expression: ${name}`)\n }\n}\n\n/** Resolves every `Expr` inside `value` (arrays and plain objects are walked in order, so the draws follow the order the page wrote its fields). */\nexport function resolve(value: unknown, context: ExprContext): unknown {\n if (Array.isArray(value)) return value.map((entry) => resolve(entry, context))\n if (typeof value !== 'object' || value === null) return value\n if (!isExpr(value)) return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, resolve(entry, context)]))\n const args = value.a ?? []\n switch (value.$) {\n case 'rnd': return context.random.between(asNumber(resolve(args[0], context)), asNumber(resolve(args[1], context)))\n case 'rand': return context.random.next()\n case 'param': return (args[0] as readonly unknown[])[context.param]\n case 'paramIndex': return context.param\n case 't': return context.time\n case 'dt': return context.dt\n case 'index': return context.index\n case 'quality': return context.quality\n case 'fresh': return context.fresh\n case 'direction': return context.direction\n case 'normal': return context.normal\n case 'scaled': return context.scaled(asNumber(resolve(args[0], context)))\n case 'var': return context.variables.get(args[0] as string)\n case 'at': return (resolve(args[0], context) as readonly unknown[])[asNumber(resolve(args[1], context))]\n case 'query': return context.query(args[0] as string, args[1] as string, args.slice(2).map((entry) => resolve(entry, context)))\n case 'cameraPosition': return context.camera.position\n case 'cameraForward': return context.camera.forward(asVector(resolve(args[0], context)))\n case 'let': return resolve(args[2], { ...context, variables: new Map([...context.variables, [args[0] as string, resolve(args[1], context)]]) })\n case 'ite': return resolve(resolve(args[0], context) ? args[1] : args[2], context)\n case 'and': return args.reduce<unknown>((result, next) => (result ? resolve(next, context) : result), true)\n case 'or': return args.reduce<unknown>((result, next) => (result ? result : resolve(next, context)), false)\n default: return evaluateCall(value.$, args.map((entry) => resolve(entry, context)))\n }\n}\n","import type { WorkshopEngine } from '../../workshop/engine'\nimport type { SpriteSpawn, Vec3Tuple } from '../../workshop/types'\nimport { normalized, plus } from '../vector-math'\n\nconst UP: Vec3Tuple = [0, 1, 0]\nconst ZERO: Vec3Tuple = [0, 0, 0]\nconst FULL_TURN_DRAWN = 6.283\n\nexport interface FireEmitOptions {\n readonly wind?: Vec3Tuple\n readonly dir?: Vec3Tuple\n readonly smoke?: number\n readonly smokeColor?: number\n readonly smokeAlpha?: number\n readonly noSmoke?: boolean\n}\n\nfunction scaledSizes(range: readonly [number, number], factor: number): readonly [number, number] {\n return [range[0] * Math.sqrt(factor), range[1] * Math.sqrt(factor)]\n}\n\n/** `fireEmit` (W399-405): flames, a hot core, embers and smoke for one frame, from a point and a radius, scaled by `factor`. */\nexport function fireEmit(engine: WorkshopEngine, key: string, dt: number, centre: Vec3Tuple, radius: number, factor: number, options: FireEmitOptions = {}): void {\n const wind = options.wind ?? ZERO\n const sqrtFactor = Math.sqrt(factor)\n engine.emit(`${key}f`, 'add', 90 * factor, dt, () => ({\n p: centre, r: radius, flat: true, dir: options.dir ?? UP, cone: 0.35, sp: [0.4, 1.4], v: wind, life: [0.35, 0.85], size: scaledSizes([0.22, 0.5], factor), grow: 0.35, c0: 0xffd27a, c1: 0x8a1a05, a: 0.85, fi: 0.15, g: -2.2,\n }))\n engine.emit(`${key}c`, 'add', 30 * factor, dt, () => ({\n p: centre, r: radius * 0.6, flat: true, dir: UP, cone: 0.3, sp: [0.3, 0.8], life: [0.2, 0.4], size: scaledSizes([0.25, 0.4], factor), grow: 0.6, c0: 0xfff2c0, c1: 0xff8a20, a: 0.8, fi: 0.1, g: -1,\n }))\n if (engine.quality < 2) {\n engine.emit(`${key}e`, 'streak', 10 * factor, dt, () => {\n const jitter = plus(wind, engine.random.between(-0.4, 0.4), 0, engine.random.between(-0.4, 0.4))\n return { p: centre, box: [radius, 0, radius], dir: UP, cone: 0.5, sp: [1.5, 3.5], v: jitter, life: [0.8, 1.8], len: 0.03, c0: 0xffb050, c1: 0x601000, g: -0.6, drag: 0.4 }\n })\n }\n if (options.noSmoke) return\n engine.emit(`${key}s`, 'normal', (options.smoke || 14) * factor, dt, () => ({\n p: plus(centre, 0, 0.8 * sqrtFactor, 0), r: radius * 0.7, dir: UP, cone: 0.35, sp: [0.6, 1.4], v: wind, drag: 0.15, life: [2.5, 4.5], size: scaledSizes([0.4, 0.8], factor), grow: 4, c0: options.smokeColor || 0x2a2522, c1: 0x121318, a: options.smokeAlpha || 0.42, fi: 0.2, g: -0.25,\n }))\n}\n\nfunction ringOfDust(engine: WorkshopEngine, point: Vec3Tuple, scale: number): void {\n for (let index = 0; index < engine.scaled(44); index++) {\n const angle = engine.random.next() * FULL_TURN_DRAWN\n const spec: SpriteSpawn = {\n p: plus(point, Math.cos(angle) * 0.5, 0.15, Math.sin(angle) * 0.5), dir: [Math.cos(angle), 0.08, Math.sin(angle)], cone: 0.15, sp: [4, 9 * scale], drag: 2.4, life: [1.4, 2.6], size: [0.5, 0.9], grow: 4, c0: 0x5d574d, c1: 0x2a2724, a: 0.5, fi: 0.1,\n }\n engine.spawn('normal', spec)\n }\n}\n\n/** `explode` (W406-416): the Workshop's blast, scaled by `scale`, that the Lab's profiles later replaced. `cameraPosition` turns the second ring to face the viewer. */\nexport function explode(engine: WorkshopEngine, point: Vec3Tuple, scale: number, cameraPosition: Vec3Tuple, chipColor?: number): void {\n engine.flash([plus(point, 0, 1, 0), 0xffa040, 14 * scale, 0.5, 30, 'boom'])\n engine.screen.shake += 0.7 * scale\n engine.screen.white += 0.18 * scale\n engine.burst('add', 55 * scale, { p: point, r: 0.3 * scale, sp: [2, 7 * scale], drag: 4, life: [0.25, 0.6], size: [0.7 * scale, 1.4 * scale], grow: 1.7, c0: 0xfff0b0, c1: 0x9a2a08, fi: 0.04 })\n engine.burst('add', 28 * scale, { p: plus(point, 0, 0.3, 0), r: 0.3, dir: UP, cone: 0.7, sp: [2, 5 * scale], drag: 1.5, life: [0.6, 1.1], size: [0.6 * scale, 1 * scale], grow: 1.3, c0: 0xffc060, c1: 0x5a1205, g: -2, fi: 0.05 })\n engine.ring([plus(point, 0, 0.05, 0), 0.3, 7 * scale, 0.55, 0xffd8a0])\n const toCamera = normalized([cameraPosition[0] - point[0], cameraPosition[1] - point[1], cameraPosition[2] - point[2]])\n engine.ring([plus(point, 0, 1, 0), 0.3, 5 * scale, 0.35, 0xfff0d0, toCamera])\n engine.burst('normal', 32 * scale, { p: plus(point, 0, 0.6, 0), r: 0.8 * scale, dir: UP, cone: 1.1, sp: [1, 3], drag: 1.2, life: [3, 6], size: [1 * scale, 1.8 * scale], grow: 2.6, c0: 0x2b2724, c1: 0x0f1013, a: 0.6, fi: 0.18, g: -0.35 })\n ringOfDust(engine, point, scale)\n if (engine.quality < 2) engine.burst('streak', 44 * scale, { p: point, dir: UP, cone: 1.45, sp: [10, 26], life: [0.3, 0.7], len: 0.03, c0: 0xffc070, c1: 0x802000, g: 9.8, gnd: 2 })\n for (let index = 0; index < engine.scaled(16 * scale); index++) {\n engine.debrisPiece({ p: point, dir: UP, cone: 1.2, sp: [3, 9], size: [0.12, 0.06, 0.1], k: [0.6, 1.6], c: chipColor || 0x2a2622, life: 9 })\n }\n engine.decal([point[0], engine.context.groundAt(point[0], point[2]), point[2], 3 * scale, 0x0a0a0a, { grow: 0.15, life: 60 }])\n}\n","import type { WorkshopEngine } from '../../workshop/engine'\nimport type { Vec3Tuple } from '../../workshop/types'\nimport { normalized, plus, plusScaled, scaledBy } from '../vector-math'\n\nconst FULL_TURN_DRAWN = 6.283\n\nexport const WEAPON_NAMES = ['glock', 'ak', 'r870', 'barrett'] as const\nexport type WeaponName = (typeof WEAPON_NAMES)[number]\n\n/** Per weapon: flash size, petals, light, smoke puffs and length of the petals (W417). */\nconst WEAPON_SIZES: Readonly<Record<WeaponName, readonly [number, number, number, number, number]>> = {\n glock: [0.35, 4, 3, 3, 0.25],\n ak: [0.6, 5, 5, 5, 0.45],\n r870: [0.95, 8, 7, 10, 0.6],\n barrett: [1.45, 6, 11, 14, 0.5],\n}\n\nfunction barrettBrake(engine: WorkshopEngine, tip: Vec3Tuple, direction: Vec3Tuple): void {\n const side = normalized([direction[2], 0, -direction[0]])\n for (const sign of [-1, 1]) {\n const sideways = scaledBy(side, sign)\n for (let index = 0; index < engine.scaled(8); index++) {\n engine.spawn('add', { p: plusScaled(tip, side, sign * 0.06), dir: sideways, cone: 0.25, sp: [2, 5], drag: 6, life: 0.09, size: [0.25, 0.4], c0: 0xffc070, c1: 0xff6010, fi: 0 })\n }\n engine.burst('normal', 10, { p: tip, dir: normalized(plus(sideways, 0, 0.2, 0)), cone: 0.3, sp: [1.5, 3], drag: 2, life: [1.2, 2], size: [0.15, 0.25], grow: 5, c0: 0x8a8a86, a: 0.3, g: -0.2 })\n }\n for (let index = 0; index < engine.scaled(26); index++) {\n const angle = engine.random.next() * FULL_TURN_DRAWN\n engine.spawn('normal', { p: [tip[0] + Math.cos(angle) * 0.3, 0.05, tip[2] + Math.sin(angle) * 0.3], dir: [Math.cos(angle), 0.15, Math.sin(angle)], cone: 0.2, sp: [1.5, 3.5], drag: 2, life: [1, 1.8], size: [0.2, 0.35], grow: 3, c0: 0x5a554b, a: 0.4 })\n }\n}\n\n/** `muzzle` (W417-429): flash core, petals along the barrel and smoke, scaled by weapon; the Barrett also vents sideways and lifts the dust. */\nexport function muzzle(engine: WorkshopEngine, weapon: WeaponName, tip: Vec3Tuple, direction: Vec3Tuple): void {\n const [flashSize, petals, light, smoke, length] = WEAPON_SIZES[weapon]\n engine.burst('add', 3, { p: tip, size: [flashSize * 0.45, flashSize * 0.75], life: 0.055, c0: 0xfff4d0, fi: 0 })\n for (let index = 0; index < engine.scaled(petals * 2); index++) {\n engine.spawn('add', { p: plusScaled(tip, direction, engine.random.between(0, length)), r: 0.03, size: [flashSize * 0.22, flashSize * 0.38], life: 0.06, c0: 0xffb050, c1: 0xff7020, fi: 0 })\n }\n engine.burst('streak', petals, { p: tip, dir: direction, cone: 0.55, sp: [6, 12], life: 0.05, len: 0.025, c0: 0xffd080 })\n if (weapon === 'barrett') barrettBrake(engine, tip, direction)\n engine.burst('normal', smoke, { p: tip, dir: direction, cone: 0.45, sp: [0.4, 1.6], drag: 2.2, life: [1, 2.2], size: [0.06, 0.14], grow: 6, c0: 0x8a8a86, c1: 0x55565a, a: 0.22, g: -0.25, fi: 0.05 })\n engine.flash([tip, 0xffc070, light, 0.05, 10])\n}\n\nexport type CasingType = 'shell' | '50' | ''\n\n/** `casing` (W430): a brass case, or a red shell, thrown out of the port. */\nexport function casing(engine: WorkshopEngine, port: Vec3Tuple, type: CasingType): void {\n const size = type === 'shell' ? ([0.03, 0.03, 0.07] as const) : type === '50' ? ([0.024, 0.024, 0.13] as const) : ([0.014, 0.014, 0.05] as const)\n engine.debrisPiece({ p: port, dir: normalized([0.8, 0.6, 0.2]), cone: 0.3, sp: [1.6, 2.6], size, c: type === 'shell' ? 0xb5281e : 0xc9a24a, spin: 20, bounce: 0.45, life: 20 })\n}\n","import type { WorkshopEngine } from '../workshop/engine'\nimport type { Vec3Tuple } from '../workshop/types'\nimport { bloodHit, impact } from './builders/blood'\nimport type { ImpactMaterial } from './builders/blood'\nimport { explode, fireEmit } from './builders/fire'\nimport type { FireEmitOptions } from './builders/fire'\nimport { chips, rustAt, sparkAt } from './builders/shared'\nimport { casing, muzzle } from './builders/weapon'\nimport type { CasingType, WeaponName } from './builders/weapon'\n\nexport interface MacroContext {\n readonly engine: WorkshopEngine\n readonly cameraPosition: Vec3Tuple\n readonly dt: number\n}\n\ntype MacroArgs = readonly unknown[]\n\n/** The helpers the page's recipes share (`bloodHit`, `impact`, `fireEmit`, `explode`, `muzzle`, `casing`, `chips`, `sparkAt`, `rustAt`), called by name from a step. */\nexport const MACROS: Readonly<Record<string, (context: MacroContext, args: MacroArgs) => void>> = {\n blood_hit: ({ engine }, [point, direction, age, scale]) => bloodHit(engine, point as Vec3Tuple, direction as Vec3Tuple, age as number, scale as number | undefined),\n impact: ({ engine }, [material, point, normal, scale, age]) => impact(engine, material as ImpactMaterial, point as Vec3Tuple, normal as Vec3Tuple, scale as number | undefined, age as number | undefined),\n fire_emit: ({ engine, dt }, [key, centre, radius, factor, options]) => fireEmit(engine, key as string, dt, centre as Vec3Tuple, radius as number, factor as number, options as FireEmitOptions | undefined),\n explode: ({ engine, cameraPosition }, [point, scale, chipColor]) => explode(engine, point as Vec3Tuple, scale as number, cameraPosition, chipColor as number | undefined),\n muzzle: ({ engine }, [weapon, tip, direction]) => muzzle(engine, weapon as WeaponName, tip as Vec3Tuple, direction as Vec3Tuple),\n casing: ({ engine }, [port, type]) => casing(engine, port as Vec3Tuple, type as CasingType),\n chips: ({ engine }, [point, direction, count, spread]) => chips(engine, point as Vec3Tuple, direction as Vec3Tuple, count as number, spread as number | undefined),\n spark_at: ({ engine }, [point, direction, count, spread]) => sparkAt(engine, point as Vec3Tuple, direction as Vec3Tuple, count as number, spread as number | undefined),\n rust_at: ({ engine }, [point, count]) => rustAt(engine, point as Vec3Tuple, count as number),\n}\n","import { createSchedule } from '../schedule'\nimport type { ScreenState } from '../types'\nimport type { Placement, WorkshopEngine } from '../workshop/engine'\nimport type { DebrisSpawn, MeshSpawn, SpriteSpawn, Vec3Tuple } from '../workshop/types'\nimport type { Behavior, BehaviorHost } from './behavior'\nimport type { ExprContext } from './expression'\nimport { resolve } from './expression'\nimport { MACROS } from './macros'\nimport type { CueRequest, EffectDef, EffectStep, ScreenKey } from './types'\n\nconst DEFAULT_DIRECTION: Vec3Tuple = [0, 0, -1]\nconst DEFAULT_NORMAL: Vec3Tuple = [0, 1, 0]\nconst SCREEN_FIELDS: Readonly<Record<ScreenKey, keyof ScreenState>> = {\n shake: 'shake', white: 'white', red: 'red', dirL: 'directionLeft', dirR: 'directionRight', chroma: 'chroma', desat: 'desat', gas: 'gas', beat: 'beat',\n}\n\n/** What an effect reads off the view: where it is and which way the camera's own axes point. The Workshop's bench also moves it (`camera` steps). */\nexport interface EffectView {\n readonly position: Vec3Tuple\n forward(local: readonly number[]): Vec3Tuple\n goTo?(eye: Vec3Tuple, target: Vec3Tuple): void\n place?(eye: Vec3Tuple, target: Vec3Tuple): void\n}\n\nexport interface InstanceDeps {\n readonly engine: WorkshopEngine\n readonly view: EffectView\n /** The prop models the recipe can call, by id (see `Behavior`). */\n readonly behavior: (id: string) => Behavior\n /** Every behaviour made so far, to age them every frame. */\n readonly behaviors: () => Iterable<Behavior>\n readonly onCue?: (cue: CueRequest) => void\n}\n\n/** Where and how a played effect sits in the world. */\nexport interface InstanceSetup {\n readonly placement?: Placement\n /** Which way the effect points in the world (`direction` in a recipe). */\n readonly direction?: Vec3Tuple\n /** The surface it hit (`normal` in a recipe). */\n readonly normal?: Vec3Tuple\n /** Named numbers the recipe reads with `variable(name)`; they are in place before its first step. */\n readonly values?: Readonly<Record<string, number>>\n}\n\n/**\n * One run of one effect (W1259-1295): `begin` runs the recipe's `setup` and `timeline`, `step` runs the timers that came due (from the last added to the\n * first) and the `tick`, once per frame. Step by step it draws random numbers where the page did and in the same order.\n */\nexport class EffectInstance {\n readonly variables = new Map<string, unknown>()\n def: EffectDef | undefined\n param = 0\n /** True until the first run of a chosen effect is over: a repeat of the loop is not fresh (`f.fresh`). */\n fresh = false\n time = 0\n /** Seconds since the effect was chosen: unlike `time`, a restart of the loop does not reset it (`el`). */\n elapsed = 0\n setup: InstanceSetup = {}\n private dt = 0\n private index = 0\n private readonly schedule = createSchedule()\n private readonly host: BehaviorHost\n private readonly context: ExprContext\n\n constructor(private readonly deps: InstanceDeps) {\n const instance = (): EffectInstance => this\n this.host = {\n engine: deps.engine,\n camera: deps.view,\n variables: this.variables,\n get param() { return instance().param },\n get time() { return instance().time },\n after: (seconds, action) => this.schedule.at(seconds, action),\n }\n this.context = {\n get random() { return deps.engine.random },\n get param() { return instance().param },\n get time() { return instance().time },\n get dt() { return instance().dt },\n get index() { return instance().index },\n variables: this.variables,\n scaled: (count) => deps.engine.scaled(count),\n query: (behavior, name, args) => deps.behavior(behavior).query(this.host, name, args),\n get quality() { return deps.engine.quality },\n get fresh() { return instance().fresh },\n get direction() { return instance().setup.direction ?? DEFAULT_DIRECTION },\n get normal() { return instance().setup.normal ?? DEFAULT_NORMAL },\n camera: { get position() { return deps.view.position }, forward: (local) => deps.view.forward(local) },\n }\n }\n\n /** The effect starts (`select` + `startLoop`): the recipe's `setup` and `timeline` run now. */\n begin(def: EffectDef, param: number | undefined, setup: InstanceSetup = {}): void {\n this.def = def\n this.param = param ?? def.params?.default ?? 0\n this.elapsed = 0\n this.fresh = true\n this.setup = setup\n this.restart()\n }\n\n /** The loop starts over (`startLoop`): timers, variables and props go back to the start. */\n restart(): void {\n const def = this.def\n if (!def) return\n this.schedule.clear()\n this.variables.clear()\n for (const [name, value] of Object.entries(this.setup.values ?? {})) this.variables.set(name, value)\n for (const behavior of this.deps.behaviors()) behavior.reset(this.host)\n this.time = 0\n this.within(() => {\n this.execute(def.setup ?? [])\n this.execute(def.timeline)\n })\n this.fresh = false\n }\n\n /** The effect's part of one frame: the timers due, then the tick, then the props. The pools are aged by whoever owns the engine. */\n step(dt: number): void {\n const def = this.def\n this.dt = dt\n if (def) {\n this.time += dt\n this.elapsed += dt\n this.within(() => {\n this.schedule.update(this.time)\n this.execute(def.tick ?? [])\n })\n }\n for (const behavior of this.deps.behaviors()) behavior.update(this.host, dt, this.elapsed)\n }\n\n /** True once a looping effect has run its seconds, or a one-shot has nothing left to fire. */\n get finished(): boolean {\n const def = this.def\n if (!def) return true\n if (def.loop && def.loop > 0) return this.time >= def.loop\n return false\n }\n\n /** The recipe's own camera shake on top of the screen's (`panico` trembles the aim). */\n get tremor(): number {\n return (this.variables.get('tremor') as number | undefined) ?? 0\n }\n\n private within(action: () => void): void {\n const engine = this.deps.engine\n const previous = engine.placement\n engine.placement = this.setup.placement\n try {\n action()\n } finally {\n engine.placement = previous\n }\n }\n\n private value(expression: unknown): unknown {\n return resolve(expression, this.context)\n }\n\n execute(steps: readonly EffectStep[]): void {\n for (const step of steps) this.run(step)\n }\n\n private run(step: EffectStep): void {\n const engine = this.deps.engine\n switch (step.op) {\n case 'burst': return engine.burst(step.system, this.value(step.count) as number, this.value(step.spawn) as SpriteSpawn)\n case 'spawn': return engine.spawn(step.system, this.value(step.spawn) as SpriteSpawn)\n case 'emit': return engine.emit(step.key, step.system, this.value(step.rate) as number, this.dt, () => this.value(step.spawn) as SpriteSpawn)\n case 'mesh': { engine.mesh(this.value(step.spawn) as MeshSpawn); return }\n case 'debris': return engine.debrisPiece(this.value(step.spawn) as DebrisSpawn)\n case 'decal': return engine.decal(this.value(step.args) as Parameters<WorkshopEngine['decal']>[0])\n case 'ring': return engine.ring(this.value(step.args) as Parameters<WorkshopEngine['ring']>[0])\n case 'flash': return engine.flash(this.value(step.args) as Parameters<WorkshopEngine['flash']>[0])\n case 'lens': return engine.lensBlob(...(this.value(step.args) as [number, number, number, string, number]))\n case 'screen': return this.changeScreen(step)\n case 'fire_light': return engine.fireLight(step.slot, this.value(step.point) as Vec3Tuple, this.value(step.intensity) as number, this.value(step.time) as number)\n case 'fire_light_off': engine.lights.fire[step.slot]!.intensity = 0; return\n case 'cue': return this.cue(step)\n case 'call': return MACROS[step.macro]!({ engine, cameraPosition: this.deps.view.position, dt: this.dt }, this.value(step.args) as readonly unknown[])\n case 'at': return this.scheduleSteps(step)\n case 'when': return this.execute(this.value(step.if) ? step.steps : (step.otherwise ?? []))\n case 'repeat': return this.repeat(step)\n case 'set': { this.variables.set(step.name, this.value(step.value)); return }\n case 'append': { this.variables.set(step.name, [...((this.variables.get(step.name) as unknown[] | undefined) ?? []), this.value(step.value)]); return }\n case 'camera': return this.moveView(step)\n case 'post': { Object.assign(engine.post, this.value(step.values)); return }\n case 'behavior': return this.deps.behavior(step.id).act(this.host, step.action, this.value(step.args))\n }\n }\n\n private moveView(step: Extract<EffectStep, { op: 'camera' }>): void {\n const eye = this.value(step.eye) as Vec3Tuple\n const target = this.value(step.target) as Vec3Tuple\n if (step.instant) this.deps.view.place?.(eye, target)\n else this.deps.view.goTo?.(eye, target)\n }\n\n private changeScreen(step: Extract<EffectStep, { op: 'screen' }>): void {\n const screen = this.deps.engine.screen\n for (const [key, expression] of Object.entries(step.add ?? {})) screen[SCREEN_FIELDS[key as ScreenKey]] += this.value(expression) as number\n for (const [key, expression] of Object.entries(step.set ?? {})) screen[SCREEN_FIELDS[key as ScreenKey]] = this.value(expression) as number\n for (const [key, expression] of Object.entries(step.max ?? {})) screen[SCREEN_FIELDS[key as ScreenKey]] = Math.max(screen[SCREEN_FIELDS[key as ScreenKey]], this.value(expression) as number)\n }\n\n private cue(step: Extract<EffectStep, { op: 'cue' }>): void {\n const args = this.value(step.args)\n for (let draw = 0; draw < (step.draws ?? 0); draw++) this.deps.engine.random.next()\n this.deps.onCue?.({ cue: step.cue, args, time: this.time })\n }\n\n private scheduleSteps(step: Extract<EffectStep, { op: 'at' }>): void {\n const captured = this.index\n this.schedule.at(this.value(step.time) as number, () => {\n const previous = this.index\n this.index = captured\n this.execute(step.steps)\n this.index = previous\n })\n }\n\n private repeat(step: Extract<EffectStep, { op: 'repeat' }>): void {\n const previous = this.index\n const count = this.value(step.count) as number\n for (let index = 0; index < count; index++) {\n this.index = index\n this.execute(step.steps)\n }\n this.index = previous\n }\n}\n","import { lehmer } from '../random'\nimport type { RandomStream } from '../random'\nimport type { Follow } from '../types'\nimport type { Placement, WorkshopEngine } from '../workshop/engine'\nimport type { Vec3Tuple } from '../workshop/types'\nimport type { Behavior } from './behavior'\nimport { WORKSHOP_BEHAVIORS } from './behaviors'\nimport { EffectInstance } from './instance'\nimport type { EffectView } from './instance'\nimport type { CueRequest, EffectDef } from './types'\n\nconst ORIGIN: Vec3Tuple = [0, 0, 0]\nconst DEFAULT_HEADING: Vec3Tuple = [0, 0, -1]\nconst DEFAULT_MAX_INSTANCES = 48\n/** A recipe with no end of its own (`loop: 0`: a campfire, a fire in a building) plays this long unless it is stopped. */\nconst ENDLESS_SECONDS = 60\n\nexport interface PlayOptions {\n /** Which way the effect points in the world (the muzzle's barrel, the spray of a jet). */\n readonly direction?: Vec3Tuple\n /** Which way the thing it belongs to is turned, as a unit quaternion `[x, y, z, w]`: the effect is placed with this turn, pitch and roll included, in place of the turn about the vertical axis `direction` gives. */\n readonly orientation?: readonly [number, number, number, number]\n /** The surface it hit. */\n readonly normal?: Vec3Tuple\n /** Repeats the same visual: a LAN client draws the same numbers from the same seed. */\n readonly seed?: number\n /** The recipe's parameter (age, material, weapon...). */\n readonly param?: number | string\n /** Named numbers the recipe reads with `variable(name)` (a size, a length of time): set before its `setup` runs, and again whenever its loop starts over. */\n readonly values?: Readonly<Record<string, number>>\n /** Moves with a thing: the effect sits at its position plus the point it was played at. */\n readonly follow?: Follow\n /** Stops after this many seconds (default: the recipe's loop). */\n readonly seconds?: number\n}\n\nexport interface EffectPlayerOptions {\n readonly engine: WorkshopEngine\n readonly effects: Readonly<Record<string, EffectDef>>\n readonly behaviors?: Readonly<Record<string, () => Behavior>>\n /** Where the viewer is, for the recipes that read it. */\n readonly view: EffectView\n /** A stream for a seed (default `lehmer`). */\n readonly streamFor?: (seed: number) => RandomStream\n readonly maxInstances?: number\n readonly onCue?: (cue: CueRequest) => void\n}\n\nexport interface EffectPlayer {\n /** Plays `id` at `point`; `null` when no recipe has that id or the cap of live effects is reached. Returns a handle for `stop`. */\n play(id: string, point: Vec3Tuple, options?: PlayOptions): number | null\n stop(handle: number): void\n /** Runs the timers and ticks of every live effect (the engine's pools are aged by its owner). */\n update(dt: number): void\n clear(): void\n readonly active: number\n has(id: string): boolean\n ids(): readonly string[]\n}\n\ninterface Playing {\n readonly handle: number\n readonly instance: EffectInstance\n readonly random: RandomStream | undefined\n readonly follow: Follow | undefined\n readonly origin: Vec3Tuple\n readonly anchor: Vec3Tuple\n readonly yaw: number\n readonly orientation: readonly [number, number, number, number] | undefined\n readonly seconds: number\n}\n\nconst positionOf = (follow: Follow): Vec3Tuple => {\n const source = 'position' in follow ? follow.position : follow\n return [source.x, source.y, source.z]\n}\n\n/** A parameter given by its option id (`'ak'`, `'concrete'`) as the index of the option; a number is taken as it is. */\nfunction paramIndex(def: EffectDef, param: number | string | undefined): number | undefined {\n if (typeof param !== 'string') return param\n const index = def.params?.options.indexOf(param) ?? -1\n return index >= 0 ? index : undefined\n}\n\n/** The turn about the vertical axis that takes the recipe's `heading` onto `direction` (three.js turns by `x' = cos x + sin z`). */\nfunction yawOf(heading: Vec3Tuple, direction: Vec3Tuple | undefined): number {\n if (!direction || (direction[0] === 0 && direction[2] === 0)) return 0\n return Math.atan2(direction[0], direction[2]) - Math.atan2(heading[0], heading[2])\n}\n\n/**\n * Plays the Workshop's recipes anywhere in the world, many at once, each with its own clock, variables and props, a place and a way it faces. They all\n * spawn into the one engine, so the pools stay shared and bounded. This is what `VfxWorld.play` and the host's `EffectSink` use.\n */\nexport function createEffectPlayer(options: EffectPlayerOptions): EffectPlayer {\n const { engine, view } = options\n const factories = options.behaviors ?? WORKSHOP_BEHAVIORS\n const streamOf = options.streamFor ?? ((seed: number) => lehmer(seed))\n const limit = options.maxInstances ?? DEFAULT_MAX_INSTANCES\n const playing: Playing[] = []\n let next = 0\n\n function makeInstance(): EffectInstance {\n const made = new Map<string, Behavior>()\n return new EffectInstance({\n engine,\n view,\n behavior: (id) => {\n let behavior = made.get(id)\n if (!behavior) {\n const make = factories[id]\n if (!make) throw new Error(`no behaviour ${id}`)\n behavior = make()\n made.set(id, behavior)\n }\n return behavior\n },\n behaviors: () => made.values(),\n ...(options.onCue ? { onCue: options.onCue } : {}),\n })\n }\n\n function placementOf(entry: Playing): Placement {\n const origin = entry.follow ? [entry.origin[0] + positionOf(entry.follow)[0], entry.origin[1] + positionOf(entry.follow)[1], entry.origin[2] + positionOf(entry.follow)[2]] as const : entry.origin\n return { origin, anchor: entry.anchor, yaw: entry.yaw, ...(entry.orientation ? { orientation: entry.orientation } : {}) }\n }\n\n /** Runs `action` drawing from the effect's own stream, if it has one. */\n function drawing(entry: Playing, action: () => void): void {\n const context = engine.context\n const shared = context.random\n if (entry.random) context.random = entry.random\n try {\n action()\n } finally {\n context.random = shared\n }\n }\n\n return {\n play(id, point, playOptions = {}) {\n const def = options.effects[id]\n if (!def || playing.length >= limit) return null\n const heading = def.heading ?? DEFAULT_HEADING\n const instance = makeInstance()\n const entry: Playing = {\n handle: ++next,\n instance,\n random: playOptions.seed !== undefined ? streamOf(playOptions.seed) : undefined,\n follow: playOptions.follow,\n origin: point,\n anchor: def.anchor ?? ORIGIN,\n yaw: yawOf(heading, playOptions.direction),\n orientation: playOptions.orientation,\n seconds: playOptions.seconds ?? (def.loop && def.loop > 0 ? def.loop : ENDLESS_SECONDS),\n }\n playing.push(entry)\n drawing(entry, () =>\n instance.begin(def, paramIndex(def, playOptions.param), {\n placement: placementOf(entry),\n ...(playOptions.direction ? { direction: playOptions.direction } : {}),\n ...(playOptions.normal ? { normal: playOptions.normal } : {}),\n ...(playOptions.values ? { values: playOptions.values } : {}),\n }),\n )\n return entry.handle\n },\n stop(handle) {\n const index = playing.findIndex((entry) => entry.handle === handle)\n if (index >= 0) playing.splice(index, 1)\n },\n update(dt) {\n for (let index = playing.length - 1; index >= 0; index--) {\n const entry = playing[index]!\n const instance = entry.instance\n instance.setup = { ...instance.setup, placement: placementOf(entry) }\n drawing(entry, () => instance.step(dt))\n if (instance.time >= entry.seconds) playing.splice(index, 1)\n }\n },\n clear() {\n playing.length = 0\n },\n get active() {\n return playing.length\n },\n has: (id) => id in options.effects,\n ids: () => Object.keys(options.effects),\n }\n}\n","import type { Rgb } from './color'\nimport { hexToRgb, srgbChannelToLinear } from './color'\n\n/** One of the 4 blast lights (L349-365). */\nexport interface BlastLight {\n x: number\n y: number\n z: number\n distance: number\n peak: number\n life: number\n t: number\n color: Rgb\n /** The level before the flash layer is applied. */\n level: number\n intensity: number\n}\n\n/** One of the 2 fire lights: they go to the two largest fires. */\nexport interface FireLight {\n x: number\n y: number\n z: number\n distance: number\n intensity: number\n}\n\n/** The glow the puff shader reads (`uGlowPos`, `uGlowCol`, `uGlowI`): where the brightest light is, in linear colour. */\nexport interface LightGlow {\n x: number\n y: number\n z: number\n color: [number, number, number]\n intensity: number\n}\n\nexport interface LightPool {\n readonly blast: BlastLight[]\n readonly fire: FireLight[]\n readonly glow: LightGlow\n /** Which blast light the next explosion takes (the Lab's `xlIdx`). */\n cursor: number\n}\n\nexport const BLAST_LIGHTS = 4\nexport const FIRE_LIGHTS = 2\nconst DEFAULT_BLAST_COLOR = 0xffa050\nconst FIRE_GLOW_COLOR: Rgb = [1, 0.42, 0.12]\nconst PEAK_HOLD = 0.05\n\nexport function createLightPool(): LightPool {\n return {\n blast: Array.from({ length: BLAST_LIGHTS }, () => ({ x: 0, y: -50, z: 0, distance: 30, peak: 0, life: 1, t: 9, color: hexToRgb(DEFAULT_BLAST_COLOR), level: 0, intensity: 0 })),\n fire: Array.from({ length: FIRE_LIGHTS }, () => ({ x: 0, y: -50, z: 0, distance: 16, intensity: 0 })),\n glow: { x: 0, y: 0, z: 0, color: [1, 0.5, 0.2], intensity: 0 },\n cursor: 0,\n }\n}\n\nexport function resetLightPool(pool: LightPool): void {\n for (const light of pool.blast) {\n light.t = 9\n light.level = 0\n light.intensity = 0\n }\n pool.cursor = 0\n}\n\nexport interface BlastLightSpec {\n readonly x: number\n readonly y: number\n readonly z: number\n readonly peak: number\n readonly distance: number\n readonly life: number\n readonly color: number\n}\n\n/** `boomLight` (L357-358): the next blast light, round robin. */\nexport function lightBlast(pool: LightPool, spec: BlastLightSpec): void {\n const light = pool.blast[pool.cursor++ % pool.blast.length]!\n light.x = spec.x\n light.y = spec.y\n light.z = spec.z\n light.distance = spec.distance\n light.peak = spec.peak\n light.life = spec.life\n light.t = 0\n light.color = hexToRgb(spec.color)\n}\n\nexport interface FireForLight {\n readonly x: number\n readonly y: number\n readonly z: number\n readonly r: number\n readonly fade: number\n}\n\nexport interface LightStep {\n readonly dt: number\n /** 1 while the flash layer is on, 0 while it is off (the Lab multiplies by it). */\n readonly flashLayer: number\n readonly clock: number\n readonly fires: readonly FireForLight[]\n}\n\nfunction largestFire(fires: readonly FireForLight[], skip: FireForLight | null): FireForLight | null {\n let best: FireForLight | null = null\n for (const fire of fires) {\n if (fire === skip) continue\n if (best === null || fire.r > best.r) best = fire\n }\n return best\n}\n\nfunction updateBlastLights(pool: LightPool, step: LightStep): BlastLight | null {\n let brightest: BlastLight | null = null\n for (const light of pool.blast) {\n light.t += step.dt\n const age = light.t / light.life\n const level = age < 1 ? light.peak * (light.t < PEAK_HOLD ? 1 : Math.pow(1 - age, 2.2)) * (0.85 + 0.15 * Math.sin(light.t * 60)) : 0\n light.level = level\n light.intensity = level * step.flashLayer\n if (level > (brightest?.level ?? 0)) brightest = light\n }\n return brightest\n}\n\n/** `updateLights` (L350-364): blast lights fade by their curve, the two fire lights follow the two largest fires, and the glow follows the brightest light. */\nexport function updateLights(pool: LightPool, step: LightStep): void {\n const brightest = updateBlastLights(pool, step)\n const brightness = brightest?.level ?? 0\n const first = largestFire(step.fires, null)\n const second = largestFire(step.fires, first)\n const largest = [first, second]\n for (let index = 0; index < FIRE_LIGHTS; index++) {\n const fire = largest[index]\n const light = pool.fire[index]!\n if (!fire) {\n light.intensity = 0\n continue\n }\n light.x = fire.x\n light.y = fire.y + Math.max(0.8, fire.r * 0.6)\n light.z = fire.z\n light.intensity = (1.4 + fire.r * 0.6) * (0.75 + 0.25 * Math.sin(step.clock * 17 + index * 3) + 0.1 * Math.sin(step.clock * 41)) * fire.fade\n light.distance = 10 + fire.r * 4\n }\n if (brightest && brightness > 0.05) {\n pool.glow.x = brightest.x\n pool.glow.y = brightest.y\n pool.glow.z = brightest.z\n pool.glow.color[0] = srgbChannelToLinear(brightest.color[0])\n pool.glow.color[1] = srgbChannelToLinear(brightest.color[1])\n pool.glow.color[2] = srgbChannelToLinear(brightest.color[2])\n pool.glow.intensity = brightness * 0.06 * step.flashLayer\n } else if (first) {\n pool.glow.x = first.x\n pool.glow.y = first.y + 0.5\n pool.glow.z = first.z\n pool.glow.color[0] = FIRE_GLOW_COLOR[0]\n pool.glow.color[1] = FIRE_GLOW_COLOR[1]\n pool.glow.color[2] = FIRE_GLOW_COLOR[2]\n pool.glow.intensity = (0.35 + first.r * 0.12) * first.fade\n } else pool.glow.intensity = 0\n}\n","import type { LodBands } from './types'\n\n/** The game's bands: distance minus the visual radius, 15 / 40 / 80 m, scales 1 / .5 / .2 / .1 (G68203, the Workshop's quality bands). The default. */\nexport const LOD_GAME: LodBands = { edges: [15, 40, 80], scales: [1, 0.5, 0.2, 0.1], radiusFactor: 1 }\n\n/** The Lab's own bands (L481): distance minus three visual radii, 20 / 45 / 85 m, scales 1 / .5 / .2 / .08. Used on the bench and in the goldens. */\nexport const LOD_LAB: LodBands = { edges: [20, 45, 85], scales: [1, 0.5, 0.2, 0.08], radiusFactor: 3 }\n\n/** The scale of an effect `distance` metres from the camera, for an effect whose fireball has `visualRadius`. */\nexport function lodScale(bands: LodBands, distance: number, visualRadius: number): number {\n const effective = distance - visualRadius * bands.radiusFactor\n if (effective < bands.edges[0]) return bands.scales[0]\n if (effective < bands.edges[1]) return bands.scales[1]\n if (effective < bands.edges[2]) return bands.scales[2]\n return bands.scales[3]\n}\n","import type { GroundField } from './ground'\nimport type { RandomStream } from './random'\n\n/** What an explosion leaves on the ground, as data: the renderer builds the meshes (L443-476). */\nexport interface ScorchMark {\n readonly kind: 'scorch'\n readonly x: number\n readonly z: number\n readonly radius: number\n /** Which of the three scorch textures. */\n readonly variant: number\n readonly lift: number\n readonly rotation: number\n}\n\nexport interface DirtMark {\n readonly kind: 'dirt'\n readonly x: number\n readonly z: number\n readonly radius: number\n readonly lift: number\n readonly rotation: number\n}\n\n/** A piece of broken asphalt on the rim of a crater. */\nexport interface RockMark {\n readonly kind: 'rock'\n readonly x: number\n readonly y: number\n readonly z: number\n readonly scale: readonly [number, number, number]\n readonly rotation: readonly [number, number, number]\n readonly plate: boolean\n readonly color: number\n}\n\nexport interface BloodMark {\n readonly kind: 'blood'\n readonly x: number\n readonly y: number\n readonly z: number\n readonly scaleX: number\n readonly scaleZ: number\n readonly rotation: number\n}\n\nexport type Mark = ScorchMark | DirtMark | RockMark | BloodMark\n\nexport type MarkEvent = { readonly type: 'add'; readonly mark: Mark } | { readonly type: 'clear' }\n\nexport interface MarkStore {\n readonly entries: Mark[]\n /** Goes up on every mark added or cleared. */\n readonly version: number\n readonly bloodCount: number\n /** The counter that lifts each decal a hair higher than the last (the Lab's `decalY`). */\n lift: number\n add(mark: Mark): void\n clear(): void\n /** Tells a renderer the moment a mark is made (the ground it conforms to is the ground of that moment) or taken away. */\n subscribe(listener: (event: MarkEvent) => void): () => void\n}\n\nconst BLOOD_LIMIT = 450\nconst SCORCH_VARIANTS = 3\nconst RIM_COLORS = [0x2b2c2f, 0x3a3836] as const\n\nexport function createMarkStore(): MarkStore {\n const entries: Mark[] = []\n let version = 0\n let bloodCount = 0\n const listeners = new Set<(event: MarkEvent) => void>()\n return {\n entries,\n get version() {\n return version\n },\n get bloodCount() {\n return bloodCount\n },\n lift: 0,\n add(mark) {\n entries.push(mark)\n if (mark.kind === 'blood') bloodCount++\n version++\n for (const listener of listeners) listener({ type: 'add', mark })\n },\n clear() {\n entries.length = 0\n bloodCount = 0\n this.lift = 0\n version++\n for (const listener of listeners) listener({ type: 'clear' })\n },\n subscribe(listener) {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n }\n}\n\nfunction nextLift(store: MarkStore): number {\n return store.lift++ % 20\n}\n\n/** `scorch` (L458): a burn mark; the texture and the turn are drawn in that order. */\nexport function addScorch(store: MarkStore, random: RandomStream, x: number, z: number, radius: number): void {\n const variant = Math.floor(random.next() * SCORCH_VARIANTS)\n const lift = 0.03 + nextLift(store) * 0.001\n const rotation = random.next() * 6\n store.add({ kind: 'scorch', x, z, radius, variant, lift, rotation })\n}\n\nfunction addRimRocks(store: MarkStore, ground: GroundField | undefined, random: RandomStream, x: number, z: number, radius: number): void {\n const count = Math.round(6 + radius * 7)\n for (let index = 0; index < count; index++) {\n const angle = (index / count) * Math.PI * 2 + random.between(-0.15, 0.15)\n const distance = radius * random.between(0.95, 1.3)\n const size = random.between(0.12, 0.26) * Math.min(1.6, 0.6 + radius * 0.4)\n const px = x + Math.cos(angle) * distance\n const pz = z + Math.sin(angle) * distance\n const plate = random.next() < 0.6\n const color = RIM_COLORS[random.next() < 0.5 ? 0 : 1]\n const tiltX = random.between(-0.5, 0.5)\n const tiltZ = random.between(-0.6, 0.2)\n store.add({\n kind: 'rock',\n x: px,\n y: (ground?.heightAt(px, pz) ?? 0) + size * 0.12,\n z: pz,\n scale: [size * 2, size * 0.35, size * 1.4],\n rotation: [tiltX, angle, tiltZ],\n plate,\n color,\n })\n }\n}\n\n/** `crater` (L460-468): digs the ground (always), then, when marks are on, lays the dirt, and broken asphalt around the rim. */\nexport function addCrater(store: MarkStore, ground: GroundField | undefined, random: RandomStream, spec: { x: number; z: number; radius: number; depth: number; marks: boolean }): void {\n ground?.dent(random, spec.x, spec.z, spec.radius, spec.depth)\n if (!spec.marks) return\n const rotation = random.next() * 6\n store.add({ kind: 'dirt', x: spec.x, z: spec.z, radius: spec.radius * 1.02, lift: 0.045, rotation })\n addRimRocks(store, ground, random, spec.x, spec.z, spec.radius)\n}\n\n/** `bloodSplat` (L471-475). */\nexport function addBloodSplat(store: MarkStore, ground: GroundField | undefined, random: RandomStream, spec: { x: number; z: number; radius: number }): void {\n if (store.bloodCount > BLOOD_LIMIT) return\n const y = (ground?.heightAt(spec.x, spec.z) ?? 0) + 0.03 + nextLift(store) * 0.0008\n const scaleX = spec.radius * random.between(1.6, 2.4)\n const scaleZ = spec.radius * random.between(1.2, 2)\n store.add({ kind: 'blood', x: spec.x, y, z: spec.z, scaleX, scaleZ, rotation: random.next() * 6 })\n}\n","import { OrderedPool } from '../pool'\nimport type { Ring } from './puffs'\n\nexport function createRingPool(capacity: number): OrderedPool<Ring> {\n return new OrderedPool<Ring>(capacity, () => ({ x: 0, z: 0, y0: 0, rise: 0, rk: 0, R0: 0, R1: 0, r: 0, roll: 0, t: 0, life: 0 }))\n}\n\nfunction ageOrKeep(ring: Ring, dt: number): boolean {\n ring.t += dt\n return ring.t <= ring.life + 1\n}\n\n/** `updateRings` (L736): a ring is kept one second past the life of its puffs. */\nexport function updateRings(pool: OrderedPool<Ring>, dt: number): void {\n pool.compact(ageOrKeep, dt)\n}\n\nexport function spawnRing(pool: OrderedPool<Ring>, spec: Omit<Ring, 't'>): Ring {\n const ring = pool.acquire()\n ring.x = spec.x\n ring.z = spec.z\n ring.y0 = spec.y0\n ring.rise = spec.rise\n ring.rk = spec.rk\n ring.R0 = spec.R0\n ring.R1 = spec.R1\n ring.r = spec.r\n ring.roll = spec.roll\n ring.t = 0\n ring.life = spec.life\n return ring\n}\n","import { OrderedPool } from '../pool'\nimport type { Puff } from './puffs'\n\n/** A shock wave: an air shell that grows to the lethal radius in 120 ms, and a dust ring on the ground (L319-348). The last block is what the renderer reads. */\nexport interface Shock {\n x: number\n y: number\n z: number\n Rs: number\n Rg: number\n t: number\n dur: number\n shellScale: number\n shellAlpha: number\n ringScale: number\n ringAlpha: number\n}\n\nconst DEFAULT_DURATION = 0.3\nconst SHELL_GROWTH_SECONDS = 0.12\nconst SHELL_FADE_SECONDS = 0.16\nconst PUSH_BAND = 0.8\nconst PUSH_CEILING = 3\nconst PUSH_AGE_LIMIT = 2\n\nexport function createShockPool(capacity: number): OrderedPool<Shock> {\n return new OrderedPool<Shock>(capacity, () => ({ x: 0, y: 0, z: 0, Rs: 0, Rg: 0, t: 0, dur: DEFAULT_DURATION, shellScale: 0, shellAlpha: 0, ringScale: 0, ringAlpha: 0 }))\n}\n\nexport function spawnShock(pool: OrderedPool<Shock>, spec: { x: number; y: number; z: number; shellRadius: number; groundRadius: number; duration: number }): Shock {\n const shock = pool.acquire()\n shock.x = spec.x\n shock.y = spec.y\n shock.z = spec.z\n shock.Rs = spec.shellRadius\n shock.Rg = spec.groundRadius\n shock.t = 0\n shock.dur = spec.duration || DEFAULT_DURATION\n shock.shellScale = 0\n shock.shellAlpha = 0\n shock.ringScale = 0\n shock.ringAlpha = 0\n return shock\n}\n\nfunction pushPuffs(puffs: readonly Puff[], shock: Shock, reach: number, dt: number, age: number): void {\n for (const puff of puffs) {\n if (puff.ring || puff.t > PUSH_AGE_LIMIT) continue\n const dx = puff.x - shock.x\n const dz = puff.z - shock.z\n const distance = Math.hypot(dx, dz) + 0.001\n if (Math.abs(distance - reach) < PUSH_BAND && puff.y < PUSH_CEILING) {\n const strength = 4 * dt * (1 - age)\n puff.vx += (dx / distance) * strength * 6\n puff.vz += (dz / distance) * strength * 6\n }\n }\n}\n\n/** `updateShocks` (L335-348), walking the list backwards like the Lab: the fronts push the smoke and dust they meet. */\nexport function updateShocks(pool: OrderedPool<Shock>, puffs: readonly Puff[], dt: number): void {\n for (let index = pool.items.length - 1; index >= 0; index--) {\n const shock = pool.items[index]!\n shock.t += dt\n const age = shock.t / shock.dur\n const shellEase = 1 - Math.pow(1 - Math.min(1, shock.t / SHELL_GROWTH_SECONDS), 3)\n shock.shellScale = Math.max(0.01, shock.Rs * shellEase)\n shock.shellAlpha = Math.max(0, 1 - shock.t / SHELL_FADE_SECONDS) * 0.9\n const ringEase = 1 - Math.pow(1 - Math.min(1, age), 2.2)\n shock.ringScale = Math.max(0.01, shock.Rg * ringEase)\n shock.ringAlpha = Math.max(0, 1 - age) * 0.85\n pushPuffs(puffs, shock, shock.Rg * ringEase, dt, age)\n if (age >= 1) pool.removeAt(index)\n }\n}\n","import { hexToLinearRgb, hexToRgb } from './color'\nimport { SMOKE_COLORS } from './constants'\nimport type { VfxCore } from './core'\nimport { lightBlast } from './lights'\nimport { lodScale } from './lod'\nimport { addCrater, addScorch } from './marks'\nimport { spawnRing } from './pools/rings'\nimport { spawnShock } from './pools/shocks'\nimport type { RandomStream, MutableVec3 } from './random'\nimport { addFire } from './emitters/fire'\nimport { addChunk, addGlow, addPuff, addSpark } from './spawn'\nimport type { ExplosionDef, MushroomSpec } from './types'\nimport { addScaled, createVec3, distanceBetween, normalize } from './vector'\n\n/** Where and how one explosion goes off. */\nexport interface ExplosionCall {\n readonly x: number\n readonly y: number\n readonly z: number\n /** Where it is aimed (a rocket's direction, a charge's face). */\n readonly direction?: MutableVec3\n /** The stream the whole effect draws from: the world's in `shared` mode, the event's own in `per_event`. */\n readonly random: RandomStream\n readonly handle: number\n}\n\ninterface Context {\n readonly core: VfxCore\n readonly def: ExplosionDef\n readonly call: ExplosionCall\n readonly random: RandomStream\n readonly x: number\n readonly y: number\n readonly z: number\n /** Fireball radius. */\n readonly size: number\n /** LOD scale of this explosion. */\n readonly scale: number\n readonly direction: MutableVec3 | undefined\n readonly smokeColor: readonly [number, number, number]\n /** Where the camera is (for the screen flash), distance to the blast. */\n readonly cameraDistance: number\n}\n\nconst DEFAULT_LIGHT = 40\nconst DEFAULT_LIGHT_LIFE = 0.45\nconst DEFAULT_LIGHT_COLOR = 0xffa050\nconst DEFAULT_FLASH_INTENSITY = 6\nconst DEFAULT_SHOCK_DURATION = 0.32\nconst DEFAULT_CHUNK_COLOR = 0x47423d\nconst ROCK_COLORS = [0x2b2c2f, 0x4a3a2c] as const\nconst SCRATCH = createVec3()\n\nfunction pick<T>(random: RandomStream, items: readonly T[]): T {\n return items[Math.floor(random.next() * items.length)]!\n}\n\nfunction composeFlash(context: Context): void {\n const { core, def, x, y, z, size } = context\n if (!core.layers.flash) return\n const flash = def.flash\n const light = { x, y: y + size * 0.6, z, peak: (flash.light || DEFAULT_LIGHT) * 0.7, distance: (def.injury || 10) * 2.4, life: flash.life || DEFAULT_LIGHT_LIFE, color: flash.color || DEFAULT_LIGHT_COLOR }\n core.trace?.light?.(light)\n lightBlast(core.lights, light)\n core.spawned.lights++\n const blinding = flash.blinding === true\n addGlow(core, { x, y: y + size * 0.4, z, s: size * 1.4, s1: size * 3.2, i: flash.intensity || DEFAULT_FLASH_INTENSITY, life: 0.22, color: [1, 0.92, 0.75], curve: 2.5 })\n if (!blinding) addGlow(core, { x, y: y + size * 0.5, z, s: size * 2.2, s1: size * 3.6, i: 2.2, life: def.fuel ? 1.4 : 0.6, color: [1, 0.45, 0.15], curve: 1.6 })\n if (core.reducedMotion) return\n const distance = context.cameraDistance\n const screen = core.screen\n if (distance < 15) {\n screen.flash = Math.max(screen.flash, (blinding ? 1 : 0.32) * (1 - distance / 15))\n screen.flashDecay = blinding ? 0.55 : 7\n }\n if (blinding && distance < 40) {\n screen.flash = Math.max(screen.flash, 1)\n screen.flashDecay = 0.5\n }\n}\n\nfunction composeFireballs(context: Context): void {\n const { core, def, random, x, y, z, size, scale, direction } = context\n if (!core.layers.fire || !def.fireballs) return\n const count = Math.round(def.fireballs * Math.max(scale, 0.35))\n const fuel = def.fuel === true\n const heat = def.heat ?? 1\n for (let index = 0; index < count; index++) {\n const d = random.direction(SCRATCH)\n d.y = fuel ? Math.abs(d.y) * 0.5 + 0.05 : Math.abs(d.y) * 0.8 + 0.1\n if (direction) normalize(addScaled(d, direction, def.directionBias ?? 1.2))\n const speed = size * random.between(4.5, 7.5) * (fuel ? 0.75 : 1)\n const s1 = size * random.between(0.42, 0.62)\n const sg = size * random.between(0.12, 0.22)\n const life = random.between(def.smoke.life * 0.7, def.smoke.life)\n const h0 = random.between(1.05, 1.35) * heat\n const hl = fuel ? random.between(0.8, 1.4) * (def.heatLifeScale ?? 1) : random.between(0.22, 0.42)\n addPuff(core, random, {\n x: x + d.x * size * 0.15, y: y + size * 0.25 + d.y * size * 0.1, z: z + d.z * size * 0.15,\n vx: d.x * speed, vy: d.y * speed + (fuel ? size * 1.4 : size * 0.6), vz: d.z * speed,\n s0: size * 0.22, s1, sg, gk: 9, life, h0, hl, drag: fuel ? 4.2 : 6.5, buoy: fuel ? 2.2 : 1.2, color: context.smokeColor, kind: 0,\n })\n }\n}\n\nfunction composeSmoke(context: Context): void {\n const { core, def, random, x, y, z, size, scale } = context\n if (!core.layers.smoke || !def.smoke.count) return\n const count = Math.round(def.smoke.count * Math.max(scale, 0.3))\n for (let index = 0; index < count; index++) {\n const d = random.direction(SCRATCH)\n d.y = Math.abs(d.y)\n const vy = size * random.between(0.6, 2.2)\n const s1 = size * random.between(0.55, 0.85)\n const sg = size * random.between(0.18, 0.3)\n const life = random.between(def.smoke.life * 0.7, def.smoke.life * 1.1)\n addPuff(core, random, {\n x: x + d.x * size * 0.6, y: y + size * 0.4 + d.y * size * 0.5, z: z + d.z * size * 0.6,\n vx: d.x * size * 2.4, vy, vz: d.z * size * 2.4, s0: size * 0.25, s1, sg, gk: 4, life, h0: 0.32, hl: 0.25, drag: 3, buoy: 0.45, color: context.smokeColor, kind: def.smoke.kind ?? 0,\n })\n }\n}\n\nfunction composeShock(context: Context): void {\n const { core, def, x, y, z, size, scale } = context\n if (!core.layers.shock || def.shock === false || !(scale > 0.15)) return\n const duration = def.shock ? def.shock.duration : DEFAULT_SHOCK_DURATION\n const spec = { x, y: y + size * 0.3, z, shellRadius: def.lethal * 0.9, groundRadius: def.injury * 0.6, duration }\n core.trace?.shock?.(spec)\n spawnShock(core.shocks, spec)\n}\n\nfunction composeDust(context: Context): void {\n const { core, def, random, x, z, size, scale } = context\n if (!core.layers.smoke || !def.dust.count || !(scale > 0.15)) return\n const count = Math.round(def.dust.count * scale)\n const color = hexToLinearRgb(def.dust.color)\n const cone = def.cone\n for (let index = 0; index < count; index++) {\n let angle = random.next() * Math.PI * 2\n if (cone) angle = coneAngle(context) + random.between(-cone.half, cone.half)\n const speed = random.between(0.6, 1) * def.injury * (cone ? 1.6 : 0.9)\n const vy = random.between(0.2, 1.2)\n const s1 = size * random.between(0.5, 0.8)\n const sg = size * random.between(0.2, 0.35)\n const life = random.between(2.2, 4)\n addPuff(core, random, {\n x: x + Math.cos(angle) * 0.3, y: 0.15, z: z + Math.sin(angle) * 0.3,\n vx: Math.cos(angle) * speed, vy, vz: Math.sin(angle) * speed, s0: size * 0.2, s1, sg, gk: 5, life, drag: 3.6, buoy: 0.2, color, kind: 1,\n })\n }\n}\n\n/** The angle of the cone's axis: the direction it is aimed at, else the one in its profile. */\nfunction coneAngle(context: Context): number {\n const cone = context.def.cone\n if (!cone) return 0\n return context.direction ? Math.atan2(context.direction.z, context.direction.x) : cone.angle\n}\n\nfunction sparkDirection(context: Context): MutableVec3 {\n const { def, random, direction } = context\n const cone = def.cone\n if (cone) {\n const angle = coneAngle(context) + random.between(-cone.half, cone.half)\n SCRATCH.x = Math.cos(angle)\n SCRATCH.y = random.between(-0.05, 0.25)\n SCRATCH.z = Math.sin(angle)\n return SCRATCH\n }\n const d = random.direction(SCRATCH)\n d.y = Math.abs(d.y) * 0.9 + 0.05\n if (direction) normalize(addScaled(d, direction, def.directionBias ?? 1))\n return d\n}\n\nfunction composeEmbers(context: Context): void {\n const { core, def, random, x, y, z, size, scale } = context\n const embers = def.shrapnel.embers\n if (!embers) return\n for (let index = 0; index < embers * scale; index++) {\n const d = random.direction(SCRATCH)\n d.y = Math.abs(d.y)\n const vx = d.x * random.between(2, 6)\n const vy = random.between(3, 8)\n const vz = d.z * random.between(2, 6)\n addSpark(core, { x, y: y + size * 0.5, z, vx, vy, vz, life: random.between(1.2, 2.6), w: 0.04, st: 0.02, kind: 4, drag: 1.6, g: 2.5 })\n }\n}\n\nfunction composeShrapnel(context: Context): void {\n const { core, def, random, x, y, z, size, scale } = context\n if (!core.layers.shrapnel || !def.shrapnel.count) return\n const count = Math.round(def.shrapnel.count * scale)\n for (let index = 0; index < count; index++) {\n const d = sparkDirection(context)\n const speed = def.shrapnel.speed * random.between(0.45, 1)\n addSpark(core, {\n x: x + d.x * 0.1, y: y + size * 0.25, z: z + d.z * 0.1, vx: d.x * speed, vy: d.y * speed, vz: d.z * speed,\n life: random.between(0.35, 1.1), w: def.shrapnel.width ?? 0.035, st: 0.03, kind: def.shrapnel.kind, drag: def.shrapnel.kind === 1 ? 0.15 : 0.9,\n })\n }\n composeEmbers(context)\n}\n\nfunction composeDebris(context: Context): void {\n const { core, def, random, x, y, z, scale, direction } = context\n const chunks = def.chunks\n if (!core.layers.shrapnel || !chunks.count || scale < 0.5) return\n const count = Math.round(chunks.count * (scale >= 1 ? 1 : 0.5))\n for (let index = 0; index < count; index++) {\n const d = random.direction(SCRATCH)\n d.y = Math.abs(d.y) * 0.9 + 0.2\n if (direction) normalize(addScaled(d, direction, 0.8))\n const speed = random.between(4, 11) * (chunks.speed ?? 1)\n const s = random.between(0.07, 0.2) * (chunks.size ?? 1)\n const plate = chunks.plate ? random.next() < 0.6 : random.next() < 0.15\n const sy = plate ? s * 0.3 : s * random.between(0.6, 1)\n const color = hexToRgb(chunks.colors.length > 0 ? pick(random, chunks.colors) : DEFAULT_CHUNK_COLOR)\n const hot = random.next() < chunks.hot\n addChunk(core, random, {\n x: x + d.x * 0.2, y: y + 0.2, z: z + d.z * 0.2, vx: d.x * speed, vy: d.y * speed + 2, vz: d.z * speed,\n sx: plate ? s * 1.6 : s, sy, sz: plate ? s * 1.1 : s, color, plate, hot,\n })\n }\n}\n\nfunction composeRimDebris(context: Context): void {\n const { core, random, x, z } = context\n const radius = context.def.mark.crater ?? 0\n if (!core.layers.shrapnel) return\n const speedScale = context.def.chunks.speed ?? 1\n const count = Math.round(4 + radius * 4)\n for (let index = 0; index < count; index++) {\n const angle = random.next() * Math.PI * 2\n const speed = random.between(3, 8) * speedScale\n const s = random.between(0.12, 0.28)\n const vy = random.between(4, 10)\n const color = hexToRgb(random.next() < 0.6 ? ROCK_COLORS[0] : ROCK_COLORS[1])\n addChunk(core, random, {\n x: x + Math.cos(angle) * 0.3, y: 0.1, z: z + Math.sin(angle) * 0.3, vx: Math.cos(angle) * speed * 0.6, vy, vz: Math.sin(angle) * speed * 0.6,\n sx: s * 1.6, sy: s * 0.35, sz: s * 1.2, color, plate: true,\n })\n }\n}\n\nfunction composeMushroom(context: Context, spec: MushroomSpec): void {\n const { core, random, x, y, z } = context\n core.trace?.mushroom?.(x, y, z, spec)\n const color = spec.color !== undefined ? hexToLinearRgb(spec.color) : SMOKE_COLORS.black\n const ring = spawnRing(core.rings, {\n x, z, y0: y + spec.height * 0.15, rise: spec.height, rk: spec.riseRate ?? 0.55, R0: spec.radius * 0.3, R1: spec.radius, r: spec.radius * 0.42, roll: spec.roll ?? 2.4, life: spec.life || 7,\n })\n const lod = core.clock.lod\n const count = Math.round((spec.count || 40) * Math.max(lod, 0.4))\n for (let index = 0; index < count; index++) {\n const s1 = spec.radius * random.between(0.35, 0.5)\n const life = random.between(ring.life * 0.75, ring.life)\n const h0 = random.between(1, 1.25)\n const hl = random.between(0.9, 1.6)\n const puff = addPuff(core, random, { x, y, z, s0: spec.radius * 0.2, s1, sg: spec.radius * 0.06, gk: 3, life, h0, hl, color, kind: 0 })\n puff.ring = ring\n puff.ph = random.next() * Math.PI * 2\n puff.th = random.next() * Math.PI * 2\n }\n const stemCount = Math.round((spec.stem || 18) * Math.max(lod, 0.4))\n for (let index = 0; index < stemCount; index++) {\n const spread = random.next() * 0.6\n const px = x + random.between(-0.4, 0.4) * spec.radius * 0.2\n const pz = z + random.between(-0.4, 0.4) * spec.radius * 0.2\n const vx = random.between(-0.3, 0.3)\n const vy = spec.height * random.between(0.6, 1.4) * (1 - spread)\n const vz = random.between(-0.3, 0.3)\n const s1 = spec.radius * random.between(0.22, 0.32)\n const life = random.between(3, 5)\n const h0 = random.between(0.9, 1.2)\n const hl = random.between(0.6, 1.1)\n addPuff(core, random, { x: px, y: y + 0.3, z: pz, vx, vy, vz, s0: spec.radius * 0.12, s1, sg: spec.radius * 0.05, gk: 4, life, h0, hl, drag: 1.2, buoy: 0.5, color })\n }\n}\n\nfunction composeAfterEffects(context: Context): void {\n const { core, def, random, x, y, z } = context\n const mark = def.mark\n if (mark.crater && mark.dent && y < 1.2) {\n core.emit({ type: 'crater', x, z, radius: mark.crater, depth: mark.dent })\n core.trace?.crater?.(x, z, mark.crater, mark.dent)\n addCrater(core.marks, core.ground, random, { x, z, radius: mark.crater, depth: mark.dent, marks: core.layers.mark })\n if (core.layers.shrapnel) composeRimDebris(context)\n }\n if (mark.scorch && y < 1.2 && core.layers.mark) {\n core.trace?.scorch?.(x, z, mark.scorch)\n addScorch(core.marks, random, x, z, mark.scorch)\n }\n if (def.mushroom && core.layers.fire) composeMushroom(context, def.mushroom)\n if (def.residualFires && core.layers.fire) {\n for (const fire of def.residualFires) {\n addFire(core, random, {\n x: x + (fire.dx ?? 0), y: 0, z: z + (fire.dz ?? 0), r: fire.radius, life: fire.life, rate: fire.rate, smoke: fire.smoke, startRadius: fire.startRadius, grow: fire.grow,\n color: fire.smokeColor !== undefined ? hexToLinearRgb(fire.smokeColor) : undefined,\n }, context.call.handle)\n }\n }\n}\n\nfunction emitRequests(context: Context): void {\n const { core, def, x, y, z } = context\n const point = [x, y, z] as const\n core.emit({\n type: 'blast_front', explosion: def.id, point, glassRadius: Math.min(def.injury, 20) * 0.85, speed: 340, lethal: def.lethal, injury: def.injury, push: def.push,\n hotParts: def.hotParts === true, blinding: def.flash.blinding === true, cone: def.cone ? { angle: coneAngle(context), half: def.cone.half, length: def.cone.length } : undefined,\n })\n if (def.ignite) core.emit({ type: 'ignite', point, radius: def.ignite })\n}\n\n/**\n * The Lab's layer compositor (`blast`, L642-714), layer by layer in the Lab's order and drawing from the stream in the Lab's order: flash, fireball,\n * its own smoke, shock wave, dust, shrapnel, debris, then marks, mushroom and lingering fires, and last what the host does (glass, characters).\n */\nexport function composeExplosion(core: VfxCore, def: ExplosionDef, call: ExplosionCall): number {\n const cameraDistance = distanceBetween(core.camera.x, core.camera.y, core.camera.z, call.x, call.y, call.z)\n const forced = core.clock.forcedLod\n const bandScale = forced ?? lodScale(core.bands, cameraDistance, def.visualRadius)\n const capScale = core.areaCap ? core.areaCap.register(core.clock.time, call.x, call.z) : 1\n const scale = forced ?? bandScale * capScale\n core.clock.lod = scale\n core.clock.lastBlast = core.clock.time\n core.spawned.puffs = core.spawned.sparks = core.spawned.chunks = core.spawned.lights = 0\n const context: Context = {\n core, def, call, random: call.random, x: call.x, y: call.y, z: call.z, size: def.visualRadius, scale, direction: call.direction,\n smokeColor: hexToLinearRgb(def.smoke.color), cameraDistance,\n }\n composeFlash(context)\n core.screen.shake = Math.max(core.screen.shake, (def.shake || 1) / (1 + cameraDistance * 0.12))\n composeFireballs(context)\n composeSmoke(context)\n composeShock(context)\n composeDust(context)\n composeShrapnel(context)\n composeDebris(context)\n composeAfterEffects(context)\n emitRequests(context)\n return scale\n}\n","import type { VfxCore } from '../core'\nimport type { Chunk } from './chunks'\nimport { addBloodSplat } from '../marks'\nimport { SMOKE_COLORS } from '../constants'\nimport { addPuff, addSpark } from '../spawn'\n\nconst RESTITUTION = -0.32\nconst GROUND_FRICTION = 0.5\nconst REST_SPEED = 0.6\nconst REST_SLIDE = 0.5\nconst TRAIL_SECONDS = 1.8\nconst TRAIL_STEP = 0.025\nconst QUARTER_TURN = Math.PI / 2\n\nfunction bounce(core: VfxCore, chunk: Chunk, floor: number): void {\n chunk.y = floor\n if (chunk.blood && !chunk.splat && core.layers.mark) {\n chunk.splat = true\n addBloodSplat(core.marks, core.ground, chunk.rng, { x: chunk.x, z: chunk.z, radius: chunk.rng.between(0.06, 0.13) })\n }\n chunk.vy *= RESTITUTION\n chunk.vx *= GROUND_FRICTION\n chunk.vz *= GROUND_FRICTION\n chunk.ax *= GROUND_FRICTION\n chunk.ay *= GROUND_FRICTION\n chunk.az *= GROUND_FRICTION\n if (Math.abs(chunk.vy) < REST_SPEED && Math.hypot(chunk.vx, chunk.vz) < REST_SLIDE) {\n chunk.rest = true\n chunk.rx = Math.round(chunk.rx / QUARTER_TURN) * QUARTER_TURN\n chunk.rz = Math.round(chunk.rz / QUARTER_TURN) * QUARTER_TURN\n }\n}\n\nfunction leaveTrail(core: VfxCore, chunk: Chunk, dt: number): void {\n chunk.trail -= dt\n if (chunk.trail > 0) return\n chunk.trail = TRAIL_STEP\n const random = chunk.rng\n addSpark(core, {\n x: chunk.x, y: chunk.y, z: chunk.z, vx: chunk.vx * 0.15, vy: chunk.vy * 0.15, vz: chunk.vz * 0.15,\n life: random.between(0.25, 0.45), w: 0.05 + chunk.sx * 0.2, st: 0.05, kind: 0, g: 0, drag: 1,\n })\n if (random.next() < 0.25) {\n addPuff(core, random, { x: chunk.x, y: chunk.y, z: chunk.z, s0: 0.04, s1: 0.14 + chunk.sx * 0.3, sg: 0.15, life: random.between(1, 1.6), drag: 2, buoy: 0.6, color: SMOKE_COLORS.grey, kind: 0 })\n }\n}\n\n/** `updateChunks` (L383-398): gravity, spin, a bounce that kills speed, a rest pose on a quarter turn, and the ember trail of a hot piece. */\nexport function updateChunks(core: VfxCore, dt: number): void {\n for (const chunk of core.chunks.items) {\n chunk.t += dt\n if (chunk.rest) continue\n chunk.vy -= core.gravity * dt\n chunk.x += chunk.vx * dt\n chunk.y += chunk.vy * dt\n chunk.z += chunk.vz * dt\n chunk.rx += chunk.ax * dt\n chunk.ry += chunk.ay * dt\n chunk.rz += chunk.az * dt\n const floor = chunk.sy * 0.45 + core.groundAt(chunk.x, chunk.z)\n if (chunk.y < floor) bounce(core, chunk, floor)\n if (chunk.hot && chunk.t < TRAIL_SECONDS && core.layers.smoke) leaveTrail(core, chunk, dt)\n }\n}\n","import type { ScreenState } from './types'\n\nconst DECAY = { shake: 5, white: 7, red: 2.2, side: 3, chroma: 3, desat: 0.7, gasSeconds: 5, beat: 6 } as const\nexport const INITIAL_FLASH_DECAY = 6\n\nexport function createScreenState(): ScreenState {\n return { shake: 0, flash: 0, flashDecay: INITIAL_FLASH_DECAY, white: 0, red: 0, directionLeft: 0, directionRight: 0, chroma: 0, desat: 0, gas: 0, beat: 0 }\n}\n\nexport function resetScreenState(state: ScreenState): void {\n Object.assign(state, createScreenState())\n}\n\n/**\n * The decays of the Lab (L1087, L1092: flash by its own rate, shake by 5 per second) and the Workshop (W354-355: white, red, the two sides, chroma,\n * desaturation, gas and the heartbeat), with the same constants.\n */\nexport function updateScreen(state: ScreenState, dt: number): void {\n state.flash *= Math.exp(-state.flashDecay * dt)\n state.shake *= Math.exp(-DECAY.shake * dt)\n state.white *= Math.exp(-DECAY.white * dt)\n state.red *= Math.exp(-DECAY.red * dt)\n state.directionLeft *= Math.exp(-DECAY.side * dt)\n state.directionRight *= Math.exp(-DECAY.side * dt)\n state.chroma *= Math.exp(-DECAY.chroma * dt)\n state.desat *= Math.exp(-DECAY.desat * dt)\n state.gas = Math.max(0, state.gas - dt / DECAY.gasSeconds)\n state.beat *= Math.exp(-DECAY.beat * dt)\n}\n","import type { SpriteKindDef } from './kinds'\n\nconst clamp = (v: number, lo: number, hi: number): number => (v < lo ? lo : v > hi ? hi : v)\nconst lerp = (a: number, b: number, t: number): number => a + (b - a) * t\n\nfunction rgb(hex: number): readonly [number, number, number] {\n return [((hex >> 16) & 255) / 255, ((hex >> 8) & 255) / 255, (hex & 255) / 255]\n}\n\n/** Per-spawn overrides. */\nexport interface SpriteSpawnOptions {\n /** `0xRRGGBB`: replaces the kind's colour ramp. */\n color?: number\n /** Multiplier on the start size. */\n size?: number\n /** Multiplier on the life. */\n life?: number\n /** Multiplier on the peak alpha. */\n alpha?: number\n /** Non-zero: a `land` sprite (a drip) that reaches the ground calls the owner's `onLand` with this tag. */\n tag?: number\n /** Who hears the landing (see `VfxWorld.onLand`). */\n owner?: string\n /** Height the sprite stops or lands at, instead of the ground's under it (a splash on a table top). */\n floor?: number\n /** Multiplier on the kind's upward acceleration (slow steam). */\n rise?: number\n}\n\nexport interface KindPoolHost {\n groundAt(x: number, z: number): number\n gravity: number\n land(owner: string, x: number, y: number, z: number, color: number, tag: number): void\n}\n\n/** The destruction renderer's old generator (mulberry32), so a seed gives the same sprites. */\nexport function spriteRandom(seed: number): () => number {\n let a = seed >>> 0\n return () => {\n a = (a + 0x6d2b79f5) >>> 0\n let t = a\n t = Math.imul(t ^ (t >>> 15), t | 1)\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n }\n}\n\n/**\n * One fixed-size ring of sprites of the game's kinds, in typed arrays a renderer draws as points: `position`, `color`, `alpha` and `size` are what it\n * reads, `drawCount` how many records from the start. Additive and normal blending are two instances.\n */\nexport class SpriteKindPool {\n live = 0\n /** Records from the start the renderer draws (the highest live index + 1). */\n drawCount = 0\n readonly position: Float32Array\n readonly color: Float32Array\n readonly alpha: Float32Array\n readonly size: Float32Array\n private next = 0\n private high = 0\n private readonly velocity: Float32Array\n private readonly age: Float32Array\n private readonly life: Float32Array\n private readonly size0: Float32Array\n private readonly alive: Uint8Array\n private readonly kind: Uint8Array\n private readonly seed: Float32Array\n private readonly alphaMul: Float32Array\n private readonly tag: Int32Array\n private readonly owner: Int32Array\n private readonly colorHex: Int32Array\n private readonly rgbStart: Float32Array\n private readonly rgbEnd: Float32Array\n private readonly bounced: Uint8Array\n private readonly floor: Float32Array\n private readonly riseMul: Float32Array\n\n constructor(\n readonly capacity: number,\n private readonly defs: readonly SpriteKindDef[],\n private readonly owners: string[],\n ) {\n const n = Math.max(1, capacity)\n this.position = new Float32Array(n * 3)\n this.color = new Float32Array(n * 3)\n this.alpha = new Float32Array(n)\n this.size = new Float32Array(n)\n this.velocity = new Float32Array(n * 3)\n this.age = new Float32Array(n)\n this.life = new Float32Array(n)\n this.size0 = new Float32Array(n)\n this.alive = new Uint8Array(n)\n this.kind = new Uint8Array(n)\n this.seed = new Float32Array(n)\n this.alphaMul = new Float32Array(n)\n this.tag = new Int32Array(n)\n this.owner = new Int32Array(n)\n this.colorHex = new Int32Array(n)\n this.rgbStart = new Float32Array(n * 3)\n this.rgbEnd = new Float32Array(n * 3)\n this.bounced = new Uint8Array(n)\n this.floor = new Float32Array(n)\n this.riseMul = new Float32Array(n)\n }\n\n spawn(kind: number, p: readonly number[], v: readonly number[], random: () => number, options?: SpriteSpawnOptions): void {\n if (this.capacity <= 0) return\n const def = this.defs[kind]!\n const i = this.next\n this.next = (this.next + 1) % this.capacity\n if (this.alive[i]) this.live--\n this.alive[i] = 1\n this.live++\n if (i + 1 > this.high) this.high = i + 1\n this.kind[i] = kind\n this.position.set(p, i * 3)\n this.velocity.set(v, i * 3)\n this.age[i] = 0\n this.life[i] = lerp(def.life[0], def.life[1], random()) * (options?.life ?? 1)\n this.size0[i] = lerp(def.size[0], def.size[1], random()) * (options?.size ?? 1)\n this.seed[i] = random()\n this.alphaMul[i] = options?.alpha ?? 1\n this.tag[i] = options?.tag ?? 0\n this.owner[i] = options?.owner === undefined ? -1 : this.ownerIndex(options.owner)\n this.colorHex[i] = options?.color ?? -1\n this.rgbStart.set(rgb(options?.color ?? def.c0), i * 3)\n this.rgbEnd.set(rgb(options?.color ?? def.c1), i * 3)\n this.bounced[i] = 0\n this.floor[i] = options?.floor ?? Number.NaN\n this.riseMul[i] = options?.rise ?? 1\n this.size[i] = this.size0[i]!\n this.alpha[i] = 0\n }\n\n private ownerIndex(name: string): number {\n let index = this.owners.indexOf(name)\n if (index < 0) index = this.owners.push(name) - 1\n return index\n }\n\n private kill(i: number): void {\n if (!this.alive[i]) return\n this.alive[i] = 0\n this.live--\n this.alpha[i] = 0\n this.size[i] = 0\n }\n\n clear(): void {\n this.alive.fill(0)\n this.alpha.fill(0)\n this.size.fill(0)\n this.live = 0\n this.high = 0\n this.next = 0\n this.drawCount = 0\n }\n\n /** Shoves the live sprites within `radius` of a point away from it (a blast). */\n push(cx: number, cy: number, cz: number, radius: number, speed: number): void {\n for (let i = 0; i < this.high; i++) {\n if (!this.alive[i]) continue\n const dx = this.position[i * 3]! - cx\n const dy = this.position[i * 3 + 1]! - cy\n const dz = this.position[i * 3 + 2]! - cz\n const d = Math.hypot(dx, dy, dz)\n if (d > radius || d < 1e-4) continue\n const f = (1 - d / radius) * speed\n this.velocity[i * 3]! += (dx / d) * f\n this.velocity[i * 3 + 1]! += (dy / d) * f\n this.velocity[i * 3 + 2]! += (dz / d) * f\n }\n }\n\n update(dt: number, host: KindPoolHost): void {\n let top = 0\n for (let i = 0; i < this.high; i++) {\n if (!this.alive[i]) continue\n const def = this.defs[this.kind[i]!]!\n const age = (this.age[i]! + dt)\n this.age[i] = age\n const life = this.life[i]!\n if (age >= life) {\n this.kill(i)\n continue\n }\n const t = age / life\n const i3 = i * 3\n let vx = this.velocity[i3]!\n let vy = this.velocity[i3 + 1]!\n let vz = this.velocity[i3 + 2]!\n vy += (def.rise * this.riseMul[i]! - def.gravity * host.gravity) * dt\n if (def.flutter) {\n const phase = age * 7 + this.seed[i]! * 40\n vx += Math.sin(phase) * def.flutter * dt\n vz += Math.cos(phase * 1.3) * def.flutter * dt\n }\n if (def.drag > 0) {\n const k = Math.exp(-def.drag * dt)\n vx *= k\n vy *= k\n vz *= k\n }\n const x = this.position[i3]! + vx * dt\n let y = this.position[i3 + 1]! + vy * dt\n const z = this.position[i3 + 2]! + vz * dt\n if (def.ground !== 'none') {\n const own = this.floor[i]!\n const gy = Number.isNaN(own) ? host.groundAt(x, z) + 0.005 : own\n if (y < gy) {\n if (def.ground === 'land') {\n const tag = this.tag[i]!\n const owner = this.owner[i]!\n if (tag !== 0 && owner >= 0) host.land(this.owners[owner]!, x, gy, z, this.colorHex[i]! >= 0 ? this.colorHex[i]! : def.c0, tag)\n this.kill(i)\n continue\n }\n y = gy\n if (def.ground === 'bounce' && !this.bounced[i]) {\n this.bounced[i] = 1\n vy = Math.abs(vy) * 0.35\n vx *= 0.6\n vz *= 0.6\n } else {\n vx = vy = vz = 0\n }\n }\n }\n this.velocity[i3] = vx\n this.velocity[i3 + 1] = vy\n this.velocity[i3 + 2] = vz\n this.position[i3] = x\n this.position[i3 + 1] = y\n this.position[i3 + 2] = z\n this.size[i] = this.size0[i]! * lerp(1, def.grow, t)\n this.color[i3] = lerp(this.rgbStart[i3]!, this.rgbEnd[i3]!, t)\n this.color[i3 + 1] = lerp(this.rgbStart[i3 + 1]!, this.rgbEnd[i3 + 1]!, t)\n this.color[i3 + 2] = lerp(this.rgbStart[i3 + 2]!, this.rgbEnd[i3 + 2]!, t)\n let envelope = t < def.fadeIn ? t / def.fadeIn : 1 - (t - def.fadeIn) / (1 - def.fadeIn)\n if (def.flicker) envelope *= 1 - def.flicker * (0.5 + 0.5 * Math.sin(age * 38 + this.seed[i]! * 50))\n this.alpha[i] = clamp(envelope, 0, 1) * def.alpha * this.alphaMul[i]!\n top = i + 1\n }\n this.high = top\n this.drawCount = top\n }\n}\n\nexport { rgb as hexToUnitRgb }\n","{\n \"fire\": {\n \"id\": \"fire\",\n \"additive\": true,\n \"life\": [\n 0.45,\n 0.9\n ],\n \"size\": [\n 0.1,\n 0.2\n ],\n \"grow\": 0.3,\n \"gravity\": 0,\n \"rise\": 2.4,\n \"drag\": 1.2,\n \"c0\": 16760896,\n \"c1\": 12591104,\n \"alpha\": 0.9,\n \"fadeIn\": 0.12,\n \"cone\": 0.7,\n \"ground\": \"none\",\n \"flicker\": 0.35\n },\n \"smoke\": {\n \"id\": \"smoke\",\n \"additive\": false,\n \"life\": [\n 1.8,\n 3.2\n ],\n \"size\": [\n 0.14,\n 0.24\n ],\n \"grow\": 3.2,\n \"gravity\": 0,\n \"rise\": 0.7,\n \"drag\": 0.8,\n \"c0\": 6710886,\n \"c1\": 10132122,\n \"alpha\": 0.35,\n \"fadeIn\": 0.15,\n \"cone\": 0.6,\n \"ground\": \"none\"\n },\n \"sparks\": {\n \"id\": \"sparks\",\n \"additive\": true,\n \"life\": [\n 0.25,\n 0.6\n ],\n \"size\": [\n 0.018,\n 0.03\n ],\n \"grow\": 0.5,\n \"gravity\": 1,\n \"rise\": 0,\n \"drag\": 0.3,\n \"c0\": 16773792,\n \"c1\": 16738832,\n \"alpha\": 1,\n \"fadeIn\": 0.02,\n \"cone\": 0.55,\n \"ground\": \"bounce\"\n },\n \"dust\": {\n \"id\": \"dust\",\n \"additive\": false,\n \"life\": [\n 1,\n 2\n ],\n \"size\": [\n 0.08,\n 0.14\n ],\n \"grow\": 2.5,\n \"gravity\": 0.05,\n \"rise\": 0,\n \"drag\": 2.5,\n \"c0\": 9274745,\n \"c1\": 9274745,\n \"alpha\": 0.4,\n \"fadeIn\": 0.1,\n \"cone\": 0.4,\n \"ground\": \"none\"\n },\n \"shards\": {\n \"id\": \"shards\",\n \"additive\": true,\n \"life\": [\n 0.8,\n 1.4\n ],\n \"size\": [\n 0.02,\n 0.035\n ],\n \"grow\": 0.8,\n \"gravity\": 1,\n \"rise\": 0,\n \"drag\": 0.2,\n \"c0\": 14677247,\n \"c1\": 10473727,\n \"alpha\": 0.9,\n \"fadeIn\": 0.03,\n \"cone\": 0.5,\n \"ground\": \"bounce\",\n \"flicker\": 0.5\n },\n \"splinters\": {\n \"id\": \"splinters\",\n \"additive\": false,\n \"life\": [\n 0.8,\n 1.5\n ],\n \"size\": [\n 0.02,\n 0.035\n ],\n \"grow\": 1,\n \"gravity\": 1,\n \"rise\": 0,\n \"drag\": 0.3,\n \"c0\": 10516042,\n \"c1\": 8017200,\n \"alpha\": 1,\n \"fadeIn\": 0.02,\n \"cone\": 0.5,\n \"ground\": \"bounce\"\n },\n \"chips\": {\n \"id\": \"chips\",\n \"additive\": false,\n \"life\": [\n 0.8,\n 1.5\n ],\n \"size\": [\n 0.025,\n 0.04\n ],\n \"grow\": 1,\n \"gravity\": 1,\n \"rise\": 0,\n \"drag\": 0.3,\n \"c0\": 9077624,\n \"c1\": 7235679,\n \"alpha\": 1,\n \"fadeIn\": 0.02,\n \"cone\": 0.5,\n \"ground\": \"bounce\"\n },\n \"fibers\": {\n \"id\": \"fibers\",\n \"additive\": false,\n \"life\": [\n 1.5,\n 2.5\n ],\n \"size\": [\n 0.02,\n 0.03\n ],\n \"grow\": 1,\n \"gravity\": 0.15,\n \"rise\": 0,\n \"drag\": 2,\n \"c0\": 12101768,\n \"c1\": 12101768,\n \"alpha\": 0.9,\n \"fadeIn\": 0.05,\n \"cone\": 0.4,\n \"ground\": \"stop\",\n \"flutter\": 2.5\n },\n \"leaves\": {\n \"id\": \"leaves\",\n \"additive\": false,\n \"life\": [\n 1.5,\n 2.8\n ],\n \"size\": [\n 0.03,\n 0.05\n ],\n \"grow\": 1,\n \"gravity\": 0.15,\n \"rise\": 0,\n \"drag\": 2,\n \"c0\": 6003260,\n \"c1\": 4882224,\n \"alpha\": 0.95,\n \"fadeIn\": 0.05,\n \"cone\": 0.4,\n \"ground\": \"stop\",\n \"flutter\": 2.8\n },\n \"drips\": {\n \"id\": \"drips\",\n \"additive\": false,\n \"life\": [\n 2,\n 3\n ],\n \"size\": [\n 0.025,\n 0.035\n ],\n \"grow\": 1,\n \"gravity\": 1,\n \"rise\": 0,\n \"drag\": 0,\n \"c0\": 6974058,\n \"c1\": 6974058,\n \"alpha\": 1,\n \"fadeIn\": 0.01,\n \"cone\": 0.2,\n \"ground\": \"land\"\n },\n \"ash\": {\n \"id\": \"ash\",\n \"additive\": false,\n \"life\": [\n 2,\n 4\n ],\n \"size\": [\n 0.02,\n 0.04\n ],\n \"grow\": 1.2,\n \"gravity\": 0.08,\n \"rise\": 0,\n \"drag\": 1.5,\n \"c0\": 2762275,\n \"c1\": 1710102,\n \"alpha\": 0.8,\n \"fadeIn\": 0.1,\n \"cone\": 0.5,\n \"ground\": \"stop\",\n \"flutter\": 0.6\n },\n \"steam\": {\n \"id\": \"steam\",\n \"additive\": false,\n \"life\": [\n 1,\n 2\n ],\n \"size\": [\n 0.12,\n 0.2\n ],\n \"grow\": 3,\n \"gravity\": 0,\n \"rise\": 0.9,\n \"drag\": 1,\n \"c0\": 16777215,\n \"c1\": 14739694,\n \"alpha\": 0.35,\n \"fadeIn\": 0.15,\n \"cone\": 0.6,\n \"ground\": \"none\"\n },\n \"flash\": {\n \"id\": \"flash\",\n \"additive\": true,\n \"life\": [\n 0.12,\n 0.2\n ],\n \"size\": [\n 1.2,\n 1.8\n ],\n \"grow\": 1.6,\n \"gravity\": 0,\n \"rise\": 0,\n \"drag\": 0,\n \"c0\": 16774080,\n \"c1\": 16748592,\n \"alpha\": 1,\n \"fadeIn\": 0.1,\n \"cone\": 0,\n \"ground\": \"none\"\n },\n \"foam\": {\n \"id\": \"foam\",\n \"additive\": false,\n \"life\": [\n 1,\n 1.8\n ],\n \"size\": [\n 0.06,\n 0.1\n ],\n \"grow\": 1.5,\n \"gravity\": 0.6,\n \"rise\": 0,\n \"drag\": 1.5,\n \"c0\": 16054527,\n \"c1\": 14542576,\n \"alpha\": 0.8,\n \"fadeIn\": 0.05,\n \"cone\": 0.5,\n \"ground\": \"stop\"\n }\n}\n","import kindData from '../data/sprite-kinds.json'\n\n/** The kinds of sprite the game's pools make: debris, sparks, smoke, steam, drips... (the destruction renderer's old `ParticleKind`). */\nexport type SpriteKind =\n | 'fire' | 'smoke' | 'sparks' | 'dust' | 'shards' | 'splinters' | 'chips' | 'fibers' | 'leaves' | 'drips' | 'ash' | 'steam' | 'flash' | 'foam'\n\n/** What happens to a sprite that reaches the ground. */\nexport type SpriteGround = 'none' | 'bounce' | 'stop' | 'land'\n\nexport interface SpriteKindDef {\n readonly id: SpriteKind\n readonly additive: boolean\n /** Life range in seconds. */\n readonly life: readonly [number, number]\n /** Start size range in metres (billboard diameter). */\n readonly size: readonly [number, number]\n /** Size multiplier reached at the end of the life. */\n readonly grow: number\n /** Gravity multiplier (1 = full gravity). */\n readonly gravity: number\n /** Constant upward acceleration in m/s² (heat, steam). */\n readonly rise: number\n /** Velocity damping per second. */\n readonly drag: number\n /** `0xRRGGBB` at the start and at the end of the life. */\n readonly c0: number\n readonly c1: number\n readonly alpha: number\n /** Fraction of the life spent fading in. */\n readonly fadeIn: number\n /** How much of the emission direction follows the surface normal (1) versus a random direction (0). */\n readonly cone: number\n readonly ground: SpriteGround\n readonly flutter?: number\n readonly flicker?: number\n}\n\nexport const SPRITE_KINDS: Readonly<Record<SpriteKind, SpriteKindDef>> = kindData as unknown as Record<SpriteKind, SpriteKindDef>\n\n/** A copy of the kinds with some changed (a partial replaces the fields it names), added or removed (`null`); a game tunes its sprites without touching the core. */\nexport function patchSpriteKinds(\n base: Readonly<Record<string, SpriteKindDef>>,\n changes: Readonly<Record<string, Partial<SpriteKindDef> | SpriteKindDef | null>>,\n): Record<string, SpriteKindDef> {\n const result: Record<string, SpriteKindDef> = { ...base }\n for (const [id, change] of Object.entries(changes)) {\n if (change === null) delete result[id]\n else result[id] = { ...(base[id] ?? {}), ...change, id } as SpriteKindDef\n }\n return result\n}\n","import { SpriteKindPool, spriteRandom } from './kind-pool'\nimport type { SpriteSpawnOptions } from './kind-pool'\nimport { SPRITE_KINDS } from './kinds'\nimport type { SpriteKind, SpriteKindDef } from './kinds'\n\ntype Vec3 = readonly [number, number, number]\n\nexport interface SpriteKindSystemOptions {\n /** Most live sprites at once (additive takes 40 %, normal the rest). */\n readonly capacity: number\n /** Count multiplier for bursts (0.6 on low quality). */\n readonly density: number\n readonly seed: number\n readonly groundAt: (x: number, z: number) => number\n readonly gravity: number\n readonly kinds?: Readonly<Record<string, SpriteKindDef>>\n}\n\nexport type LandListener = (x: number, y: number, z: number, color: number, tag: number) => void\n\n/**\n * The game's sprites of fixed kinds (debris, sparks, smoke, steam, drips...), burst or spawned one by one: two pooled rings, additive and normal, that\n * a renderer draws as points. This is the destruction renderer's old particle system with the maths unchanged and the drawing taken out.\n */\nexport class SpriteKindSystem {\n readonly additive: SpriteKindPool\n readonly normal: SpriteKindPool\n private readonly random: () => number\n private readonly ids: readonly string[]\n private readonly defs: readonly SpriteKindDef[]\n private readonly index = new Map<string, number>()\n private readonly owners: string[] = []\n private readonly listeners = new Map<string, LandListener>()\n\n constructor(private readonly options: SpriteKindSystemOptions) {\n const kinds: Readonly<Record<string, SpriteKindDef>> = options.kinds ?? SPRITE_KINDS\n this.ids = Object.keys(kinds)\n this.defs = this.ids.map((id) => kinds[id]!)\n this.ids.forEach((id, at) => this.index.set(id, at))\n const additiveCapacity = Math.floor(options.capacity * 0.4)\n this.additive = new SpriteKindPool(additiveCapacity, this.defs, this.owners)\n this.normal = new SpriteKindPool(options.capacity - additiveCapacity, this.defs, this.owners)\n this.random = spriteRandom(options.seed)\n }\n\n get capacity(): number {\n return this.additive.capacity + this.normal.capacity\n }\n\n get live(): number {\n return this.additive.live + this.normal.live\n }\n\n /** Hears the landing of the drips of `owner` (the ones spawned with `{ owner, tag }`). Returns the function that stops listening. */\n onLand(owner: string, listener: LandListener): () => void {\n this.listeners.set(owner, listener)\n return () => {\n if (this.listeners.get(owner) === listener) this.listeners.delete(owner)\n }\n }\n\n private kindOf(kind: string): { index: number; def: SpriteKindDef } {\n const index = this.index.get(kind)\n if (index === undefined) throw new Error(`unknown sprite kind ${kind}`)\n return { index, def: this.defs[index]! }\n }\n\n /** One sprite with an explicit velocity. */\n spawn(kind: SpriteKind | string, point: Vec3, velocity: Vec3, options?: SpriteSpawnOptions): void {\n const { index, def } = this.kindOf(kind)\n ;(def.additive ? this.additive : this.normal).spawn(index, point, velocity, this.random, options)\n }\n\n /** `count` sprites leaving `point` around `normal` at about `speed` m/s. */\n burst(kind: SpriteKind | string, point: Vec3, normal: Vec3, count: number, speed = 1, options?: SpriteSpawnOptions): void {\n const { def } = this.kindOf(kind)\n const n = Math.max(0, Math.round(count * this.options.density))\n const rnd = this.random\n const length = Math.hypot(normal[0], normal[1], normal[2]) || 1\n const nx = normal[0] / length\n const ny = normal[1] / length\n const nz = normal[2] / length\n for (let k = 0; k < n; k++) {\n const u = rnd() * 2 - 1\n const phase = rnd() * Math.PI * 2\n const s = Math.sqrt(1 - u * u)\n const rx = s * Math.cos(phase)\n const ry = u\n const rz = s * Math.sin(phase)\n let dx = nx * def.cone + rx * (1 - def.cone)\n let dy = ny * def.cone + ry * (1 - def.cone)\n let dz = nz * def.cone + rz * (1 - def.cone)\n // a little scatter even for a pure cone, and never push into the surface for debris\n dx += rx * 0.35\n dy += ry * 0.35\n dz += rz * 0.35\n const l = Math.hypot(dx, dy, dz) || 1\n const sp = speed * (0.35 + 0.65 * rnd())\n const jitter = 0.01 + (def.size[1] > 0.5 ? 0.15 : 0.02)\n this.spawn(\n def.id,\n [point[0] + (rnd() - 0.5) * jitter, point[1] + (rnd() - 0.5) * jitter, point[2] + (rnd() - 0.5) * jitter],\n [(dx / l) * sp, (dy / l) * sp, (dz / l) * sp],\n options,\n )\n }\n }\n\n /** Blast wave: shoves the live sprites away from a point. */\n push(point: Vec3, radius: number, speed: number): void {\n this.additive.push(point[0], point[1], point[2], radius, speed)\n this.normal.push(point[0], point[1], point[2], radius, speed)\n }\n\n update(dt: number): void {\n const host = {\n groundAt: this.options.groundAt,\n gravity: this.options.gravity,\n land: (owner: string, x: number, y: number, z: number, color: number, tag: number) => this.listeners.get(owner)?.(x, y, z, color, tag),\n }\n if (this.additive.live > 0 || this.additive.drawCount > 0) this.additive.update(dt, host)\n if (this.normal.live > 0 || this.normal.drawCount > 0) this.normal.update(dt, host)\n }\n\n clear(): void {\n this.additive.clear()\n this.normal.clear()\n }\n}\n","import type { RandomStream, MutableVec3 } from '../random'\nimport { addScaled, normalize } from '../vector'\nimport type { SpriteRange, Vec3Tuple } from './types'\n\nconst TWO_PI_ROUNDED = 6.2832\nconst WIDE_CONE = 3.1\nconst UP: Vec3Tuple = [0, 1, 0]\n\nexport function drawRange(random: RandomStream, range: SpriteRange | undefined): number {\n if (Array.isArray(range)) return random.between(range[0], range[1])\n return (range as number | undefined) || 0\n}\n\n/** `randUnit` (W166): the page rounds a turn to 6.2832, so this is not `RandomStream.direction`. */\nexport function randomUnit(random: RandomStream, out: MutableVec3): MutableVec3 {\n const up = random.next() * 2 - 1\n const angle = random.next() * TWO_PI_ROUNDED\n const ring = Math.sqrt(1 - up * up)\n out.x = ring * Math.cos(angle)\n out.y = up\n out.z = ring * Math.sin(angle)\n return out\n}\n\nconst tangentA: MutableVec3 = { x: 0, y: 0, z: 0 }\nconst tangentB: MutableVec3 = { x: 0, y: 0, z: 0 }\nconst axis: MutableVec3 = { x: 0, y: 0, z: 0 }\n\nfunction cross(out: MutableVec3, a: MutableVec3, b: MutableVec3): MutableVec3 {\n const x = a.y * b.z - a.z * b.y\n const y = a.z * b.x - a.x * b.z\n const z = a.x * b.y - a.y * b.x\n out.x = x\n out.y = y\n out.z = z\n return out\n}\n\n/** `coneDir` (W169-171): a direction inside `cone` radians of `direction`, two draws (a full sphere when the cone is wide). */\nexport function coneDirection(random: RandomStream, direction: Vec3Tuple | undefined, cone: number, out: MutableVec3): MutableVec3 {\n if (cone >= WIDE_CONE) return randomUnit(random, out)\n const [dx, dy, dz] = direction ?? UP\n const cosTilt = 1 - random.next() * (1 - Math.cos(cone))\n const sinTilt = Math.sqrt(1 - cosTilt * cosTilt)\n const phase = random.next() * TWO_PI_ROUNDED\n axis.x = dx\n axis.y = dy\n axis.z = dz\n tangentA.x = 1\n tangentA.y = 0\n tangentA.z = 0\n if (Math.abs(dx) > 0.9) {\n tangentA.x = 0\n tangentA.y = 1\n }\n normalize(cross(tangentB, axis, tangentA))\n normalize(cross(tangentA, tangentB, axis))\n out.x = dx * cosTilt\n out.y = dy * cosTilt\n out.z = dz * cosTilt\n addScaled(out, tangentA, sinTilt * Math.cos(phase))\n addScaled(out, tangentB, sinTilt * Math.sin(phase))\n return out\n}\n","import type { MutableVec3 } from '../random'\nimport { coneDirection, drawRange } from './direction'\nimport type { DebrisSpawn, WorkshopContext } from './types'\n\nconst CAPACITY = 500\nconst GRAVITY = 9.8\nconst QUARTER_TURN = 1.5708\nconst FADE_SECONDS = 0.5\nconst DEFAULT_SIZE = [0.05, 0.05, 0.05] as const\nconst DEFAULT_SPIN = 12\nconst DEFAULT_COLOR = 0x888888\nconst DEFAULT_LIFE = 8\nconst DEFAULT_BOUNCE = 0.3\nconst FULL_TURN_DRAWN = 6\n\nexport interface DebrisPiece {\n on: boolean\n x: number\n y: number\n z: number\n vx: number\n vy: number\n vz: number\n sx: number\n sy: number\n sz: number\n rx: number\n ry: number\n rz: number\n ax: number\n ay: number\n az: number\n life: number\n bounce: number\n rest: boolean\n color: number\n}\n\nfunction createPiece(): DebrisPiece {\n return { on: false, x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, sx: 0, sy: 0, sz: 0, rx: 0, ry: 0, rz: 0, ax: 0, ay: 0, az: 0, life: 0, bounce: 0, rest: false, color: DEFAULT_COLOR }\n}\n\n/** The Workshop's physics debris (W248-263): 500 boxes in a ring buffer, the newest overwriting the oldest, that tumble, bounce, settle on a face and shrink away. */\nexport class DebrisPool {\n readonly pieces: DebrisPiece[] = Array.from({ length: CAPACITY }, createPiece)\n count = 0\n private next = 0\n private readonly direction: MutableVec3 = { x: 0, y: 0, z: 0 }\n\n constructor(private readonly context: WorkshopContext) {}\n\n spawn(options: DebrisSpawn): void {\n const random = this.context.random\n const piece = this.pieces[this.next++ % CAPACITY]!\n coneDirection(random, options.dir, options.cone ?? 3.2, this.direction)\n const speed = drawRange(random, options.sp)\n const size = options.size ?? DEFAULT_SIZE\n const scale = options.k ? drawRange(random, options.k) : 1\n const box = options.box\n const spin = options.spin || DEFAULT_SPIN\n piece.on = true\n piece.x = options.p[0] + (box ? random.between(-box[0], box[0]) : 0)\n piece.y = options.p[1] + (box ? random.between(-box[1], box[1]) : 0)\n piece.z = options.p[2] + (box ? random.between(-box[2], box[2]) : 0)\n piece.vx = this.direction.x * speed + (options.v ? options.v[0] : 0)\n piece.vy = this.direction.y * speed + (options.v ? options.v[1] : 0)\n piece.vz = this.direction.z * speed + (options.v ? options.v[2] : 0)\n piece.sx = (size[0] as number) * scale\n piece.sy = (size[1] as number) * scale\n piece.sz = (size[2] as number) * scale\n piece.rx = random.between(0, FULL_TURN_DRAWN)\n piece.ry = random.between(0, FULL_TURN_DRAWN)\n piece.rz = random.between(0, FULL_TURN_DRAWN)\n piece.ax = random.between(-1, 1) * spin\n piece.ay = random.between(-1, 1) * spin\n piece.az = random.between(-1, 1) * spin\n piece.life = options.life || DEFAULT_LIFE\n piece.bounce = options.bounce ?? DEFAULT_BOUNCE\n piece.rest = false\n piece.color = options.c ?? DEFAULT_COLOR\n }\n\n clear(): void {\n for (const piece of this.pieces) piece.on = false\n this.count = 0\n }\n\n update(dt: number): void {\n this.count = 0\n for (const piece of this.pieces) {\n if (!piece.on) continue\n this.count++\n piece.life -= dt\n if (piece.life <= 0) {\n piece.on = false\n continue\n }\n if (!piece.rest) this.move(piece, dt)\n }\n }\n\n /** Scale at the end of its life, 1 until the last half second (the renderer's `sh`). */\n static fade(piece: DebrisPiece): number {\n return Math.min(1, piece.life / FADE_SECONDS)\n }\n\n private move(piece: DebrisPiece, dt: number): void {\n piece.vy -= GRAVITY * dt\n piece.x += piece.vx * dt\n piece.y += piece.vy * dt\n piece.z += piece.vz * dt\n piece.rx += piece.ax * dt\n piece.ry += piece.ay * dt\n piece.rz += piece.az * dt\n const groundHeight = this.context.groundAt(piece.x, piece.z) + Math.min(piece.sx, piece.sy, piece.sz) * 0.5\n if (piece.y >= groundHeight) return\n piece.y = groundHeight\n piece.vy *= -piece.bounce\n piece.vx *= 0.55\n piece.vz *= 0.55\n piece.ax *= 0.5\n piece.ay *= 0.5\n piece.az *= 0.5\n if (Math.abs(piece.vy) < 0.5 && Math.hypot(piece.vx, piece.vz) < 0.3) {\n piece.rest = true\n piece.rx = Math.round(piece.rx / QUARTER_TURN) * QUARTER_TURN\n piece.rz = Math.round(piece.rz / QUARTER_TURN) * QUARTER_TURN\n }\n }\n}\n","import type { DecalOptions, Vec3Tuple, WorkshopContext } from './types'\n\nconst CAPACITY = 400\nconst UP: Vec3Tuple = [0, 1, 0]\nconst DEFAULT_LIFE = 60\nconst DEFAULT_GROW = 1.2\n\n/** The seconds a fluid decal waits and then takes to evaporate away (W276); blood, bile and oil only darken. */\nconst EVAPORATION = { fuel: [6, 5], water: [40, 20] } as const\n\nexport interface Decal {\n on: boolean\n x: number\n y: number\n z: number\n sx: number\n sy: number\n t: number\n life: number\n grow: number\n color: number\n /** Where the surface faces, and the colour a fluid dries to (`-1`: it does not dry). */\n normal: Vec3Tuple\n fluid: DecalOptions['fluid'] | undefined\n dryColor: number\n dryAt: number\n dryTime: number\n}\n\nfunction createDecal(): Decal {\n return { on: false, x: 0, y: 0, z: 0, sx: 0, sy: 0, t: 0, life: 0, grow: 0, color: 0, normal: UP, fluid: undefined, dryColor: -1, dryAt: 0, dryTime: 0 }\n}\n\n/** The Workshop's decals (W265-306): 400 marks on the ground in a ring buffer. Wet ones spread and evaporate (fuel, water), the rest last their life. */\nexport class DecalPool {\n readonly decals: Decal[] = Array.from({ length: CAPACITY }, createDecal)\n count = 0\n private next = 0\n\n constructor(private readonly context: WorkshopContext) {}\n\n add(x: number, y: number, z: number, size: number, color: number, options: DecalOptions = {}): void {\n const random = this.context.random\n const index = this.next++ % CAPACITY\n const decal = this.decals[index]!\n const normal = options.n ?? UP\n random.between(0, 6.28)\n const offset = 0.004 + (index % 40) * 0.0004\n decal.on = true\n decal.x = x + normal[0] * offset\n decal.y = y + normal[1] * offset\n decal.z = z + normal[2] * offset\n decal.sx = size * (options.ar ? 1 : random.between(0.8, 1.2))\n decal.sy = size * (options.ar || random.between(0.65, 1.1))\n decal.t = -(options.delay || 0)\n decal.grow = options.grow ?? DEFAULT_GROW\n decal.life = options.life || DEFAULT_LIFE\n decal.color = color\n decal.normal = normal\n const fluid = options.fluid ?? (options.dark ? 'blood' : undefined)\n decal.fluid = fluid\n decal.dryAt = options.dark && !options.fluid ? 0 : options.dryAt ?? (fluid === 'blood' || fluid === 'bile' ? 40 : 0)\n decal.dryTime = options.dark && !options.fluid ? options.dark : options.dryT ?? (fluid === 'blood' || fluid === 'bile' ? 30 : 0)\n decal.dryColor = options.dry ?? (fluid === 'bile' ? 0x3e4220 : -1)\n }\n\n clear(): void {\n for (const decal of this.decals) decal.on = false\n this.count = 0\n }\n\n /**\n * Takes away the marks inside a box: world `center`, `half` extents along the axes of the world `rotation` (a quaternion, x y z w; none means the\n * world axes). The surface they were on is gone (a brick came out of a wall with its bullet holes). Returns how many went.\n */\n clearInside(center: Vec3Tuple, half: Vec3Tuple, rotation: readonly [number, number, number, number] = [0, 0, 0, 1]): number {\n // the inverse rotation takes a point into the box's axes: v' = v + 2w(q x v) + 2 q x (q x v) with q = -xyz of the rotation\n const qx = -rotation[0], qy = -rotation[1], qz = -rotation[2], qw = rotation[3]\n let gone = 0\n for (const decal of this.decals) {\n if (!decal.on) continue\n const vx = decal.x - center[0], vy = decal.y - center[1], vz = decal.z - center[2]\n const tx = 2 * (qy * vz - qz * vy), ty = 2 * (qz * vx - qx * vz), tz = 2 * (qx * vy - qy * vx)\n const lx = vx + qw * tx + (qy * tz - qz * ty), ly = vy + qw * ty + (qz * tx - qx * tz), lz = vz + qw * tz + (qx * ty - qy * tx)\n if (Math.abs(lx) > half[0] || Math.abs(ly) > half[1] || Math.abs(lz) > half[2]) continue\n decal.on = false\n gone++\n }\n return gone\n }\n\n update(dt: number): void {\n this.count = 0\n for (const decal of this.decals) {\n if (!decal.on) continue\n this.count++\n decal.t += dt\n if (decal.t < 0) continue\n decal.life -= dt\n if (decal.life <= 0) {\n decal.on = false\n continue\n }\n const evaporation = decal.fluid === 'fuel' || decal.fluid === 'water' ? EVAPORATION[decal.fluid] : undefined\n if (evaporation && (decal.t - evaporation[0]) / evaporation[1] >= 1) decal.on = false\n }\n }\n}\n","import type { MeshSpawn, MeshShape, Vec3Tuple } from './types'\n\nconst CAPACITY = 400\nconst IDENTITY: Quaternion = [0, 0, 0, 1]\n\n/** A unit quaternion `[x, y, z, w]`. */\nexport type Quaternion = readonly [number, number, number, number]\n\n/** One solid particle as the pool keeps it; a renderer draws it from these fields and never changes them. */\nexport interface MeshParticle {\n /** Grows by one for every particle ever spawned: a renderer knows a new one by it. */\n readonly id: number\n readonly shape: MeshShape\n readonly radius: number\n readonly height: number\n readonly segments: number\n readonly segments2: number\n readonly color: number\n readonly additive: boolean\n readonly lit: boolean\n /** Where the group is, how it is turned (placement and roll), where the piece sits in it (before the scale) and how the piece is turned in it (Euler XYZ, changes with `spin`). */\n x: number\n y: number\n z: number\n readonly frame: Quaternion\n readonly at: Vec3Tuple\n rx: number\n ry: number\n rz: number\n scale: number\n opacity: number\n age: number\n readonly life: number\n vx: number\n vy: number\n vz: number\n readonly spin: Vec3Tuple\n readonly gravity: number\n readonly drag: number\n readonly grow: number\n readonly ramp: readonly [number, number] | undefined\n readonly fade: boolean\n readonly startOpacity: number\n}\n\nfunction multiply(a: Quaternion, b: Quaternion): Quaternion {\n const [ax, ay, az, aw] = a\n const [bx, by, bz, bw] = b\n return [aw * bx + ax * bw + ay * bz - az * by, aw * by - ax * bz + ay * bw + az * bx, aw * bz + ax * by - ay * bx + az * bw, aw * bw - ax * bx - ay * by - az * bz]\n}\n\n/** The turn of `angle` about the x axis. */\nconst aboutX = (angle: number): Quaternion => [Math.sin(angle / 2), 0, 0, Math.cos(angle / 2)]\n\n/**\n * The solids of the Workshop's recipes: the gallery's `Particles` (thrown with a velocity and a spin, slowed, lifted by a negative gravity, grown and faded) without\n * the floor, in order of birth. The oldest goes when the pool is full. Each frame does what the gallery's did, in its order: age, fade, gravity, drag, move, spin, grow.\n */\nexport class MeshPool {\n readonly particles: MeshParticle[] = []\n private serial = 0\n\n constructor(readonly capacity: number = CAPACITY) {}\n\n get count(): number {\n return this.particles.length\n }\n\n spawn(options: MeshSpawn, frame: Quaternion = IDENTITY): MeshParticle {\n const velocity = options.v ?? [0, 0, 0]\n const world = options.wv ?? [0, 0, 0]\n const particle: MeshParticle = {\n id: ++this.serial,\n shape: options.shape,\n radius: options.r,\n height: options.h ?? 0,\n segments: options.seg ?? 0,\n segments2: options.seg2 ?? 0,\n color: options.c0,\n additive: options.add === true,\n lit: options.lit === true,\n x: options.p[0],\n y: options.p[1],\n z: options.p[2],\n frame: options.roll ? multiply(frame, aboutX(options.roll)) : frame,\n at: options.at ?? [0, 0, 0],\n rx: options.rot?.[0] ?? 0,\n ry: options.rot?.[1] ?? 0,\n rz: options.rot?.[2] ?? 0,\n scale: 1,\n opacity: options.a,\n age: 0,\n life: options.life,\n vx: velocity[0] + world[0],\n vy: velocity[1] + world[1],\n vz: velocity[2] + world[2],\n spin: options.spin ?? [0, 0, 0],\n gravity: options.g ?? 0,\n drag: options.drag ?? 0,\n grow: options.grow ?? 0,\n ramp: options.ramp,\n fade: options.fade === true,\n startOpacity: options.a,\n }\n this.particles.push(particle)\n if (this.particles.length > this.capacity) this.particles.shift()\n return particle\n }\n\n update(dt: number): void {\n const live = this.particles\n for (let index = 0; index < live.length; index++) {\n const particle = live[index]!\n particle.age += dt\n if (particle.ramp ? particle.age >= particle.life : particle.age > particle.life) {\n live.splice(index--, 1)\n continue\n }\n if (particle.fade) particle.opacity = Math.max(0, particle.startOpacity * (1 - particle.age / particle.life))\n particle.vy -= particle.gravity * dt\n if (particle.drag) {\n const keep = Math.max(0, 1 - particle.drag * dt)\n particle.vx *= keep\n particle.vy *= keep\n particle.vz *= keep\n }\n particle.x += particle.vx * dt\n particle.y += particle.vy * dt\n particle.z += particle.vz * dt\n particle.rx += particle.spin[0] * dt\n particle.ry += particle.spin[1] * dt\n particle.rz += particle.spin[2] * dt\n if (particle.grow) particle.scale *= 1 + particle.grow * dt\n if (particle.ramp) particle.scale = particle.ramp[0] + Math.min(1, particle.age / particle.life) * particle.ramp[1]\n }\n }\n\n clear(): void {\n this.particles.length = 0\n }\n}\n","import { hexToRgb } from '../color'\nimport { lerp } from '../vector'\nimport type { MutableVec3, RandomStream } from '../random'\nimport { coneDirection, drawRange, randomUnit } from './direction'\nimport type { SpriteSpawn, WorkshopContext } from './types'\n\n/** Colour, size and alpha of a particle at its current age: what the renderer reads (`col`, `siz`, `alp`). */\nconst DEFAULT_FADE_IN = 0.1\nconst FLOATS_PER_VECTOR = 3\nconst BYTE_MAX = 255\n\nexport interface DecalFromSprite {\n (x: number, y: number, z: number, size: number, color: number, options: { readonly grow: number; readonly fluid: 'blood' }): void\n}\n\n/**\n * The Workshop's `PSys` (W179-220): a pool of sprites in typed arrays, as the page keeps them. A dead record is replaced by the last one, so the order\n * in the arrays is the page's, and the golden hashes the arrays as they are. Additive and normal-blend systems are two instances.\n */\nexport class SpritePool {\n count = 0\n readonly position: Float32Array\n readonly velocity: Float32Array\n readonly colorStart: Float32Array\n readonly colorEnd: Float32Array\n readonly life: Float32Array\n readonly maxLife: Float32Array\n readonly startSize: Float32Array\n readonly growth: Float32Array\n readonly startAlpha: Float32Array\n readonly gravity: Float32Array\n readonly drag: Float32Array\n readonly fadeIn: Float32Array\n readonly groundMode: Uint8Array\n readonly decalSize: Float32Array\n /** Written by `update`: what the renderer draws. */\n readonly color: Float32Array\n readonly size: Float32Array\n readonly alpha: Float32Array\n private readonly vectorArrays: readonly Float32Array[]\n private readonly scalarArrays: readonly (Float32Array | Uint8Array)[]\n private readonly direction: MutableVec3 = { x: 0, y: 0, z: 0 }\n private readonly unit: MutableVec3 = { x: 0, y: 0, z: 0 }\n\n constructor(readonly capacity: number, private readonly context: WorkshopContext) {\n const vectorArray = (): Float32Array => new Float32Array(capacity * FLOATS_PER_VECTOR)\n const scalarArray = (): Float32Array => new Float32Array(capacity)\n this.position = vectorArray()\n this.velocity = vectorArray()\n this.colorStart = vectorArray()\n this.colorEnd = vectorArray()\n this.color = vectorArray()\n this.life = scalarArray()\n this.maxLife = scalarArray()\n this.startSize = scalarArray()\n this.growth = scalarArray()\n this.startAlpha = scalarArray()\n this.gravity = scalarArray()\n this.drag = scalarArray()\n this.fadeIn = scalarArray()\n this.decalSize = scalarArray()\n this.size = scalarArray()\n this.alpha = scalarArray()\n this.groundMode = new Uint8Array(capacity)\n this.vectorArrays = [this.position, this.velocity, this.colorStart, this.colorEnd]\n this.scalarArrays = [this.life, this.maxLife, this.startSize, this.growth, this.startAlpha, this.gravity, this.drag, this.fadeIn, this.groundMode, this.decalSize]\n }\n\n spawn(options: SpriteSpawn): void {\n if (this.count >= this.capacity) return\n const random: RandomStream = this.context.random\n const index = this.count++\n const offset = index * FLOATS_PER_VECTOR\n let [x, y, z] = options.p\n if (options.r) {\n randomUnit(random, this.unit)\n const reach = Math.cbrt(random.next()) * options.r\n x += this.unit.x * reach\n y += this.unit.y * reach * (options.flat ? 0 : 1)\n z += this.unit.z * reach\n }\n if (options.box) {\n x += random.between(-options.box[0], options.box[0])\n y += random.between(-options.box[1], options.box[1])\n z += random.between(-options.box[2], options.box[2])\n }\n this.position[offset] = x\n this.position[offset + 1] = y\n this.position[offset + 2] = z\n coneDirection(random, options.dir, options.cone ?? 3.2, this.direction)\n const speed = drawRange(random, options.sp)\n this.velocity[offset] = this.direction.x * speed + (options.v ? options.v[0] : 0)\n this.velocity[offset + 1] = this.direction.y * speed + (options.v ? options.v[1] : 0)\n this.velocity[offset + 2] = this.direction.z * speed + (options.v ? options.v[2] : 0)\n const start = hexToRgb(options.c0 ?? 0xffffff)\n const end = options.c1 != null ? hexToRgb(options.c1) : start\n for (let channel = 0; channel < FLOATS_PER_VECTOR; channel++) {\n this.colorStart[offset + channel] = start[channel]\n this.colorEnd[offset + channel] = end[channel]\n }\n this.life[index] = this.maxLife[index] = drawRange(random, options.life || 1)\n this.startSize[index] = drawRange(random, options.size || 0.1)\n this.growth[index] = options.grow || 1\n this.startAlpha[index] = options.a ?? 1\n this.gravity[index] = options.g || 0\n this.drag[index] = options.drag || 0\n this.fadeIn[index] = options.fi ?? DEFAULT_FADE_IN\n this.groundMode[index] = options.gnd || 0\n this.decalSize[index] = options.ds ? drawRange(random, options.ds) : 0.1\n }\n\n kill(index: number): void {\n const last = --this.count\n if (index === last) return\n for (const array of this.vectorArrays) {\n for (let component = 0; component < FLOATS_PER_VECTOR; component++) array[index * FLOATS_PER_VECTOR + component] = array[last * FLOATS_PER_VECTOR + component]\n }\n for (const array of this.scalarArrays) array[index] = array[last]\n }\n\n clear(): void {\n this.count = 0\n }\n\n update(dt: number, leaveDecal: DecalFromSprite): void {\n const { position, velocity } = this\n for (let index = this.count - 1; index >= 0; index--) {\n this.life[index] -= dt\n if (this.life[index] <= 0) {\n this.kill(index)\n continue\n }\n const offset = index * FLOATS_PER_VECTOR\n const age = 1 - this.life[index] / this.maxLife[index]\n velocity[offset + 1] -= this.gravity[index] * dt\n const factor = Math.max(0, 1 - this.drag[index] * dt)\n velocity[offset] *= factor\n velocity[offset + 1] *= factor\n velocity[offset + 2] *= factor\n position[offset] += velocity[offset] * dt\n position[offset + 1] += velocity[offset + 1] * dt\n position[offset + 2] += velocity[offset + 2] * dt\n const mode = this.groundMode[index]\n if (mode) {\n const groundHeight = this.context.groundAt(position[offset], position[offset + 2])\n if (position[offset + 1] < groundHeight) {\n if (mode === 1) {\n this.kill(index)\n continue\n }\n if (mode === 2) {\n position[offset + 1] = groundHeight + 0.001\n velocity[offset + 1] *= -0.3\n velocity[offset] *= 0.55\n velocity[offset + 2] *= 0.55\n }\n if (mode === 3) {\n if (this.context.quality.level < 2) {\n const red = (this.colorStart[offset] * BYTE_MAX) << 16\n const green = (this.colorStart[offset + 1] * BYTE_MAX) << 8\n leaveDecal(position[offset], groundHeight, position[offset + 2], this.decalSize[index], red | green | (this.colorStart[offset + 2] * BYTE_MAX), { grow: 0.15, fluid: 'blood' })\n }\n this.kill(index)\n continue\n }\n }\n }\n this.size[index] = this.startSize[index] * (1 + (this.growth[index] - 1) * age)\n const fadeIn = this.fadeIn[index]\n this.alpha[index] = this.startAlpha[index] * (fadeIn > 0 && age < fadeIn ? age / fadeIn : 1 - (age - fadeIn) / (1 - fadeIn))\n for (let channel = 0; channel < FLOATS_PER_VECTOR; channel++) this.color[offset + channel] = lerp(this.colorStart[offset + channel], this.colorEnd[offset + channel], age)\n }\n }\n}\n","import { hexToRgb } from '../color'\nimport type { MutableVec3 } from '../random'\nimport { lerp } from '../vector'\nimport { coneDirection, drawRange } from './direction'\nimport type { SpriteSpawn, WorkshopContext } from './types'\n\nconst FLOATS_PER_VECTOR = 3\nconst SEGMENT_FLOATS = 6\nconst TAIL_BRIGHTNESS = 0.15\n\n/** The Workshop's `Streaks` (W223-245): short lines that follow their own velocity (sparks, tracers, rain-like glints). Same swap-with-last pool as `SpritePool`. */\nexport class StreakPool {\n count = 0\n readonly point: Float32Array\n readonly velocity: Float32Array\n readonly colorStart: Float32Array\n readonly colorEnd: Float32Array\n readonly life: Float32Array\n readonly maxLife: Float32Array\n readonly length: Float32Array\n readonly gravity: Float32Array\n readonly drag: Float32Array\n readonly groundMode: Uint8Array\n readonly startAlpha: Float32Array\n /** Two end points and two colours per line: what the renderer draws. */\n readonly segment: Float32Array\n readonly segmentColor: Float32Array\n private readonly vectorArrays: readonly Float32Array[]\n private readonly scalarArrays: readonly (Float32Array | Uint8Array)[]\n private readonly direction: MutableVec3 = { x: 0, y: 0, z: 0 }\n\n constructor(readonly capacity: number, private readonly context: WorkshopContext) {\n const vectorArray = (): Float32Array => new Float32Array(capacity * FLOATS_PER_VECTOR)\n const scalarArray = (): Float32Array => new Float32Array(capacity)\n this.point = vectorArray()\n this.velocity = vectorArray()\n this.colorStart = vectorArray()\n this.colorEnd = vectorArray()\n this.life = scalarArray()\n this.maxLife = scalarArray()\n this.length = scalarArray()\n this.gravity = scalarArray()\n this.drag = scalarArray()\n this.startAlpha = scalarArray()\n this.groundMode = new Uint8Array(capacity)\n this.segment = new Float32Array(capacity * SEGMENT_FLOATS)\n this.segmentColor = new Float32Array(capacity * SEGMENT_FLOATS)\n this.vectorArrays = [this.point, this.velocity, this.colorStart, this.colorEnd]\n this.scalarArrays = [this.life, this.maxLife, this.length, this.gravity, this.drag, this.groundMode, this.startAlpha]\n }\n\n spawn(options: SpriteSpawn): void {\n if (this.count >= this.capacity) return\n const random = this.context.random\n const index = this.count++\n const offset = index * FLOATS_PER_VECTOR\n let [x, y, z] = options.p\n if (options.box) {\n x += random.between(-options.box[0], options.box[0])\n y += random.between(-options.box[1], options.box[1])\n z += random.between(-options.box[2], options.box[2])\n }\n this.point[offset] = x\n this.point[offset + 1] = y\n this.point[offset + 2] = z\n coneDirection(random, options.dir, options.cone ?? 3.2, this.direction)\n const speed = drawRange(random, options.sp)\n this.velocity[offset] = this.direction.x * speed + (options.v ? options.v[0] : 0)\n this.velocity[offset + 1] = this.direction.y * speed + (options.v ? options.v[1] : 0)\n this.velocity[offset + 2] = this.direction.z * speed + (options.v ? options.v[2] : 0)\n const start = hexToRgb(options.c0 ?? 0xffd080)\n const end = options.c1 != null ? hexToRgb(options.c1) : start\n for (let channel = 0; channel < FLOATS_PER_VECTOR; channel++) {\n this.colorStart[offset + channel] = start[channel]\n this.colorEnd[offset + channel] = end[channel]\n }\n this.life[index] = this.maxLife[index] = drawRange(random, options.life || 0.4)\n this.length[index] = options.len || 0.02\n this.gravity[index] = options.g || 0\n this.drag[index] = options.drag || 0\n this.groundMode[index] = options.gnd || 0\n this.startAlpha[index] = options.a ?? 1\n }\n\n kill(index: number): void {\n const last = --this.count\n if (index === last) return\n for (const array of this.vectorArrays) {\n for (let component = 0; component < FLOATS_PER_VECTOR; component++) array[index * FLOATS_PER_VECTOR + component] = array[last * FLOATS_PER_VECTOR + component]\n }\n for (const array of this.scalarArrays) array[index] = array[last]\n }\n\n clear(): void {\n this.count = 0\n }\n\n update(dt: number): void {\n const { point, velocity } = this\n for (let index = this.count - 1; index >= 0; index--) {\n this.life[index] -= dt\n if (this.life[index] <= 0) {\n this.kill(index)\n continue\n }\n const offset = index * FLOATS_PER_VECTOR\n const age = 1 - this.life[index] / this.maxLife[index]\n velocity[offset + 1] -= this.gravity[index] * dt\n const factor = Math.max(0, 1 - this.drag[index] * dt)\n velocity[offset] *= factor\n velocity[offset + 1] *= factor\n velocity[offset + 2] *= factor\n point[offset] += velocity[offset] * dt\n point[offset + 1] += velocity[offset + 1] * dt\n point[offset + 2] += velocity[offset + 2] * dt\n if (this.groundMode[index]) {\n const groundHeight = this.context.groundAt(point[offset], point[offset + 2])\n if (point[offset + 1] < groundHeight) {\n if (this.groundMode[index] === 1) {\n this.kill(index)\n continue\n }\n point[offset + 1] = groundHeight + 0.001\n velocity[offset + 1] *= -0.35\n velocity[offset] *= 0.6\n velocity[offset + 2] *= 0.6\n }\n }\n const length = this.length[index]\n const alpha = this.startAlpha[index] * (1 - age)\n const at = index * SEGMENT_FLOATS\n this.segment[at] = point[offset]\n this.segment[at + 1] = point[offset + 1]\n this.segment[at + 2] = point[offset + 2]\n this.segment[at + 3] = point[offset] - velocity[offset] * length\n this.segment[at + 4] = point[offset + 1] - velocity[offset + 1] * length\n this.segment[at + 5] = point[offset + 2] - velocity[offset + 2] * length\n for (let channel = 0; channel < FLOATS_PER_VECTOR; channel++) {\n const shade = lerp(this.colorStart[offset + channel], this.colorEnd[offset + channel], age) * alpha\n this.segmentColor[at + channel] = shade\n this.segmentColor[at + 3 + channel] = shade * TAIL_BRIGHTNESS\n }\n }\n }\n}\n","import type { Vec3Tuple, WorkshopContext } from './types'\n\nconst RING_CAPACITY = 120\nconst LIGHT_CAPACITY = 4\nconst UP: Vec3Tuple = [0, 1, 0]\nconst DEFAULT_LIGHT_DISTANCE = 14\n\nexport interface WaveRing {\n on: boolean\n point: Vec3Tuple\n from: number\n to: number\n t: number\n duration: number\n color: number\n normal: Vec3Tuple\n hold: number\n}\n\n/** The Workshop's expanding rings on the ground (W308-315): 120 in a ring buffer. A ring with `hold` stays at that brightness until cleared. */\nexport class WaveRingPool {\n readonly rings: WaveRing[] = Array.from({ length: RING_CAPACITY }, () => ({ on: false, point: UP, from: 0, to: 0, t: 0, duration: 1, color: 0, normal: UP, hold: 0 }))\n count = 0\n private next = 0\n\n add(point: Vec3Tuple, from: number, to: number, duration: number, color: number, normal?: Vec3Tuple, hold?: number): void {\n const ring = this.rings[this.next++ % RING_CAPACITY]!\n ring.on = true\n ring.point = [point[0], point[1], point[2]]\n ring.from = from\n ring.to = to\n ring.t = 0\n ring.duration = duration\n ring.color = color\n ring.normal = normal ?? UP\n ring.hold = hold || 0\n }\n\n clear(): void {\n for (const ring of this.rings) ring.on = false\n this.count = 0\n }\n\n update(dt: number): void {\n this.count = 0\n for (const ring of this.rings) {\n if (!ring.on) continue\n this.count++\n ring.t += dt\n if (ring.t / ring.duration >= 1 && !ring.hold) ring.on = false\n }\n }\n}\n\nexport interface FlashLight {\n x: number\n y: number\n z: number\n color: number\n distance: number\n peak: number\n duration: number\n t: number\n intensity: number\n /** Holds `peak` until `duration` is over. */\n steady: boolean\n}\n\nexport interface FireLightSlot {\n x: number\n y: number\n z: number\n intensity: number\n}\n\n/** The four short flashes of the Workshop (W333-337), taken in turn, and its two fire lights. */\nexport class FlashLightPool {\n readonly lights: FlashLight[] = Array.from({ length: LIGHT_CAPACITY }, () => ({ x: 0, y: 0, z: 0, color: 0xffc070, distance: DEFAULT_LIGHT_DISTANCE, peak: 0, duration: 1, t: 0, intensity: 0, steady: false }))\n readonly fire: FireLightSlot[] = [\n { x: 0, y: 0, z: 0, intensity: 0 },\n { x: 0, y: 0, z: 0, intensity: 0 },\n ]\n private next = 0\n\n constructor(private readonly context: WorkshopContext) {}\n\n get lit(): number {\n return this.lights.filter((light) => light.intensity > 0).length\n }\n\n flash(point: Vec3Tuple, color: number, intensity: number, duration: number, distance?: number, force?: boolean | 'boom', steady?: boolean): void {\n const { level } = this.context.quality\n if (level >= 1 && !force) return\n if (level >= 2 && force !== 'boom') return\n const light = this.lights[this.next++ % LIGHT_CAPACITY]!\n light.x = point[0]\n light.y = point[1]\n light.z = point[2]\n light.color = color\n light.distance = distance || DEFAULT_LIGHT_DISTANCE\n light.peak = intensity\n light.duration = duration\n light.t = 0\n light.steady = steady === true\n light.intensity = intensity\n }\n\n clear(): void {\n for (const light of this.lights) light.intensity = 0\n for (const light of this.fire) light.intensity = 0\n }\n\n update(dt: number): void {\n for (const light of this.lights) {\n if (light.intensity <= 0) continue\n light.t += dt\n const age = light.t / light.duration\n light.intensity = age >= 1 ? 0 : light.steady ? light.peak : light.peak * (1 - age) * (1 - age)\n }\n }\n}\n","import { PROFILES } from '../budget'\nimport type { RandomStream } from '../random'\nimport { createScreenState, resetScreenState, updateScreen } from '../screen'\nimport type { ScreenState, VfxProfile } from '../types'\nimport { DebrisPool } from './debris'\nimport { DecalPool } from './decals'\nimport { RainField } from './rain'\nimport { MeshPool } from './meshes'\nimport type { MeshParticle, Quaternion } from './meshes'\nimport { SpritePool } from './sprites'\nimport { StreakPool } from './streaks'\nimport type { DebrisSpawn, DecalOptions, MeshSpawn, SpriteSpawn, SpriteSystem, Vec3Tuple, WorkshopContext, WorkshopTrace } from './types'\nimport { FlashLightPool, WaveRingPool } from './waves'\n\nconst QUALITY_MULTIPLIERS = [1, 0.5, 0.2] as const\nconst FIRE_FLICKER_SWAY = 0.25\nconst FIRE_FLICKER_JITTER = 0.08\nconst LENS_FADE = 0.16\n\n/** Water or blood on the camera lens: a soft blob that fades and runs down (W351-352). */\nexport interface LensBlob {\n x: number\n y: number\n radius: number\n color: string\n alpha: number\n fall: number\n}\n\n/** The page's `P0` (W347, W1277): what an effect asks of the post-process for this frame only; it is emptied before every frame's tick. */\nexport interface PostProcess {\n desat?: number\n tunnel?: number\n blur?: number\n cold?: number\n chroma?: number\n hue?: number\n red?: number\n tintAlpha?: number\n tintColor?: string\n}\n\n/**\n * Where an effect is put when it is played at a point instead of on the Workshop's set: what the recipe wrote at `anchor` lands at `origin`, turned\n * about the vertical axis by `yaw`. Positions are moved and turned, directions and velocities only turned. With an `orientation` (a unit quaternion) the turn is\n * that one, pitch and roll included, and `yaw` is not used.\n */\nexport interface Placement {\n readonly origin: Vec3Tuple\n readonly anchor: Vec3Tuple\n readonly yaw: number\n readonly orientation?: Quaternion\n}\n\n/** The vector turned by the unit quaternion (three.js's `applyQuaternion`). */\nfunction rotate(q: Quaternion, v: Vec3Tuple): Vec3Tuple {\n const [qx, qy, qz, qw] = q\n const ix = qw * v[0] + qy * v[2] - qz * v[1]\n const iy = qw * v[1] + qz * v[0] - qx * v[2]\n const iz = qw * v[2] + qx * v[1] - qy * v[0]\n const iw = -qx * v[0] - qy * v[1] - qz * v[2]\n return [ix * qw + iw * -qx + iy * -qz - iz * -qy, iy * qw + iw * -qy + iz * -qx - ix * -qz, iz * qw + iw * -qz + ix * -qy - iy * -qx]\n}\n\nexport interface WorkshopOptions {\n readonly random: RandomStream\n readonly profile?: VfxProfile\n readonly groundAt?: (x: number, z: number) => number\n readonly trace?: WorkshopTrace\n}\n\n/** The default ground of the Workshop scene: a raised kerb 12 cm high between z -4.5 and -2.9 (W29). */\nexport function workshopGround(x: number, z: number): number {\n return z < -2.9 && z > -4.5 && Math.abs(x) < 13 ? 0.12 : 0\n}\n\n/**\n * Everything the Workshop's recipes spawn into and the loop that ages it (W168-371, W1285-1287): sprites, streaks, debris, decals, rings, lights, the\n * screen state and the lens blobs, plus the quality level that thins what a recipe asks for and the accumulators that turn a rate into spawns.\n */\nexport class WorkshopEngine {\n readonly context: WorkshopContext\n readonly add: SpritePool\n readonly normal: SpritePool\n readonly streaks: StreakPool\n readonly debris: DebrisPool\n readonly decals: DecalPool\n readonly rings = new WaveRingPool()\n readonly lights: FlashLightPool\n readonly rain = new RainField(this)\n readonly meshes = new MeshPool()\n readonly screen: ScreenState = createScreenState()\n readonly lens: LensBlob[] = []\n post: PostProcess = {}\n /** Set by whoever runs an effect for the time it runs; `undefined` leaves every number as the recipe wrote it. */\n placement: Placement | undefined\n private readonly accumulators = new Map<string, number>()\n\n constructor(options: WorkshopOptions) {\n const profile = options.profile ?? PROFILES.high\n this.context = { random: options.random, groundAt: options.groundAt ?? workshopGround, quality: { level: 0, mult: 1 }, trace: options.trace }\n this.add = new SpritePool(profile.sprites.add, this.context)\n this.normal = new SpritePool(profile.sprites.normal, this.context)\n this.streaks = new StreakPool(profile.streaks, this.context)\n this.debris = new DebrisPool(this.context)\n this.decals = new DecalPool(this.context)\n this.lights = new FlashLightPool(this.context)\n }\n\n get random(): RandomStream {\n return this.context.random\n }\n\n get quality(): number {\n return this.context.quality.level\n }\n\n setQuality(level: number): void {\n this.context.quality.level = level\n this.context.quality.mult = QUALITY_MULTIPLIERS[level] ?? 1\n }\n\n /** `n` (W340): a count thinned by the quality level, never to zero when something was asked. */\n scaled(count: number): number {\n return Math.max(count > 0 ? 1 : 0, Math.round(count * this.context.quality.mult))\n }\n\n /** A point of the recipe's space in the world (`undefined` placement: as is). */\n place(point: Vec3Tuple): Vec3Tuple {\n const placement = this.placement\n if (!placement) return point\n const { anchor, origin, yaw, orientation } = placement\n if (orientation) {\n const turned = rotate(orientation, [point[0] - anchor[0], point[1] - anchor[1], point[2] - anchor[2]])\n return [origin[0] + turned[0], origin[1] + turned[1], origin[2] + turned[2]]\n }\n const x = point[0] - anchor[0]\n const z = point[2] - anchor[2]\n const cos = Math.cos(yaw)\n const sin = Math.sin(yaw)\n return [origin[0] + cos * x + sin * z, origin[1] + point[1] - anchor[1], origin[2] - sin * x + cos * z]\n }\n\n /** A direction or velocity of the recipe's space in the world. */\n turn(vector: Vec3Tuple): Vec3Tuple {\n const placement = this.placement\n if (!placement) return vector\n if (placement.orientation) return rotate(placement.orientation, vector)\n const cos = Math.cos(placement.yaw)\n const sin = Math.sin(placement.yaw)\n return [cos * vector[0] + sin * vector[2], vector[1], -sin * vector[0] + cos * vector[2]]\n }\n\n private placed<T extends { readonly p: Vec3Tuple; readonly dir?: Vec3Tuple; readonly v?: Vec3Tuple }>(options: T): T {\n if (!this.placement) return options\n return { ...options, p: this.place(options.p), ...(options.dir ? { dir: this.turn(options.dir) } : {}), ...(options.v ? { v: this.turn(options.v) } : {}) }\n }\n\n spawn(system: SpriteSystem, recipe: SpriteSpawn): void {\n const options = this.placed(recipe)\n this.context.trace?.(system, [options])\n if (system === 'add') this.add.spawn(options)\n else if (system === 'normal') this.normal.spawn(options)\n else this.streaks.spawn(options)\n }\n\n burst(system: SpriteSystem, count: number, options: SpriteSpawn): void {\n const total = this.scaled(count)\n for (let spawned = 0; spawned < total; spawned++) this.spawn(system, options)\n }\n\n /** `emit` (W343-345): `rate` per second, thinned by the quality, carried over between frames under `key`. `options` is read again for every particle. */\n emit(key: string, system: SpriteSystem, rate: number, dt: number, options: () => SpriteSpawn): void {\n let carried = (this.accumulators.get(key) ?? 0) + rate * this.context.quality.mult * dt\n while (carried >= 1) {\n carried -= 1\n this.spawn(system, options())\n }\n this.accumulators.set(key, carried)\n }\n\n /** A solid particle (a cone, a sphere): the group sits at `p` turned by the placement, `v` is turned with it and `wv` is not. */\n mesh(recipe: MeshSpawn): MeshParticle {\n const placement = this.placement\n const options: MeshSpawn = placement ? { ...recipe, p: this.place(recipe.p), ...(recipe.v ? { v: this.turn(recipe.v) } : {}) } : recipe\n this.context.trace?.('mesh', [options])\n // a piece of a group (`at`, `roll`) is turned with the placement; a free one (smoke, a flame) keeps its own turn in the world\n const grouped = recipe.at !== undefined || recipe.roll !== undefined\n return this.meshes.spawn(options, placement && grouped ? (placement.orientation ?? ([0, Math.sin(placement.yaw / 2), 0, Math.cos(placement.yaw / 2)] as const)) : undefined)\n }\n\n debrisPiece(recipe: DebrisSpawn): void {\n const options = this.placed(recipe)\n this.context.trace?.('debris', [options])\n this.debris.spawn(options)\n }\n\n decal(recipe: readonly [number, number, number, number, number, DecalOptions?]): void {\n let args = recipe\n if (this.placement) {\n const [x, y, z] = this.place([recipe[0], recipe[1], recipe[2]])\n const options = recipe[5]\n args = [x, y, z, recipe[3], recipe[4], options?.n ? { ...options, n: this.turn(options.n) } : options]\n }\n this.context.trace?.('decal', args)\n this.decals.add(args[0], args[1], args[2], args[3], args[4], args[5])\n }\n\n ring(recipe: readonly [Vec3Tuple, number, number, number, number, Vec3Tuple?, number?]): void {\n const args = this.placement ? ([this.place(recipe[0]), recipe[1], recipe[2], recipe[3], recipe[4], recipe[5] && this.turn(recipe[5]), recipe[6]] as const) : recipe\n this.context.trace?.('ring', args)\n this.rings.add(args[0], args[1], args[2], args[3], args[4], args[5], args[6])\n }\n\n /** `steady` (a seventh number): the light holds its intensity for its duration and goes out, where the Workshop's fades with the square of its age. */\n flash(recipe: readonly [Vec3Tuple, number, number, number, number?, (boolean | 'boom')?, boolean?]): void {\n const args = this.placement ? ([this.place(recipe[0]), recipe[1], recipe[2], recipe[3], recipe[4], recipe[5], recipe[6]] as const) : recipe\n this.context.trace?.('flash', args.slice(0, 6))\n this.lights.flash(args[0], args[1], args[2], args[3], args[4], args[5], args[6])\n }\n\n /** `fireLight` (W400): sways with the clock and flickers with one draw. */\n fireLight(slot: 0 | 1, recipe: Vec3Tuple, intensity: number, time: number): void {\n const light = this.lights.fire[slot]!\n const point = this.place(recipe)\n light.x = point[0]\n light.y = point[1]\n light.z = point[2]\n light.intensity = intensity * (0.75 + FIRE_FLICKER_SWAY * Math.sin(time * 23) * Math.sin(time * 7.3) + this.random.between(-FIRE_FLICKER_JITTER, FIRE_FLICKER_JITTER))\n }\n\n lensBlob(x: number, y: number, radius: number, color: string, alpha: number): void {\n this.lens.push({ x, y, radius, color, alpha, fall: this.random.between(2, 8) })\n }\n\n /** The order of the page's loop (W1285-1287): the sprites, streaks, debris, decals, rings and lights, then the screen and its lens. */\n update(dt: number): void {\n const leaveDecal = (x: number, y: number, z: number, size: number, color: number, options: DecalOptions): void => {\n this.decal([x, y, z, size, color, options])\n }\n this.add.update(dt, leaveDecal)\n this.normal.update(dt, leaveDecal)\n this.streaks.update(dt)\n this.debris.update(dt)\n this.decals.update(dt)\n this.rings.update(dt)\n this.meshes.update(dt)\n this.lights.update(dt)\n updateScreen(this.screen, dt)\n for (let index = this.lens.length - 1; index >= 0; index--) {\n const blob = this.lens[index]!\n blob.alpha -= dt * LENS_FADE\n blob.y += blob.fall * dt\n if (blob.alpha <= 0) this.lens.splice(index, 1)\n }\n }\n\n clear(): void {\n this.add.clear()\n this.normal.clear()\n this.streaks.clear()\n this.debris.clear()\n this.decals.clear()\n this.rings.clear()\n this.meshes.clear()\n this.lights.clear()\n this.rain.clear()\n this.accumulators.clear()\n resetScreenState(this.screen)\n this.lens.length = 0\n this.post = {}\n }\n}\n","import type { Rgb } from './color'\n\nexport type { Rgb }\n\nexport type Vec3Like = readonly [number, number, number]\n\n/** The six layers of an explosion the viewer can turn off one by one (L478): flash and light, fire, shock wave, smoke and dust, shrapnel and debris, marks on the ground. */\nexport type VfxLayer = 'flash' | 'fire' | 'shock' | 'smoke' | 'shrapnel' | 'mark'\n\nexport const VFX_LAYERS: readonly VfxLayer[] = ['flash', 'fire', 'shock', 'smoke', 'shrapnel', 'mark']\n\n/** A chain of things that move: anything with a position, or a position. */\nexport type Follow = { readonly x: number; readonly y: number; readonly z: number } | { readonly position: { readonly x: number; readonly y: number; readonly z: number } }\n\n/** A residual fire left behind by an explosion (L798 `residual`), or started by `world.fire`. */\nexport interface FireSpec {\n /** Offsets from the blast point. */\n readonly dx?: number\n readonly dz?: number\n readonly radius: number\n readonly life: number\n readonly rate?: number\n readonly smoke?: number\n /** Grows from `startRadius` to `radius` over `grow` seconds. */\n readonly startRadius?: number\n readonly grow?: number\n /** `0xRRGGBB` of the smoke the fire gives (default soot black). */\n readonly smokeColor?: number\n}\n\nexport interface MushroomSpec {\n readonly height: number\n readonly radius: number\n readonly count: number\n readonly stem: number\n readonly life: number\n /** How fast the ring climbs towards `height` (the Lab's `rk`, default .55). */\n readonly riseRate?: number\n readonly roll?: number\n readonly color?: number\n}\n\n/** The Lab's explosion profile with English names; every number is the Lab's (`sources` lists the lines they come from). */\nexport interface ExplosionDef {\n readonly id: string\n /** Radius (m) inside which a body is torn apart, and the one inside which it is hurt. */\n readonly lethal: number\n readonly injury: number\n /** Radius of the fireball (`vis`). */\n readonly visualRadius: number\n readonly fireballs: number\n readonly fuel?: boolean\n readonly heat?: number\n readonly heatLifeScale?: number\n readonly smoke: { readonly color: number; readonly count: number; readonly life: number; readonly kind?: 0 | 3 }\n readonly dust: { readonly color: number; readonly count: number }\n readonly shrapnel: { readonly count: number; readonly kind: 0 | 1 | 2 | 3 | 4; readonly speed: number; readonly width?: number; readonly embers?: number }\n readonly chunks: { readonly count: number; readonly colors: readonly number[]; readonly plate?: boolean; readonly hot: number; readonly speed?: number; readonly size?: number }\n readonly mark: { readonly scorch?: number; readonly crater?: number; readonly dent?: number }\n readonly flash: { readonly light: number; readonly life?: number; readonly color?: number; readonly intensity?: number; readonly blinding?: boolean }\n readonly shock?: false | { readonly duration: number }\n readonly shake: number\n readonly push: number\n readonly mushroom?: MushroomSpec\n readonly residualFires?: readonly FireSpec[]\n /** Radius (m) in which characters catch fire. */\n readonly ignite?: number\n /** The pieces that fly are hot (the characters' parts burn). */\n readonly hotParts?: boolean\n readonly cone?: { readonly angle: number; readonly half: number; readonly length: number }\n readonly directionBias?: number\n readonly sources: readonly string[]\n}\n\n/** What a host does with what an effect cannot do by itself (break the windows, hurt the characters, dig the ground). */\nexport type VfxSideRequest =\n | { readonly type: 'crater'; readonly x: number; readonly z: number; readonly radius: number; readonly depth: number }\n | { readonly type: 'ignite'; readonly point: Vec3Like; readonly radius: number }\n | {\n readonly type: 'blast_front'\n readonly explosion: string\n readonly point: Vec3Like\n /** Where the glass breaks, damage and push reach: the Lab's `min(injury, 20) * .85`. */\n readonly glassRadius: number\n /** Metres per second the front travels (the speed of sound): the host delays what it breaks by `distance / speed`. */\n readonly speed: number\n readonly lethal: number\n readonly injury: number\n readonly push: number\n readonly hotParts: boolean\n readonly blinding: boolean\n readonly cone?: { readonly angle: number; readonly half: number; readonly length: number }\n }\n\nexport interface VfxProfile {\n readonly puffs: number\n readonly sparks: number\n readonly glows: number\n readonly chunks: number\n readonly sprites: { readonly add: number; readonly normal: number }\n readonly streaks: number\n readonly debris: number\n}\n\nexport type VfxProfileName = 'low' | 'medium' | 'high'\n\nexport interface LodBands {\n /** Distances (m) at which the scale drops. */\n readonly edges: readonly [number, number, number]\n /** The scale in each of the four bands. */\n readonly scales: readonly [number, number, number, number]\n /** How many visual radii are taken off the distance: a big blast stays detailed farther away. */\n readonly radiusFactor: number\n}\n\nexport interface AreaCap {\n readonly radius: number\n readonly window: number\n readonly count: number\n readonly excessScale: number\n}\n\nexport interface ScreenState {\n /** Camera shake amplitude (the Lab's `shakeAmp`). */\n shake: number\n /** White flash of the Lab, 0..1, and how fast it fades. */\n flash: number\n flashDecay: number\n white: number\n red: number\n directionLeft: number\n directionRight: number\n chroma: number\n desat: number\n gas: number\n beat: number\n}\n\nexport interface VfxStats {\n readonly puffs: number\n readonly sparks: number\n readonly glows: number\n readonly chunks: number\n readonly fires: number\n readonly shocks: number\n readonly rings: number\n /** Blast lights and fire lights that are lit. */\n readonly blastLights: number\n readonly fireLights: number\n /** What the last explosion made: puffs, sparks, chunks, lights. */\n readonly lastBlast: { readonly puffs: number; readonly sparks: number; readonly chunks: number; readonly lights: number }\n /** The scale the last explosion was made at (LOD times the area cap). */\n readonly lod: number\n /** Seconds since the last explosion, or `-1` before the first. */\n readonly blastTime: number\n}\n","import { AREA_CAP_GAME, createAreaCap } from './area-cap'\nimport { PROFILES } from './budget'\nimport { EXPLOSIONS, EXPLOSION_ALIASES } from './catalog'\nimport { hexToLinearRgb } from './color'\nimport { DEFAULT_GRAVITY, DEFAULT_WIND } from './constants'\nimport type { RandomMode, VfxCore } from './core'\nimport { addFire, createFirePool, updateFires } from './emitters/fire'\nimport type { Fire } from './emitters/fire'\nimport { GAME_EFFECTS } from './effects/catalog'\nimport { createEffectPlayer } from './effects/player'\nimport type { EffectPlayer, PlayOptions } from './effects/player'\nimport type { EffectDef } from './effects/types'\nimport { composeExplosion } from './explosion'\nimport type { GroundField } from './ground'\nimport { createLightPool, resetLightPool, updateLights } from './lights'\nimport type { LightPool } from './lights'\nimport { LOD_GAME } from './lod'\nimport { createMarkStore } from './marks'\nimport type { MarkEvent, MarkStore } from './marks'\nimport type { OrderedPool } from './pool'\nimport { updateChunks } from './pools/chunk-motion'\nimport { createChunkPool } from './pools/chunks'\nimport type { Chunk } from './pools/chunks'\nimport { createGlowPool, updateGlows } from './pools/glows'\nimport type { Glow } from './pools/glows'\nimport { createPuffPool, updatePuffs } from './pools/puffs'\nimport type { Puff, Ring } from './pools/puffs'\nimport { createRingPool, updateRings } from './pools/rings'\nimport { createShockPool, updateShocks } from './pools/shocks'\nimport type { Shock } from './pools/shocks'\nimport { createSparkPool, updateSparks } from './pools/sparks'\nimport type { Spark } from './pools/sparks'\nimport { lehmer, streamFor } from './random'\nimport type { MutableVec3, RandomStream } from './random'\nimport { createSchedule } from './schedule'\nimport { createScreenState, resetScreenState, updateScreen } from './screen'\nimport { SpriteKindSystem } from './sprites/system'\nimport type { LandListener } from './sprites/system'\nimport type { SpriteSpawnOptions } from './sprites/kind-pool'\nimport type { SpriteKind, SpriteKindDef } from './sprites/kinds'\nimport type { VfxTrace } from './trace'\nimport { WorkshopEngine } from './workshop/engine'\nimport type { Vec3Tuple } from './workshop/types'\nimport type { AreaCap, ExplosionDef, Follow, LodBands, ScreenState, VfxLayer, VfxProfile, VfxProfileName, VfxSideRequest, VfxStats, Vec3Like } from './types'\nimport { VFX_LAYERS } from './types'\n\nexport interface VfxOptions {\n readonly seed?: number\n /** `per_event` (default): every explosion draws from a stream of its own seed, so a LAN client repeats it from a few bytes. `shared`: one stream for everything, like the Lab page (the goldens). */\n readonly randomMode?: RandomMode\n readonly profile?: VfxProfileName | VfxProfile\n readonly lod?: LodBands\n /** The cap on explosions in one area; `false` turns it off. Default the game's: 6 in 4 s within 40 m, the extra ones at 0.3. */\n readonly areaCap?: AreaCap | false\n /** Height of the ground for what bounces. Default: the `ground` field, else flat at 0. */\n readonly groundAt?: (x: number, z: number) => number\n /** A deformable ground the craters dig into (see `createGroundField`). */\n readonly ground?: GroundField\n readonly wind?: Vec3Like\n readonly gravity?: number\n /** The catalogue by id (default the Lab's 16 and `generic`); build another with `patchExplosions(EXPLOSIONS, ...)`. */\n readonly explosions?: Readonly<Record<string, ExplosionDef>>\n readonly aliases?: Readonly<Record<string, string>>\n /** The Workshop's recipes by id for `play` (default `GAME_EFFECTS`; build another with `patchEffects`). */\n readonly effects?: Readonly<Record<string, EffectDef>>\n /** The sprites of fixed kinds (`burst`, `spawn`): pool size and count multiplier (default 2500 and 1; a `low` profile halves the pool and uses 0.6) and the kinds by id. */\n readonly sprites?: { readonly capacity?: number; readonly density?: number; readonly kinds?: Readonly<Record<string, SpriteKindDef>> }\n readonly reducedMotion?: boolean\n /** Hears everything that spawns, in order (the goldens use it). */\n readonly trace?: VfxTrace\n readonly onRequest?: (request: VfxSideRequest) => void\n}\n\nexport interface ExplosionOptions {\n readonly direction?: Vec3Like\n /** Repeats the same visual: used in `per_event` mode. */\n readonly seed?: number\n /** Multiplies the size of the effect (radius, marks, mushroom, fires). */\n readonly scale?: number\n /** Seconds from now until it goes off (a chain). */\n readonly delay?: number\n}\n\nexport interface FireOptions {\n readonly point: Vec3Like\n readonly radius: number\n readonly life: number\n /** Flames per second per square-ish metre (the Lab's `rate`, default 22) and smoke puffs (default .6). */\n readonly rate?: number\n readonly smoke?: number\n /** Grows from `startRadius` to `radius` over `grow` seconds. */\n readonly startRadius?: number\n readonly grow?: number\n /** `0xRRGGBB` of the smoke (default soot black). */\n readonly smokeColor?: number\n /** Moves with a thing (a burning part): the fire sits at its position plus `offset`. */\n readonly follow?: Follow\n readonly offset?: Vec3Like\n readonly seed?: number\n}\n\n/** The pools as the renderer reads them: live records, oldest first. */\nexport interface VfxPoolsView {\n readonly puffs: OrderedPool<Puff>\n readonly sparks: OrderedPool<Spark>\n readonly glows: OrderedPool<Glow>\n readonly chunks: OrderedPool<Chunk>\n readonly shocks: OrderedPool<Shock>\n readonly rings: OrderedPool<Ring>\n readonly lights: LightPool\n readonly marks: MarkStore\n readonly ground: GroundField | undefined\n}\n\nexport interface VfxWorld {\n /** Sets off an explosion of the catalogue by id (an explosive id or an alias); `null` when the id is unknown. */\n explosion(id: string, point: Vec3Like, options?: ExplosionOptions): number | null\n /** A lingering area fire (a molotov's pool, a burning part). Returns a handle for `stop`. */\n fire(options: FireOptions): number\n /**\n * Plays a recipe of the Workshop (a blood hit, a muzzle flash, a collapse) at `point`, facing `options.direction`, into the world's Workshop pools\n * (`workshop()`); `null` when the id is unknown. Returns a handle for `stop`.\n */\n play(id: string, point: Vec3Like, options?: PlayOptions): number | null\n /** The Workshop's pools (sprites, streaks, debris, decals, rings, lights, screen): what the renderer draws for `play`. Made on first use. */\n workshop(): WorkshopEngine\n /** The Workshop engine if something already made it (`play`, `workshop()`); never creates it. */\n peekWorkshop(): WorkshopEngine | undefined\n /** The ids `play` accepts. */\n effectIds(): readonly string[]\n /** `count` sprites of a kind leaving `point` around `normal` at about `speed` m/s (debris, sparks, smoke, steam, drips...). */\n burst(kind: SpriteKind | string, point: Vec3Like, normal: Vec3Like, count: number, speed?: number, options?: SpriteSpawnOptions): void\n /** One sprite of a kind with an explicit velocity. */\n spawn(kind: SpriteKind | string, point: Vec3Like, velocity: Vec3Like, options?: SpriteSpawnOptions): void\n /** Hears where the sprites of `owner` that land (drips spawned with `{ owner, tag }`) touch the ground. Returns the function that stops listening. */\n onLand(owner: string, listener: LandListener): () => void\n /** The sprite pools as the renderer reads them. */\n sprites(): SpriteKindSystem\n /** Pushes the smoke and dust within `radius` of `point` outwards, as fast as `speed` at the centre. */\n push(point: Vec3Like, radius: number, speed: number): void\n stop(handle: number): void\n setLayer(layer: VfxLayer, on: boolean): void\n layer(layer: VfxLayer): boolean\n setCamera(position: Vec3Like): void\n /** The wind that carries the smoke and dust, in m/s (`VfxOptions.wind` is where it starts). The weather moves it. */\n setWind(wind: Vec3Like): void\n wind(): Vec3Tuple\n /** Forces the scale of every explosion from now on (`null`: back to the distance bands and the cap). */\n forceLod(scale: number | null): void\n update(dt: number): void\n screen(): Readonly<ScreenState>\n stats(): VfxStats\n pools(): VfxPoolsView\n /** The live lingering fires, oldest first. */\n fires(): readonly Fire[]\n /** Hears every mark the moment it is made, and when they are cleared. Returns the function that stops listening. */\n onMarks(listener: (event: MarkEvent) => void): () => void\n /** Seconds the world has run since it was created or cleared. */\n time(): number\n /** The state of the shared stream (`randomMode: 'shared'`): what a golden compares after a run. */\n randomState(): number\n /** Takes everything away (pools, marks, ground, timers, lights, screen) and restarts the clock. */\n clear(): void\n /** The catalogue ids `explosion` accepts, aliases included. */\n catalog(): readonly string[]\n}\n\nconst FIRE_CAPACITY = 64\nconst RING_CAPACITY = 16\nconst SHOCK_CAPACITY = 16\nconst AUTO_SEED = 0x2545f491\n\nfunction resolveProfile(profile: VfxOptions['profile']): VfxProfile {\n if (profile === undefined) return PROFILES.medium\n return typeof profile === 'string' ? PROFILES[profile] : profile\n}\n\nfunction scaleDefinition(def: ExplosionDef, factor: number): ExplosionDef {\n if (factor === 1) return def\n const mushroom = def.mushroom && { ...def.mushroom, height: def.mushroom.height * factor, radius: def.mushroom.radius * factor }\n const residual = def.residualFires?.map((fire) => ({ ...fire, radius: fire.radius * factor, dx: (fire.dx ?? 0) * factor, dz: (fire.dz ?? 0) * factor }))\n return {\n ...def,\n lethal: def.lethal * factor,\n injury: def.injury * factor,\n visualRadius: def.visualRadius * factor,\n mark: { scorch: def.mark.scorch && def.mark.scorch * factor, crater: def.mark.crater && def.mark.crater * factor, dent: def.mark.dent && def.mark.dent * factor },\n ...(mushroom ? { mushroom } : {}),\n ...(residual ? { residualFires: residual } : {}),\n }\n}\n\nexport function createVfx(options: VfxOptions = {}): VfxWorld {\n const profile = resolveProfile(options.profile)\n const sharedRandom = lehmer(options.seed ?? 1)\n const explosions = options.explosions ?? EXPLOSIONS\n const aliases = options.aliases ?? EXPLOSION_ALIASES\n const randomMode = options.randomMode ?? 'per_event'\n const wind: MutableVec3 = { x: DEFAULT_WIND.x, y: DEFAULT_WIND.y, z: DEFAULT_WIND.z }\n if (options.wind) [wind.x, wind.y, wind.z] = options.wind\n const camera: MutableVec3 = { x: 0, y: 1.6, z: 0 }\n const layers = Object.fromEntries(VFX_LAYERS.map((layer) => [layer, true])) as Record<VfxLayer, boolean>\n const ground = options.ground\n const groundAt = options.groundAt ?? (ground ? (x: number, z: number) => ground.heightAt(x, z) : () => 0)\n const handles = { next: 0, events: 0 }\n const low = options.profile === 'low'\n const spriteSystem = new SpriteKindSystem({\n capacity: options.sprites?.capacity ?? (low ? 1250 : 2500),\n density: options.sprites?.density ?? (low ? 0.6 : 1),\n seed: 0x9e3779b1 + (options.seed ?? 0),\n groundAt,\n gravity: options.gravity ?? DEFAULT_GRAVITY,\n ...(options.sprites?.kinds ? { kinds: options.sprites.kinds } : {}),\n })\n const core: VfxCore = {\n profile,\n puffs: createPuffPool(profile.puffs),\n sparks: createSparkPool(profile.sparks),\n glows: createGlowPool(profile.glows),\n chunks: createChunkPool(profile.chunks, sharedRandom),\n fires: createFirePool(FIRE_CAPACITY, sharedRandom),\n shocks: createShockPool(SHOCK_CAPACITY),\n rings: createRingPool(RING_CAPACITY),\n lights: createLightPool(),\n marks: createMarkStore(),\n screen: createScreenState(),\n layers,\n schedule: createSchedule(),\n wind,\n camera,\n ground,\n bands: options.lod ?? LOD_GAME,\n areaCap: options.areaCap === false ? undefined : createAreaCap(options.areaCap ?? AREA_CAP_GAME),\n randomMode,\n sharedRandom,\n reducedMotion: options.reducedMotion ?? false,\n gravity: options.gravity ?? DEFAULT_GRAVITY,\n trace: options.trace,\n spawned: { puffs: 0, sparks: 0, chunks: 0, lights: 0 },\n clock: { time: 0, lastBlast: -1, lod: 1, forcedLod: null },\n groundAt,\n emit: (request) => options.onRequest?.(request),\n }\n\n function definitionOf(id: string): ExplosionDef | undefined {\n return explosions[id] ?? explosions[aliases[id] ?? '']\n }\n\n function streamOf(seed: number | undefined): RandomStream {\n if (randomMode === 'shared') return sharedRandom\n handles.events++\n return streamFor(seed ?? Math.imul(handles.events, AUTO_SEED) + (options.seed ?? 0))\n }\n\n function detonate(def: ExplosionDef, point: Vec3Like, call: { direction: MutableVec3 | undefined; random: RandomStream; handle: number }): void {\n composeExplosion(core, def, { x: point[0], y: point[1], z: point[2], direction: call.direction, random: call.random, handle: call.handle })\n }\n\n let engine: WorkshopEngine | undefined\n let player: EffectPlayer | undefined\n const effects = options.effects ?? GAME_EFFECTS\n const view = { get position(): Vec3Tuple { return [camera.x, camera.y, camera.z] }, forward: (local: readonly number[]): Vec3Tuple => [local[0] ?? 0, local[1] ?? 0, local[2] ?? 0] }\n\n function workshop(): WorkshopEngine {\n return (engine ??= new WorkshopEngine({ random: sharedRandom, profile, groundAt }))\n }\n\n function effectPlayer(): EffectPlayer {\n return (player ??= createEffectPlayer({ engine: workshop(), effects, view }))\n }\n\n return {\n play(id, point, playOptions = {}) {\n if (!(id in effects)) return null\n const seed = playOptions.seed ?? (randomMode === 'shared' ? undefined : Math.imul(++handles.events, AUTO_SEED) + (options.seed ?? 0))\n return effectPlayer().play(id, [point[0], point[1], point[2]], { ...playOptions, ...(seed !== undefined ? { seed } : {}) })\n },\n burst: (kind, point, normal, count, speed, spriteOptions) => spriteSystem.burst(kind, point, normal, count, speed, spriteOptions),\n spawn: (kind, point, velocity, spriteOptions) => spriteSystem.spawn(kind, point, velocity, spriteOptions),\n onLand: (owner, listener) => spriteSystem.onLand(owner, listener),\n sprites: () => spriteSystem,\n workshop,\n peekWorkshop: () => engine,\n effectIds: () => Object.keys(effects),\n explosion(id, point, explosionOptions = {}) {\n const base = definitionOf(id)\n if (!base) return null\n const def = scaleDefinition(base, explosionOptions.scale ?? 1)\n const handle = ++handles.next\n const direction = explosionOptions.direction && { x: explosionOptions.direction[0], y: explosionOptions.direction[1], z: explosionOptions.direction[2] }\n const random = streamOf(explosionOptions.seed)\n if (explosionOptions.delay) core.schedule.at(core.clock.time + explosionOptions.delay, () => detonate(def, point, { direction, random, handle }))\n else detonate(def, point, { direction, random, handle })\n return handle\n },\n fire(fireOptions) {\n const handle = ++handles.next\n const { point, offset, seed, radius, smokeColor, ...rest } = fireOptions\n addFire(core, streamOf(seed), {\n ...rest, r: radius, x: point[0], y: point[1], z: point[2], fx: offset?.[0] ?? 0, fy: offset?.[1] ?? 0, fz: offset?.[2] ?? 0,\n ...(smokeColor !== undefined ? { color: hexToLinearRgb(smokeColor) } : {}),\n }, handle)\n return handle\n },\n push(point, radius, speed) {\n spriteSystem.push(point, radius, speed)\n for (const puff of core.puffs.items) {\n const dx = puff.x - point[0]\n const dy = puff.y - point[1]\n const dz = puff.z - point[2]\n const distance = Math.sqrt(dx * dx + dy * dy + dz * dz)\n if (distance >= radius) continue\n const strength = (speed * (1 - distance / radius)) / (distance + 0.001)\n puff.vx += dx * strength\n puff.vy += dy * strength\n puff.vz += dz * strength\n }\n },\n stop(handle) {\n player?.stop(handle)\n const fires = core.fires\n for (let index = fires.items.length - 1; index >= 0; index--) if (fires.items[index]!.handle === handle) fires.removeAt(index)\n },\n setLayer(layer, on) {\n layers[layer] = on\n },\n layer: (layer) => layers[layer],\n setCamera(position) {\n camera.x = position[0]\n camera.y = position[1]\n camera.z = position[2]\n },\n setWind(next) {\n wind.x = next[0]\n wind.y = next[1]\n wind.z = next[2]\n },\n wind: () => [wind.x, wind.y, wind.z],\n forceLod(scale) {\n core.clock.forcedLod = scale\n },\n update(dt) {\n core.clock.time += dt\n core.schedule.update(core.clock.time)\n updateRings(core.rings, dt)\n updatePuffs(core.puffs, dt, wind)\n updateSparks(core.sparks, { dt, groundAt })\n updateGlows(core.glows, dt)\n updateShocks(core.shocks, core.puffs.items, dt)\n updateChunks(core, dt)\n updateFires(core, dt)\n updateLights(core.lights, { dt, flashLayer: layers.flash ? 1 : 0, clock: core.clock.time, fires: core.fires.items })\n updateScreen(core.screen, dt)\n spriteSystem.update(dt)\n if (engine) {\n engine.post = {}\n player?.update(dt)\n engine.update(dt)\n }\n },\n screen: () => core.screen,\n stats() {\n const lights = core.lights\n return {\n puffs: core.puffs.count,\n sparks: core.sparks.count,\n glows: core.glows.count,\n chunks: core.chunks.count,\n fires: core.fires.count,\n shocks: core.shocks.count,\n rings: core.rings.count,\n blastLights: lights.blast.filter((light) => light.intensity > 0.05).length,\n fireLights: lights.fire.filter((light) => light.intensity > 0.05).length,\n lastBlast: { ...core.spawned },\n lod: core.clock.lod,\n blastTime: core.clock.lastBlast < 0 ? -1 : core.clock.time - core.clock.lastBlast,\n }\n },\n pools: () => ({\n puffs: core.puffs, sparks: core.sparks, glows: core.glows, chunks: core.chunks, shocks: core.shocks, rings: core.rings, lights: core.lights, marks: core.marks, ground,\n }),\n fires: () => core.fires.items,\n onMarks: (listener) => core.marks.subscribe(listener),\n time: () => core.clock.time,\n randomState: () => sharedRandom.state,\n clear() {\n for (const pool of [core.puffs, core.sparks, core.glows, core.chunks, core.fires, core.shocks, core.rings] as OrderedPool<object>[]) pool.clear()\n core.marks.clear()\n core.schedule.clear()\n core.areaCap?.clear()\n ground?.reset()\n resetLightPool(core.lights)\n resetScreenState(core.screen)\n spriteSystem.clear()\n player?.clear()\n engine?.clear()\n core.clock.time = 0\n core.clock.lastBlast = -1\n core.clock.lod = 1\n core.spawned.puffs = core.spawned.sparks = core.spawned.chunks = core.spawned.lights = 0\n },\n catalog: () => [...new Set([...Object.keys(explosions), ...Object.keys(aliases)])],\n }\n}\n","{\n \"spall\": \"impact_{material}\",\n \"stage\": { \"1\": \"stage_crack\", \"2\": \"breach_dust\", \"3\": \"collapse_dust\" },\n \"expose\": \"rebar_expose\",\n \"collapse\": \"collapse_dust\",\n \"explode\": \"explosion\",\n \"fracture\": { \"glass\": \"glass_burst\" }\n}\n","import eventData from '../data/event-effects.json'\n\n/** What a game event looks like to the map: the kind of event and the little that picks its effect. */\nexport interface GameEvent {\n /** `spall`, `stage`, `expose`, `collapse`, `explode`, `fracture`... (strings, so there is no compile dependency on the destruction package). */\n readonly kind: string\n /** The stage of a `stage` event (1, 2 or 3). */\n readonly stage?: number\n /** The material of a `spall` or `fracture` event. */\n readonly material?: string\n}\n\ntype EventMap = Readonly<Record<string, string | Readonly<Record<string, string>>>>\n\nexport const EVENT_EFFECTS_MAP: EventMap = eventData\n\n/**\n * The effect id that plays for an event (`undefined` when the map has none). A string entry may use `{material}`; an object entry is chosen by the event's\n * stage or material. A game passes its own map to add or change entries.\n */\nexport function effectForEvent(event: GameEvent, map: EventMap = EVENT_EFFECTS_MAP): string | undefined {\n const entry = map[event.kind]\n if (entry === undefined) return undefined\n if (typeof entry === 'string') return event.material || !entry.includes('{material}') ? entry.replace('{material}', event.material ?? '') : undefined\n const key = event.stage !== undefined ? String(event.stage) : event.material\n return key === undefined ? undefined : entry[key]\n}\n","import type { EffectTexts } from '../locale'\n\n/** The Workshop's sheet for each effect, in English (the pt-BR texts are the page's own). */\nexport const en: Readonly<Record<string, EffectTexts>> = {\n blood_hit: {\n name: 'Blood by infection age',\n short: 'A mist on the way in, a jet on the way out, a pool that grows. Age changes colour and amount.',\n cost: '≤ 70 particles',\n costNote: '1 pool · 6 splashes',\n paramLabel: 'Age',\n when: 'Every bullet or blow that hits a zombie. The blood value comes from the bestiary age: 1.0 · 0.7 · 0.35 · 0.1.',\n look: 'Fresh sprays bright red and drips. Recent is burgundy. Old is brown and sparse. Decrepit does not bleed: it gives off black dust and dry flakes. The pool grows for 2 to 4 s and darkens in 60 s.',\n budget: 'Up to 70 particles per hit (Fresh), 1 pool and up to 6 splashes on the ground. Drops that land become decals.',\n network: 'Cosmetic. Goes in the impact event with 2 bits of age and the seed.',\n },\n headshot: {\n name: 'Headshot',\n short: 'The head bursts, the eyes go out on the same frame and the body drops.',\n cost: '≤ 70 particles',\n costNote: '8 debris · 1 pool',\n when: 'A head hit that kills (bestiary rule: a head kills at once).',\n look: 'A red mist around the head, a jet of drops backwards, 6 to 8 pieces of bone and skin, a swap to the burst-neck mesh. The eye glow goes out on the same frame, which is the most readable signal from afar.',\n budget: '20 mist + 35 drops + 8 debris + 1 pool. In the horde only the 12 closest get debris.',\n network: 'Cosmetic. The server already sends \"death by head\".',\n },\n dismember: {\n name: 'Dismemberment',\n short: 'The arm comes off whole with physics and the shoulder gushes for half a second.',\n cost: '≤ 60 particles',\n costNote: '1 limb · 4 splashes',\n when: 'Damage to a limb above the threshold (multipliers by region from the bestiary) or an axe blow.',\n look: 'The limb becomes a loose piece with physics and bounces. A short pulsing jet from the shoulder, drops that form a trail to the pool. The zombie staggers but keeps walking.',\n budget: '1 limb with physics (counts toward the cap of 300), a jet of up to 60 drops in 0.7 s.',\n network: 'The server sends which limb came off. The piece is local; each client simulates its own.',\n },\n bloater_cloud: {\n name: 'Bloater cloud',\n short: 'The belly pulses for 0.5 s, bursts and leaves 4 m of gas for 8 s.',\n cost: '≤ 180 particles',\n costNote: 'server entity',\n when: 'Death of the Bloater, by any cause.',\n look: 'A 0.5 s warning with the belly swelling, a burst with pieces and a greenish jet, and a low moss-green cloud that undulates on the ground for 8 s. Loose green glows inside the cloud mark the area.',\n budget: '40 large sprites at the burst and up to 60 alive in the cloud, at half resolution. Fade near the camera so it does not paint the whole screen.',\n network: 'Server entity (radius 4 m, 8 s): blinds for 5 s, doubles the scent trail and makes people cough. The client only draws.',\n },\n screamer_shriek: {\n name: 'Screamer shriek',\n short: 'A 1.5 s wind-up with steam, a wave on the ground and the horde\\'s eyes light up.',\n cost: '≤ 150 particles',\n costNote: '3 rings · aberration',\n when: 'The Screamer spots the player and starts the shriek (1.5 s wind-up, calls everyone within 150 m).',\n look: 'Head back and steam from the mouth during the wind-up. On the shriek, three pressure rings on the ground, dust raised in the radius, a short chromatic aberration on the screen of whoever is near, and the eyes of nearby zombies flare when they hear.',\n budget: '3 rings, up to 150 dust particles. The screen aberration is a 0.4 s post effect.',\n network: 'The server sends \"shriek at X\". The screen effect depends on local distance.',\n },\n eyes_in_fog: {\n name: 'Eyes in the fog',\n short: 'Eye glow that cuts through the fog; runners leave a red trail.',\n cost: '2 sprites/zombie',\n costNote: '0 lights',\n when: 'Always, on every zombie. It is the bestiary reading system: eye colour tells the category.',\n look: 'Two emissive points with bloom that ignore the fog. The body disappears in the mist at 25 m, the eyes stay readable at 60 m. Runners leave a short trail in the eye colour when they run.',\n budget: '2 sprites per zombie in the same instanced batch as the body. Trail only for runners closer than 30 m. No real light.',\n network: 'Nothing goes over the network: it comes from the zombie category.',\n },\n corpse_decay: {\n name: 'Corpse: darken and dissolve',\n short: '15 minutes in 8 seconds: fall, darken, flies and turn to dust.',\n cost: '≤ 40 particles',\n costNote: 'pool of 400 bodies',\n when: 'Zombie death. Bestiary timeline: ragdoll 3 s, frozen mesh, darken, dissolve between 14 and 15 min.',\n look: 'The skin loses colour until almost black, the pool dries to brown, flies appear from minute 5 and in the last minute the body sinks and gives off dark dust.',\n budget: 'Flies: 6 particles per body, only closer than 10 m. Dust: 40 particles once. The body uses the dissolve shader, no extra particle.',\n network: 'The server sends the time of death; each client works out the state.',\n },\n reanimation: {\n name: 'Reanimation',\n short: 'Veins light up, spasms, the eyes switch on and the body gets up.',\n cost: '≤ 30 particles',\n costNote: 'emissive in the shader',\n when: 'End of infection of a player or NPC: reanimates in 2 to 6 min as Fresh (70% Slow, 30% Runner).',\n look: 'Green pulses through the skin (emissive in the shader, not particles), spasms in the arms and head, the eyes light up from the inside out and the body stands. Breath as steam on the first shriek.',\n budget: 'Only animated emissive and 30 particles. It is rare and important, so it may be expensive.',\n network: 'The server sends \"reanimated\". For the friend who sees a teammate get up, it is the strongest moment of co-op.',\n },\n muzzle_flash: {\n name: 'Muzzle flash',\n short: 'Core, petals and smoke scaled by weapon. The Barrett raises dust.',\n cost: '≤ 60 particles',\n costNote: '1 light · 50 ms',\n paramLabel: 'Weapon',\n when: 'Every shot. In first person it uses the weapon model; from afar it is only the core sprite.',\n look: 'Three layers: a white-yellow core of 1 frame, petals along the barrel and grey smoke that stays 1 to 2 s. The 12-gauge has a wide flash and thick smoke. The Barrett vents two side jets through the muzzle brake and raises a ring of dust from the ground. The light illuminates the room for 50 ms (request from the map brief).',\n budget: 'Up to 60 particles and 1 light per shot. An AK burst uses the same light in a loop. A pistol has no light beyond 15 m.',\n network: 'Cosmetic. It comes from the shot event the server already sends (weapon, origin, direction).',\n },\n impact: {\n name: 'Impact by material',\n short: 'Concrete, metal, wood, glass, flesh and water: each with its own response.',\n cost: '≤ 40 particles',\n costNote: '1 hole',\n paramLabel: 'Material',\n when: 'Every bullet that hits something. The material comes from the mesh surface tag.',\n look: 'Concrete: pale dust in a cone and grey chips. Metal: a shower of bouncing sparks, no dust, with a short light. Wood: thin splinters and brown dust. Glass: glint, shards and a crack. Flesh: blood by age. Water: a crown of drops and two rings.',\n budget: '14 dust + 6 chips + up to 18 sparks + 1 hole. At 40 m only the dust remains.',\n network: '12-byte impact event with the material in 4 bits.',\n },\n penetration: {\n name: 'Bullet through the horde',\n short: 'Barrett tracer passing through 5 zombies, in 10x slow motion.',\n cost: '≤ 250 particles',\n costNote: 'aggregate event',\n when: 'Weapon with penetration: the AK goes through 2, the Remington 700 through 3, the Barrett through 6 and cars.',\n look: 'A tracer with a hot-air trail, blood exiting each body in the order the bullet passes (each in the colour of its age), bodies falling one after another and a final impact on the concrete.',\n budget: 'All hits of a shot go in a single aggregate event with the list of points. 5 bodies = 5 exits + 5 pools.',\n network: 'The server sends the shot and the ordered list of hits.',\n },\n casings_and_magazine: {\n name: 'Casings and magazine',\n short: 'Brass that bounces and stays on the ground, an empty magazine falling, smoke from the hot barrel.',\n cost: '200/player',\n costNote: 'physics 1.5 s',\n when: 'Every shot of a semi-automatic weapon, every reload, and the pump of the 12-gauge or the rifle bolt.',\n look: 'The casing leaves the ejection port spinning, bounces on the asphalt two or three times and stays. The empty magazine drops on reload. After a burst, thin smoke rises from the hot barrel for 2 s.',\n budget: 'Physics only for 1.5 s, then it becomes a static instance. Cap of 200 per player, the oldest goes first.',\n network: 'Cosmetic, each client generates its own. Ground position is not synchronised.',\n },\n molotov: {\n name: 'Molotov cocktail',\n short: 'A lit wick in the arc, glass breaking and a pool of fire that spreads.',\n cost: '≤ 300 particles',\n costNote: '2 lights · entity',\n when: 'A molotov hitting the ground or a zombie.',\n look: 'A bottle in an arc with the wick giving off flame and smoke. On impact, green shards and a splash of fire that opens from 0.5 m to 2.2 m in 0.8 s. Low flames, embers, dark smoke and a flickering light for 6 s, then embers and a burn mark.',\n budget: 'Area fire with up to 300 particles and 1 fire light. Burning zombies use a small emitter attached to the body (40 particles) and only the 8 closest.',\n network: 'Fire is a server entity: area, damage per second, time. The client draws from it.',\n },\n grenade: {\n name: 'Fragmentation grenade',\n short: 'A bounce on the asphalt, flash, shock wave, shrapnel and a ring of dust.',\n cost: '≤ 400 particles',\n costNote: 'aggregate event',\n when: 'A grenade exploding (1.8 s fuse after the throw).',\n look: 'The grenade bounces twice. A layered explosion: a white flash that paints the screen, a short fireball, a shock ring on the ground and another facing the camera, bright shrapnel that bounces, a ring of low dust, grey smoke that rises and a burn mark. Zombies in the radius fall outwards.',\n budget: 'Up to 400 particles, 1 strong light of 0.5 s, 16 debris, 1 mark. The 40 zombies hit go in a single aggregate event.',\n network: 'The server sends the explosion and the list of those hit. Visual is local.',\n },\n explosive_barrel: {\n name: 'Explosive barrel',\n short: 'A mushroom of fire, the lid flying, petrol burning on the ground and a column of smoke.',\n cost: '≤ 450 particles',\n costNote: '2 lights · fire entity',\n when: 'A red barrel with damage above the threshold, a shot or fire touching it.',\n look: 'Everything the grenade does, plus: a fireball that rises into a mushroom, the lid and red pieces flying high, three pools of petrol burning for 5 s and a black smoke column that serves as a landmark from afar.',\n budget: 'Peak of 450 particles. The smoke column stays visible beyond 80 m with 6 large sprites.',\n network: 'Explosion + a fire entity in the pools.',\n },\n shopfront_glass: {\n name: 'Shop window bursting',\n short: 'Two cracks, then the whole pane comes down in shards that stay on the pavement.',\n cost: '≤ 80 debris',\n costNote: '1 crack',\n when: 'Glass with accumulated damage (shot, blow, horde pushing). A 20 m noise that draws the horde.',\n look: 'The first blows open star cracks in the glass texture. At the burst the pane vanishes and becomes 60 to 80 shards that fall spinning, glinting in the light, and stay on the pavement. A veil of fine glass dust in front.',\n budget: 'Shards with physics count toward the cap of 300 and become static in 2 s. 3 crack stages in a single mask.',\n network: 'The server sends the glass state (0 to 3). Shards are local.',\n },\n door_stages: {\n name: 'Door giving way in 3 stages',\n short: 'Blows shake dust from the frame, a plank cracks and the door falls.',\n cost: '≤ 120 particles',\n costNote: '3 stages',\n when: 'The horde hitting a door or barricade (the Brute does 3x damage). Stages: 66%, 33%, 0% health.',\n look: 'Each blow shakes the door and loosens dust from the frame and splinters. At stage 2 the middle plank cracks and falls. At 3 the door topples and raises a cloud of low dust, and the zombie comes out of the dark.',\n budget: 'Dust: 12 particles per blow. Fall: 40 of dust and 6 debris. Blows beyond 15 m have no particles, only sound.',\n network: 'The server sends the door health. The visual stage comes from the value.',\n },\n building_collapse: {\n name: 'Collapse',\n short: 'Dust trickling from the awning, blocks falling and a cloud of dust rolling down the street.',\n cost: '≤ 350 particles',\n costNote: '26 debris',\n when: 'A structure that loses its health: explosion, vehicle impact, a big horde pushing (destruction of the map buildings).',\n look: 'Before: fine dust trickling and pebbles falling for 1 s, as a warning. Then: large and small blocks fall, the camera shakes and a cloud of grey-beige dust rolls along the ground and hangs in the air for 5 s, hiding zombies.',\n budget: '26 debris (6 large), 70 dust sprites at half resolution. A rare event, it fits the budget.',\n network: 'The server sends the collapse and the dust area, which blocks zombie sight for 4 s.',\n },\n block_wall: {\n name: 'Wall brought down with holes',\n short: 'A burst leaves holes and chips; the wall falls in blocks and the holes stay on them.',\n cost: '≤ 300 particles',\n costNote: '30 blocks · attached holes',\n when: 'A concrete wall, low wall or barricade that loses health from shots, explosion, the Brute or a car (map and arsenal destruction).',\n look: 'Each bullet opens a dark hole with a chipped rim, giving off dust and pebbles. When the wall gives way it splits into blocks that topple with physics and a cloud of dust rolls along the ground. The bullet holes are attached to the blocks: they stay on the fallen pieces, turned with them, and do not vanish when the wall breaks.',\n budget: 'Holes are decals that are children of the piece (not of the world), up to 12 per block and 128 in total. Blocks count toward the cap of 300 debris and become static at rest. Dust: 60 sprites at half resolution.',\n network: 'The server sends the wall health and the list of holes (2-byte local position per hole). Whoever joins later sees the same holes on the same pieces. The blocks fall using a common seed.',\n },\n metal_door: {\n name: 'Metal door denting',\n short: 'Each blow sinks the plate, flakes the paint and throws sparks; the hinge bursts.',\n cost: '≤ 160 particles',\n costNote: 'deformable mesh',\n when: 'A door, gate or steel plate taking a zombie blow (Brute 3x), a 12-gauge shot or a car hit.',\n look: 'The plate sinks where it is hit (a deformed mesh, not a model swap), with 3 visible levels. Each blow gives off sparks, flakes of green paint and rust dust. When the health runs out the top hinge bursts in sparks, the door topples and scrapes on the ground giving off more sparks.',\n budget: 'Per-vertex deformation only at the moment of the blow (CPU, 400 vertices). Per blow: 8 sparks, 5 flakes, 6 of rust. Fall: 30 sparks and 40 of dust.',\n network: 'The server keeps 3 bits of dent per door zone (9 zones). Whoever joins later sees the same dented door.',\n },\n iron_railing: {\n name: 'Iron railing bending',\n short: 'The horde pushes, the bars bow, the weld pops in a spark and the railing falls.',\n cost: '≤ 200 particles',\n costNote: '3 deformable bars',\n when: 'A railing, iron gate, handrail or chain-link fence with zombies pushing (horde pressure) or a car hitting it.',\n look: 'The bars bow gradually at the point of pressure and the vertical ones tilt with them. Rust falls in a thread during the strain. At two moments the weld pops with a crackle of sparks. At the end the middle post gives and the railing topples, leaving the bent iron on the ground.',\n budget: 'Curvature worked out only when the pressure changes, up to 180 vertices. Rust: 10 per second during the strain. Weld pop: 20 sparks.',\n network: 'The server sends the pressure (0 to 1) and when it gives. The curve comes from the value, the same on all clients.',\n },\n wooden_fence: {\n name: 'Wooden fence breaking',\n short: 'Planks snap one by one, chips and dust, and the section topples as the horde passes.',\n cost: '≤ 150 particles',\n costNote: '12 planks',\n when: 'A fence, yard gate or plank barricade taking a shot, blow or horde pressure.',\n look: 'Each plank snaps at a random point: the stump stays on the rail and the top piece flies off with chips and wood dust. The zombie appears through the gap. At the end the rail cracks and the whole section topples forward.',\n budget: 'Each plank is a piece: breaking swaps it for a stump + a piece with physics. Per break: 6 chips, 8 of dust. Topple: 35 of dust.',\n network: 'The server sends which planks broke (12 bits) and when the section falls.',\n },\n hit_point_breakage: {\n name: 'Breakage by the point hit',\n short: 'A shot to the leg, a machete on the corner, a bat on the top and a 12-gauge up close: each weapon breaks the part it hit.',\n cost: '≤ 120 particles',\n costNote: 'parts with their own health',\n when: 'Any destructible item. The damage goes to the part that was hit, not to the whole item.',\n look: 'A 9 mm shot to the front leg: the lower part of the leg breaks off and the table tips toward that corner. A machete on the corner of the top: the corner comes off whole in a straight cut, with chips in a line. A bat on the other corner: the top cracks and falls apart in pieces with dust. A 12-gauge up close on what is left: 9 grouped holes that stay in the wood.',\n budget: 'Each part has health and material. A common item: 4 to 12 parts. Particles only from the part hit, by its material.',\n network: 'The server sends which part lost health and how much. A loose part simulates on the client; the final position is sent once.',\n },\n pressure_cylinders: {\n name: 'Extinguisher and oxygen cylinder',\n short: 'A shot to the extinguisher makes a white cloud and a spin; on oxygen the valve breaks and the cylinder becomes a rocket.',\n cost: '≤ 250 particles',\n costNote: 'cloud entity',\n paramLabel: 'Cylinder',\n when: 'A shot, a strong blow or fire on an extinguisher, oxygen cylinder or welding cylinder (extinguisher and oxygen_cylinder keys of the map catalogue).',\n look: 'Extinguisher: the hole gives off a jet of white powder, the extinguisher falls, spins on the ground pushed by the jet and fills a 5 m area with a cloud that blocks sight for 6 s. Oxygen: the valve breaks with a crack, the jet becomes propulsion, the cylinder shoots along the ground scraping and throwing sparks, bounces and stops against a wall. With fire nearby, the oxygen blows up like the barrel.',\n budget: 'Jet: 60 particles per second for 3 s. Extinguisher cloud: up to 40 large sprites. Scraping sparks like the car.',\n network: 'The extinguisher cloud is a server entity (blocks zombie and player sight). The cylinder trajectory uses a common seed and the server corrects the final position.',\n },\n building_fire: {\n name: 'Building on fire',\n short: 'Fire coming out of the windows, licking the facade, embers and smoke with the wind.',\n cost: '≤ 600 particles',\n costNote: '2 lights · reference',\n when: 'A building fire (molotov, explosion, vehicle). It lasts minutes and spreads to the neighbour.',\n look: 'Windows lit orange that flicker, tongues of fire coming out of the top of each window and climbing the facade, embers carried by the wind and a thick slanted column of smoke. The light paints the street.',\n budget: 'Up to 600 particles per building and 2 fire lights. Beyond 80 m only the smoke column and the glow of the windows remain, which become a landmark.',\n network: 'A fire entity on the server per building, with a spread state.',\n },\n rain_and_puddles: {\n name: 'Rain, splashes and puddles',\n short: 'Drops with wind, splashes and rings only near the camera, lightning in the storm.',\n cost: '≤ 4,000 drops',\n costNote: 'splash up to 12 m',\n paramLabel: 'Intensity',\n when: 'The weather of the world. Rain raises the background noise and masks footsteps (it can be used for stealth).',\n look: 'A fine drizzle, almost vertical. Rain with splashes on the asphalt and rings in the puddles, which are dark and mirror-like. A storm with drops slanted by the wind, sheets of water passing and a double flash of lightning that lights the scene for 0.2 s and reveals the horde.',\n budget: 'Up to 4,000 drops tied to the camera in a 30 m box. Splashes and rings only within 12 m and only on what the camera sees.',\n network: 'The server sends the weather and the lightning seed, so everyone sees the same flash.',\n },\n fog_and_ash: {\n name: 'Fog and ash',\n short: 'Low layered mist, ash falling and the lamp post with its cone of light.',\n cost: '≤ 120 sprites',\n costNote: '1 cone of light',\n when: 'Before dawn, near the river and in burnt zones (ash). It cuts the sight of the zombies and of the player.',\n look: 'The engine fog plus a layer of low mist that moves slowly, fine ash falling in a spiral and the beam of the lamp posts in view. Zombie silhouettes show before their details.',\n budget: 'Up to 60 large mist sprites (half resolution) and 60 of ash. The lamp cone is an additive mesh, not real volumetric light.',\n network: 'The weather comes from the server. The density is local, by zone.',\n },\n scraping_sparks: {\n name: 'Scraping sparks',\n short: 'A bumper dragging on the asphalt: a river of bouncing sparks and a mark on the ground.',\n cost: '≤ 200 sparks/s',\n costNote: '1 light',\n when: 'A car part in contact with the ground or a wall (a fallen bumper, a rim without a tyre, a flipped car sliding).',\n look: 'A jet of sparks behind the movement, bouncing on the asphalt and dying from white to orange. A glow and a short light at the contact point. A dark scratched mark on the ground.',\n budget: 'Up to 200 sparks per second, 1 light. Beyond 40 m it is only the glow of the point.',\n network: 'Cosmetic, from the part state and speed that are already replicated.',\n },\n engine_smoke: {\n name: 'Engine smoke',\n short: 'White steam, grey smoke, then black with fire, depending on condition.',\n cost: '≤ 120 particles',\n costNote: 'continuous',\n paramLabel: 'Condition',\n when: 'Whenever the engine is on or hot, from the part condition (0 to 100) in the fleet brief.',\n look: '100: exhaust only. 60: a thread of white steam from the bonnet. 30: steady grey smoke and the odd spark. 10: thick black smoke, flames at the grille and fire light, a warning that it will explode.',\n budget: 'Continuous, so cheap: up to 40 particles per second and 1 light only in the critical state.',\n network: 'Comes from the engine condition, which is already replicated.',\n },\n fuel_leak: {\n name: 'Tank leak and fire',\n short: 'Dripping petrol, a pool, a spark and the fire running to the tank.',\n cost: '≤ 400 particles',\n costNote: 'fire entity',\n when: 'A tank punctured by a shot or a crash. A spark, a shot or nearby fire lights the pool.',\n look: 'Drops of petrol with an oily sheen forming a growing pool. A shot on the metal throws a spark, the fire lights on the pool, runs to the car and, if nobody puts it out, the car explodes.',\n budget: 'Drops: 12 per second. Pool fire like the molotov. The final explosion is the next effect.',\n network: 'The pool and the fire are server entities (they can burn zombies and players).',\n },\n car_explosion: {\n name: 'Car explosion',\n short: 'A fireball, the car jumping, the bonnet and doors flying, the shell burning.',\n cost: '≤ 600 particles',\n costNote: 'rare event',\n when: 'A tank on fire for more than 4 s, or damage above the threshold on a car with fuel.',\n look: 'A large explosion (grenade scale 1.8), the car jumps and lands crooked, the bonnet, doors and a wheel fly off, the glass bursts into shards. The bodywork goes black and the shell burns with black smoke for minutes.',\n budget: 'Peak of 600 particles and 34 debris. It is a rare event and may go over the per-event cap, never the global cap.',\n network: 'Explosion on the server. The car becomes a wreck (another prefab) for everyone.',\n },\n windshield: {\n name: 'Windshield: blood and cracks',\n short: 'View from inside the car: blows, cracks in 4 stages and a bloody hand.',\n cost: '0 particles',\n costNote: '4-stage mask',\n when: 'A zombie hitting the windshield or a side window (fleet brief: the glass takes 8 blows).',\n look: 'Each blow shakes the camera and adds a star crack. Stages 2 and 4 leave a blood stain with a handprint running down. At stage 4 the glass is white with cracks and the next blow breaks it.',\n budget: 'Texture only: a crack mask with 4 stages and a blood decal on the glass. Glass dust: 6 particles per blow.',\n network: 'The server sends the glass health. Blood on the glass is local.',\n },\n headlights_in_rain: {\n name: 'Headlights in the rain',\n short: 'A visible beam, drops lit inside the cone and the horde walking into the light.',\n cost: '≤ 600 lit drops',\n costNote: '2 spots, no shadow',\n when: 'Headlights on at night, with rain or fog.',\n look: 'Two cones of light in the air, rain drops that shine only inside the beam and zombie silhouettes that light up when they enter the light. Outside the cone the rain almost disappears.',\n budget: '2 spotlights without shadow, 2 additive cones and up to 600 extra drops only inside the cone. Headlights of cars beyond 40 m stay as a cone only, with no real light.',\n network: 'The state of the headlights is already replicated with the car.',\n },\n damage_and_bite: {\n name: 'Damage and bite',\n short: 'A red edge on the side of the blow, blood on the lens and the pulse of the bite.',\n cost: 'post-process',\n costNote: 'local only',\n when: 'The player takes a blow (with direction) or a bite (100% infection, characters rule).',\n look: 'Blow: a red edge on the side it came from, a short shake and splashes on the lens. Bite: a red flash over the whole screen, the colour drains for 2 s, a heartbeat pulsing in a dark tunnel and big blood on the lens that runs down.',\n budget: 'A post-process effect and one lens layer. No particles in the world besides the normal blood.',\n network: 'Only on the screen of whoever was hit. The others see the blood in the world.',\n },\n infection_phases: {\n name: 'Infection phases',\n short: 'Malaise, fever with a cold edge, delirium with ghosts and the end.',\n cost: 'post-process',\n costNote: 'local only',\n paramLabel: 'Phase',\n when: 'Phases from the characters brief: 2–12 h malaise, 12–30 h fever, 30–48 h delirium, 48 h death.',\n look: 'Malaise: slightly washed colour and a slowly breathing vignette. Fever: a bluish cold edge only for the infected, waves of blur, a cough that shakes the camera. Delirium: colour aberration, an oscillating hue and fake zombies flickering in the fog. 48 h: almost no colour, a green tone, a dark tunnel and a heartbeat.',\n budget: 'Post-process only. The delirium ghosts are 3 zombie instances without collision, only on the infected client.',\n network: 'Never goes over the network. The other player notices only the cough and the breathing (see \"Cough and breath\").',\n },\n panic: {\n name: 'Panic',\n short: 'The aim shakes, the tunnel closes as the horde arrives and the heart speeds up.',\n cost: 'post-process',\n costNote: 'local only',\n when: 'The characters\\' panic meter (rises with visible zombies, runners and a dead companion). Effects above 60 and above 90.',\n look: 'A fine, constant tremor in the camera (the aim shakes), a dark vignette that closes as the nearest zombie arrives and pulses with the heartbeat, which speeds up from 70 to 150 bpm. Colour loses strength.',\n budget: 'Post-process and camera noise only.',\n network: 'Local. Others\\' panic shows in their aim and in the scream (sound).',\n },\n bloater_blindness: {\n name: 'Bloater blindness',\n short: 'Green gas in the face: a blurred screen for 5 s and mucus on the lens.',\n cost: 'post + 30 particles',\n costNote: 'server entity',\n when: 'A player inside the Bloater cloud (radius 4 m) when it spawns.',\n look: 'A green cloud bursts between the zombie and the camera, the screen goes green and blurred and clears in 5 s, with drops of green mucus on the lens that run down. A cough shakes the camera twice.',\n budget: 'Post-process + 30 gas sprites near the camera, with a fade so particles do not cover the whole screen.',\n network: 'The blindness comes from the server entity; the visual is local.',\n },\n campfire: {\n name: 'Campsite fire',\n short: 'A low flame, embers spiralling up and a flickering warm light.',\n cost: '≤ 120 particles',\n costNote: '1 fire light',\n when: 'A campfire lit by the group or by NPCs. It warms, lights and draws zombies from afar (light).',\n look: 'Small low flames, embers that rise turning and fade, thin smoke and a warm light that flickers and paints the survivor.',\n budget: '120 particles and 1 fire light. Several campfires of the camp share the same light if they are closer than 6 m.',\n network: 'A replicated scene object (lit or out). Visual is local.',\n },\n cough_and_breath: {\n name: 'Cough, breath and flashlight',\n short: 'Breath vapour in the cold, the fever cough visible to others and the flashlight beam.',\n cost: '≤ 40 particles',\n costNote: '1 spot with shadow',\n when: 'A cold night: breath always. Infection fever: a cough every few seconds, audible at 10 m.',\n look: 'A short puff of vapour every 2.4 s. On a cough, three quick bigger puffs and the body jolts. The flashlight shows the beam in the fog and is the only light with a shadow.',\n budget: 'Vapour: 8 particles per puff, only closer than 15 m. Flashlight: 1 spot with a shadow per player, only the local player\\'s casts a shadow.',\n network: 'The cough is the social signal of the infection: the server sends the cough event (without saying it is infection) and the others see the vapour and hear it.',\n },\n}\n","import type { EffectTexts } from '../locale'\n\n/** The Workshop's sheet for each effect (W450-1262), as the page wrote it. */\nexport const ptBR: Readonly<Record<string, EffectTexts>> = {\n blood_hit: {\n name: 'Sangue por idade da infecção',\n short: 'Névoa na entrada, jato na saída, poça que cresce. A idade muda cor e quantidade.',\n cost: '≤ 70 part.',\n costNote: '1 poça · 6 respingos',\n paramLabel: 'Idade',\n when: 'Cada acerto de bala ou golpe em zumbi. O valor de sangue vem da idade do bestiário: 1,0 · 0,7 · 0,35 · 0,1.',\n look: 'Fresco espirra vermelho vivo e pinga. Recente sai bordô. Antigo sai marrom e pouco. Decrépito não sangra: solta pó preto e lascas secas. A poça cresce por 2 a 4 s e escurece em 60 s.',\n budget: 'Até 70 partículas por acerto (Fresco), 1 poça e até 6 respingos no chão. Gotas que caem viram decalque.',\n network: 'Cosmético. Vai no evento de impacto com 2 bits de idade e a semente.',\n },\n headshot: {\n name: 'Headshot',\n short: 'A cabeça estoura, os olhos apagam no mesmo quadro e o corpo cai.',\n cost: '≤ 70 part.',\n costNote: '8 detritos · 1 poça',\n when: 'Acerto na cabeça que mata (regra do bestiário: cabeça mata na hora).',\n look: 'Névoa vermelha em volta da cabeça, jato de gotas para trás, 6 a 8 pedaços de osso e pele, troca pela malha de pescoço estourado. O brilho dos olhos apaga no mesmo quadro, que é o sinal mais legível de longe.',\n budget: '20 de névoa + 35 gotas + 8 detritos + 1 poça. Na horda, só os 12 mais próximos ganham detritos.',\n network: 'Cosmético. O servidor já manda “morte por cabeça”.',\n },\n dismember: {\n name: 'Desmembramento',\n short: 'O braço sai inteiro com física e o ombro jorra por meio segundo.',\n cost: '≤ 60 part.',\n costNote: '1 membro · 4 respingos',\n when: 'Dano no membro acima do limite (multiplicadores por região do bestiário) ou golpe de machado.',\n look: 'O membro vira peça solta com física e quica. Jato curto do ombro em pulsos, gotas que formam um rastro até a poça. O zumbi cambaleia mas continua andando.',\n budget: '1 membro com física (conta no teto de 300), jato de até 60 gotas em 0,7 s.',\n network: 'O servidor manda qual membro saiu. A peça é local; cada cliente simula a sua.',\n },\n bloater_cloud: {\n name: 'Nuvem do Inchado',\n short: 'Barriga pulsa por 0,5 s, estoura e deixa 4 m de gás por 8 s.',\n cost: '≤ 180 part.',\n costNote: 'entidade no servidor',\n when: 'Morte do Inchado, por qualquer causa.',\n look: 'Aviso de 0,5 s com a barriga inchando, estouro com pedaços e jato esverdeado, e uma nuvem baixa verde-musgo que ondula no chão por 8 s. Brilhos verdes soltos dentro da nuvem marcam a área.',\n budget: '40 sprites grandes no estouro e até 60 vivos na nuvem, em meia resolução. Fade perto da câmera para não pintar a tela inteira.',\n network: 'Entidade no servidor (raio 4 m, 8 s): cega por 5 s, dobra o rastro de cheiro e faz tossir. O cliente só desenha.',\n },\n screamer_shriek: {\n name: 'Grito do Gritador',\n short: 'Preparo de 1,5 s com vapor, onda no chão e os olhos da horda acendem.',\n cost: '≤ 150 part.',\n costNote: '3 anéis · aberração',\n when: 'O Gritador avista o jogador e começa o grito (preparo de 1,5 s, chama todos num raio de 150 m).',\n look: 'Cabeça para trás e vapor saindo da boca durante o preparo. No grito, três anéis de pressão no chão, poeira levantada no raio, aberração cromática curta na tela de quem está perto e os olhos dos zumbis próximos piscam forte quando escutam.',\n budget: '3 anéis, até 150 partículas de poeira. A aberração na tela é um efeito de pós de 0,4 s.',\n network: 'O servidor manda “grito em X”. O efeito de tela depende da distância local.',\n },\n eyes_in_fog: {\n name: 'Olhos na neblina',\n short: 'Brilho dos olhos que fura a neblina; os corredores deixam rastro vermelho.',\n cost: '2 sprites/zumbi',\n costNote: '0 luzes',\n when: 'Sempre, em todo zumbi. É o sistema de leitura do bestiário: cor do olho diz a categoria.',\n look: 'Dois pontos emissivos com bloom que ignoram a neblina. O corpo some na névoa a 25 m, os olhos continuam legíveis a 60 m. Corredores deixam um rastro curto da cor do olho quando correm.',\n budget: '2 sprites por zumbi no mesmo lote instanciado do corpo. Rastro só para corredores a menos de 30 m. Nenhuma luz real.',\n network: 'Nada vai pela rede: sai da categoria do zumbi.',\n },\n corpse_decay: {\n name: 'Corpo: escurecer e dissolver',\n short: '15 minutos em 8 segundos: queda, escurece, moscas e vira pó.',\n cost: '≤ 40 part.',\n costNote: 'pool de 400 corpos',\n when: 'Morte do zumbi. Linha do tempo do bestiário: ragdoll 3 s, malha congelada, escurece, dissolve entre 14 e 15 min.',\n look: 'A pele perde a cor até ficar quase preta, a poça seca para marrom, aparecem moscas a partir do minuto 5 e no último minuto o corpo afunda e solta pó escuro.',\n budget: 'Moscas: 6 partículas por corpo, só a menos de 10 m. Pó: 40 partículas uma vez. O corpo usa o shader de dissolve, sem partícula extra.',\n network: 'O servidor manda a hora da morte; cada cliente calcula o estado.',\n },\n reanimation: {\n name: 'Reanimação',\n short: 'Veias acendem, espasmos, os olhos ligam e o corpo levanta.',\n cost: '≤ 30 part.',\n costNote: 'emissivo no shader',\n when: 'Fim da infecção de jogador ou NPC: reanima em 2 a 6 min como Fresco (70% Lento, 30% Corredor).',\n look: 'Pulsos verdes pela pele (emissivo no shader, não partícula), espasmos nos braços e na cabeça, os olhos acendem de dentro para fora e o corpo se levanta. Respiração em vapor no primeiro grito.',\n budget: 'Só emissivo animado e 30 partículas. É raro e importante, então pode ser caro.',\n network: 'O servidor manda “reanimou”. Para o amigo que vê o colega levantar, é o momento mais forte do coop.',\n },\n muzzle_flash: {\n name: 'Clarão de disparo',\n short: 'Núcleo, pétalas e fumaça com escala por arma. A Barrett levanta poeira.',\n cost: '≤ 60 part.',\n costNote: '1 luz · 50 ms',\n paramLabel: 'Arma',\n when: 'Cada disparo. Em primeira pessoa usa o modelo da arma; de longe vira só o sprite do núcleo.',\n look: 'Três camadas: núcleo branco-amarelo de 1 quadro, pétalas na direção do cano e fumaça cinza que fica 1 a 2 s. A 12 tem clarão largo e fumaça grossa. A Barrett solta dois jatos laterais pelo freio de boca e levanta um anel de poeira do chão. A luz ilumina o cômodo por 50 ms (pedido do brief do mapa).',\n budget: 'Até 60 partículas e 1 luz por tiro. Rajada de AK usa a mesma luz em loop. Pistola sem luz a mais de 15 m.',\n network: 'Cosmético. Sai do evento de tiro que o servidor já manda (arma, origem, direção).',\n },\n impact: {\n name: 'Impacto por material',\n short: 'Concreto, metal, madeira, vidro, carne e água: cada um com sua resposta.',\n cost: '≤ 40 part.',\n costNote: '1 furo',\n paramLabel: 'Material',\n when: 'Toda bala que acerta algo. O material vem da tag de superfície da malha.',\n look: 'Concreto: pó claro em cone e lascas cinza. Metal: chuva de faíscas que quicam, sem pó, com luz curta. Madeira: lascas finas e pó marrom. Vidro: brilho, cacos e rachadura. Carne: sangue pela idade. Água: coroa de gotas e dois anéis.',\n budget: '14 de pó + 6 lascas + até 18 faíscas + 1 furo. A 40 m sobra só o pó.',\n network: 'Evento de impacto de 12 bytes com material em 4 bits.',\n },\n penetration: {\n name: 'Bala que atravessa a horda',\n short: 'Traçante da Barrett passando por 5 zumbis, em câmera lenta 10×.',\n cost: '≤ 250 part.',\n costNote: 'evento agregado',\n when: 'Arma com penetração: AK atravessa 2, Remington 700 atravessa 3, Barrett atravessa 6 e carros.',\n look: 'Traçante com rastro de ar quente, saída de sangue em cada corpo na ordem em que a bala passa (cada um com a cor da sua idade), corpos caindo um atrás do outro e impacto final no concreto.',\n budget: 'Todos os acertos de um tiro saem num único evento agregado com a lista de pontos. 5 corpos = 5 saídas + 5 poças.',\n network: 'O servidor manda o tiro e a lista ordenada de acertos.',\n },\n casings_and_magazine: {\n name: 'Cápsulas e carregador',\n short: 'Latão que quica e fica no chão, carregador vazio caindo, fumaça do cano quente.',\n cost: '200/jogador',\n costNote: 'física 1,5 s',\n when: 'Cada disparo de arma semiautomática, recarga, e bombeada da 12 ou ferrolho do rifle.',\n look: 'Cápsula sai pela janela de ejeção girando, quica no asfalto duas ou três vezes e fica. O carregador vazio cai na recarga. Depois da rajada, fumaça fina sobe do cano quente por 2 s.',\n budget: 'Física só por 1,5 s, depois vira instância estática. Teto de 200 por jogador, o mais antigo some primeiro.',\n network: 'Cosmético, cada cliente gera as suas. Não sincroniza posição no chão.',\n },\n molotov: {\n name: 'Coquetel molotov',\n short: 'Pavio aceso no arco, vidro quebrando e poça de fogo que espalha.',\n cost: '≤ 300 part.',\n costNote: '2 luzes · entidade',\n when: 'Molotov atingindo o chão ou um zumbi.',\n look: 'Garrafa em arco com pavio soltando chama e fumaça. Ao bater, cacos verdes e um respingo de fogo que abre de 0,5 m para 2,2 m em 0,8 s. Chamas baixas, brasas, fumaça escura e luz tremendo por 6 s, depois brasa e mancha queimada.',\n budget: 'Fogo de área com até 300 partículas e 1 luz de fogo. Zumbis em chamas usam um emissor pequeno preso ao corpo (40 partículas) e só os 8 mais próximos.',\n network: 'Fogo é entidade no servidor: área, dano por segundo, tempo. O cliente desenha a partir dela.',\n },\n grenade: {\n name: 'Granada de fragmentação',\n short: 'Quique no asfalto, clarão, onda de choque, estilhaços e anel de poeira.',\n cost: '≤ 400 part.',\n costNote: 'evento agregado',\n when: 'Granada explodindo (pavio de 1,8 s após o arremesso).',\n look: 'Granada quica duas vezes. Explosão em camadas: clarão branco que pinta a tela, bola de fogo curta, anel de choque no chão e outro de frente para a câmera, estilhaços brilhantes que quicam, anel de poeira rasteira, fumaça cinza que sobe e marca queimada. Zumbis no raio caem para fora.',\n budget: 'Até 400 partículas, 1 luz forte de 0,5 s, 16 detritos, 1 marca. Os 40 zumbis atingidos entram num único evento agregado.',\n network: 'O servidor manda a explosão e a lista de atingidos. Visual local.',\n },\n explosive_barrel: {\n name: 'Barril explosivo',\n short: 'Cogumelo de fogo, tampa voando, gasolina queimando no chão e coluna de fumaça.',\n cost: '≤ 450 part.',\n costNote: '2 luzes · entidade de fogo',\n when: 'Barril vermelho com dano acima do limite, tiro ou fogo encostado.',\n look: 'Tudo da granada, mais: bola de fogo que sobe em cogumelo, tampa e pedaços vermelhos voando alto, três poças de gasolina queimando por 5 s e coluna de fumaça preta que serve de referência de longe.',\n budget: 'Pico de 450 partículas. A coluna de fumaça fica visível além de 80 m com 6 sprites grandes.',\n network: 'Explosão + entidade de fogo nas poças.',\n },\n shopfront_glass: {\n name: 'Vitrine estourando',\n short: 'Duas trincas, depois a placa inteira desce em cacos que ficam na calçada.',\n cost: '≤ 80 detritos',\n costNote: '1 rachadura',\n when: 'Vidro com dano acumulado (tiro, golpe, horda empurrando). Barulho de 20 m que atrai a horda.',\n look: 'Primeiros golpes abrem trincas em estrela na textura do vidro. No estouro a placa some e vira 60 a 80 cacos que caem girando, brilhando na luz, e ficam na calçada. Um véu de pó de vidro fino na frente.',\n budget: 'Cacos com física contam no teto de 300 e viram estáticos em 2 s. 3 estágios de rachadura numa máscara só.',\n network: 'O servidor manda o estado do vidro (0 a 3). Cacos são locais.',\n },\n door_stages: {\n name: 'Porta cedendo em 3 estágios',\n short: 'Pancadas soltam pó do batente, uma tábua racha e a porta cai.',\n cost: '≤ 120 part.',\n costNote: '3 estágios',\n when: 'Horda batendo em porta ou barricada (o Brutamonte faz 3× dano). Estágios: 66%, 33%, 0% de vida.',\n look: 'Cada pancada sacode a porta e solta pó do batente e lascas. No estágio 2 a tábua do meio racha e cai. No 3 a porta tomba e levanta uma nuvem de poeira rasteira, e o zumbi sai do escuro.',\n budget: 'Pó: 12 partículas por pancada. Queda: 40 de poeira e 6 detritos. Pancadas a mais de 15 m sem partícula, só som.',\n network: 'O servidor manda a vida da porta. Estágio visual sai do valor.',\n },\n building_collapse: {\n name: 'Desabamento',\n short: 'Pó escorrendo da marquise, blocos caindo e nuvem de poeira que rola pela rua.',\n cost: '≤ 350 part.',\n costNote: '26 detritos',\n when: 'Estrutura que perde a vida: explosão, veículo batendo, horda grande empurrando (destruição de prédios do mapa).',\n look: 'Antes: pó fino escorrendo e pedrinhas caindo por 1 s, como aviso. Depois: blocos grandes e pequenos caem, a câmera treme e uma nuvem de poeira cinza-bege rola pelo chão e fica no ar por 5 s, escondendo zumbis.',\n budget: '26 detritos (6 grandes), 70 sprites de poeira em meia resolução. É evento raro, cabe no orçamento.',\n network: 'O servidor manda o colapso e a área de poeira, que bloqueia visão de zumbi por 4 s.',\n },\n block_wall: {\n name: 'Parede derrubada com furos',\n short: 'Rajada deixa furos e lascas; a parede cai em blocos e os furos continuam neles.',\n cost: '≤ 300 part.',\n costNote: '30 blocos · furos presos',\n when: 'Parede, mureta ou barricada de concreto que perde a vida por tiros, explosão, Brutamonte ou carro (destruição do mapa e do arsenal).',\n look: 'Cada bala abre um furo escuro com borda lascada, solta pó e pedrinhas. Quando a parede cede, ela se parte em blocos que tombam com física e uma nuvem de poeira rola pelo chão. Os furos de bala estão presos aos blocos: ficam nos pedaços caídos, girados junto com eles, e não somem quando a parede quebra.',\n budget: 'Furos são decalques filhos do pedaço (não do mundo), até 12 por bloco e 128 no total. Blocos contam no teto de 300 detritos e viram estáticos em repouso. Poeira: 60 sprites em meia resolução.',\n network: 'O servidor manda vida da parede e a lista de furos (posição local de 2 bytes por furo). Quem entra depois vê os mesmos furos nos mesmos pedaços. Queda dos blocos usa semente comum.',\n },\n metal_door: {\n name: 'Porta de metal amassando',\n short: 'Cada pancada afunda a chapa, descasca a tinta e solta faísca; a dobradiça arrebenta.',\n cost: '≤ 160 part.',\n costNote: 'malha deformável',\n when: 'Porta, portão ou chapa de aço levando golpe de zumbi (Brutamonte 3×), tiro de 12 ou batida de carro.',\n look: 'A chapa afunda onde apanha (malha deformada, não troca de modelo), com 3 níveis visíveis. Cada pancada solta faísca, lasca de tinta verde e pó de ferrugem. Quando a vida acaba, a dobradiça de cima estoura em faíscas, a porta tomba e raspa no chão soltando mais faísca.',\n budget: 'Deformação por vértice só na hora do golpe (CPU, 400 vértices). Por golpe: 8 faíscas, 5 lascas, 6 de ferrugem. Queda: 30 faíscas e 40 de poeira.',\n network: 'O servidor guarda 3 bits de amassado por zona da porta (9 zonas). Quem entra depois vê a mesma porta amassada.',\n },\n iron_railing: {\n name: 'Grade de ferro entortando',\n short: 'A horda empurra, as barras envergam, a solda estoura em faísca e a grade tomba.',\n cost: '≤ 200 part.',\n costNote: '3 barras deformáveis',\n when: 'Grade, portão de ferro, corrimão ou alambrado com zumbis empurrando (pressão da horda) ou carro batendo.',\n look: 'As barras envergam aos poucos no ponto de pressão e as verticais inclinam junto. Ferrugem cai em fio durante o esforço. Em dois momentos a solda estoura com um estalo de faíscas. No fim o poste do meio cede e a grade tomba, deixando o ferro torto no chão.',\n budget: 'Curvatura calculada só quando a pressão muda, até 180 vértices. Ferrugem: 10 por segundo durante o esforço. Estouro de solda: 20 faíscas.',\n network: 'O servidor manda a pressão (0 a 1) e quando cede. A curva sai do valor, igual em todos os clientes.',\n },\n wooden_fence: {\n name: 'Cerca de madeira quebrando',\n short: 'Tábuas partem uma a uma, lascas e pó, e a seção tomba com a horda passando.',\n cost: '≤ 150 part.',\n costNote: '12 tábuas',\n when: 'Cerca, portão de quintal ou barricada de tábuas recebendo tiro, golpe ou pressão da horda.',\n look: 'Cada tábua parte num ponto aleatório: o toco fica preso no trilho e o pedaço de cima voa com lascas e pó de madeira. O zumbi aparece pelo vão. No fim o trilho racha e a seção inteira tomba para frente.',\n budget: 'Cada tábua é uma peça: quebra troca para toco + pedaço com física. Por quebra: 6 lascas, 8 de pó. Tombo: 35 de poeira.',\n network: 'O servidor manda quais tábuas quebraram (12 bits) e quando a seção cai.',\n },\n hit_point_breakage: {\n name: 'Quebra pelo ponto atingido',\n short: 'Tiro na perna, facão na quina, taco no tampo e 12 de perto: cada arma quebra a parte que pegou.',\n cost: '≤ 120 part.',\n costNote: 'peças com vida própria',\n when: 'Qualquer item destrutível. O dano vai para a peça que foi atingida, não para o item inteiro.',\n look: 'Tiro de 9 mm na perna da frente: a parte de baixo da perna parte e a mesa tomba para aquele canto. Facão na quina do tampo: a quina sai inteira num corte reto, com lascas em linha. Taco no outro canto: o tampo racha e se desfaz em pedaços com pó. 12 de perto no que sobrou: 9 furos agrupados que ficam na madeira.',\n budget: 'Cada peça tem vida e material. Item comum: 4 a 12 peças. Partículas só da peça atingida, pelo material dela.',\n network: 'O servidor manda qual peça perdeu vida e quanto. Peça solta simula no cliente; a posição final é mandada uma vez.',\n },\n pressure_cylinders: {\n name: 'Extintor e cilindro de oxigênio',\n short: 'Tiro no extintor faz nuvem branca e giro; no oxigênio a válvula quebra e o cilindro vira foguete.',\n cost: '≤ 250 part.',\n costNote: 'entidade de nuvem',\n paramLabel: 'Cilindro',\n when: 'Tiro, golpe forte ou fogo em extintor, cilindro de oxigênio ou de solda (chaves extintor e cilindro_oxigenio do catálogo do mapa).',\n look: 'Extintor: o furo solta um jato de pó branco, o extintor cai, gira no chão empurrado pelo jato e enche uma área de 5 m de nuvem que tapa a visão por 6 s. Oxigênio: a válvula quebra com estalo, o jato vira propulsão, o cilindro dispara rente ao chão raspando e soltando faísca, quica e para numa parede. Com fogo por perto, o oxigênio estoura como o barril.',\n budget: 'Jato: 60 partículas por segundo por 3 s. Nuvem do extintor: até 40 sprites grandes. Faísca de raspagem como a do carro.',\n network: 'Nuvem do extintor é entidade no servidor (bloqueia visão de zumbi e de jogador). A trajetória do cilindro usa semente comum e o servidor corrige a posição final.',\n },\n building_fire: {\n name: 'Prédio em chamas',\n short: 'Fogo saindo pelas janelas, lambendo a fachada, brasas e fumaça com vento.',\n cost: '≤ 600 part.',\n costNote: '2 luzes · referência',\n when: 'Incêndio de prédio (molotov, explosão, veículo). Dura minutos e espalha para o vizinho.',\n look: 'Janelas acesas em laranja que tremem, línguas de fogo saindo pela parte de cima de cada janela e subindo pela fachada, brasas levadas pelo vento e uma coluna de fumaça grossa inclinada. A luz pinta a rua.',\n budget: 'Até 600 partículas por prédio e 2 luzes de fogo. Além de 80 m fica só a coluna de fumaça e o brilho das janelas, que viram ponto de referência.',\n network: 'Entidade de fogo no servidor por prédio, com estado de espalhar.',\n },\n rain_and_puddles: {\n name: 'Chuva, respingos e poças',\n short: 'Gotas com vento, respingos e anéis só perto da câmera, relâmpago no temporal.',\n cost: '≤ 4.000 gotas',\n costNote: 'respingo até 12 m',\n paramLabel: 'Intensidade',\n when: 'Clima do mundo. Chuva aumenta o ruído de fundo e mascara passos (dá para usar na furtividade).',\n look: 'Garoa fina quase vertical. Chuva com respingos no asfalto e anéis nas poças, que ficam escuras e espelhadas. Temporal com gotas inclinadas pelo vento, cortinas de água passando e relâmpago duplo que clareia a cena por 0,2 s e revela a horda.',\n budget: 'Até 4.000 gotas presas à câmera numa caixa de 30 m. Respingos e anéis só a menos de 12 m e só em cima do que a câmera vê.',\n network: 'O servidor manda o clima e a semente do relâmpago, para todos verem o mesmo clarão.',\n },\n fog_and_ash: {\n name: 'Neblina e cinzas',\n short: 'Névoa rasteira em camadas, cinza caindo e o poste com cone de luz.',\n cost: '≤ 120 sprites',\n costNote: '1 cone de luz',\n when: 'Madrugada, perto de rio e em zonas queimadas (cinza). Reduz a visão dos zumbis e do jogador.',\n look: 'Neblina do motor mais uma camada de névoa baixa que se move devagar, cinza fina caindo em espiral e o feixe dos postes visível. Silhuetas de zumbis aparecem antes dos detalhes.',\n budget: 'Até 60 sprites grandes de névoa (meia resolução) e 60 de cinza. O cone do poste é uma malha aditiva, não luz volumétrica real.',\n network: 'Clima vem do servidor. Densidade é local por zona.',\n },\n scraping_sparks: {\n name: 'Faíscas de raspagem',\n short: 'Para-choque arrastando no asfalto: rio de faíscas que quicam e marca no chão.',\n cost: '≤ 200 faíscas/s',\n costNote: '1 luz',\n when: 'Peça de carro em contato com o chão ou com parede (para-choque caído, roda sem pneu, carro capotado deslizando).',\n look: 'Jato de faíscas para trás do movimento, que quicam no asfalto e apagam do branco para o laranja. Brilho e luz curta no ponto de contato. Marca escura riscada no chão.',\n budget: 'Até 200 faíscas por segundo, 1 luz. A mais de 40 m vira só o brilho do ponto.',\n network: 'Cosmético, a partir do estado da peça e da velocidade que já são replicados.',\n },\n engine_smoke: {\n name: 'Fumaça do motor',\n short: 'Vapor branco, fumaça cinza, depois preta com fogo, conforme a condição.',\n cost: '≤ 120 part.',\n costNote: 'contínuo',\n paramLabel: 'Condição',\n when: 'Sempre que o motor está ligado ou quente, a partir da condição da peça (0 a 100) do brief da frota.',\n look: '100: só escapamento. 60: fio de vapor branco saindo pelo capô. 30: fumaça cinza constante e faísca de vez em quando. 10: fumaça preta grossa, chamas na grade e luz de fogo, aviso de que vai explodir.',\n budget: 'Contínuo, então barato: até 40 partículas por segundo e 1 luz só no estado crítico.',\n network: 'Sai da condição do motor, que já é replicada.',\n },\n fuel_leak: {\n name: 'Vazamento e incêndio do tanque',\n short: 'Gasolina pingando, poça, uma faísca e o fogo correndo até o tanque.',\n cost: '≤ 400 part.',\n costNote: 'entidade de fogo',\n when: 'Tanque furado por tiro ou batida. Faísca, tiro ou fogo próximo acende a poça.',\n look: 'Pingos de gasolina com brilho oleoso formando uma poça que cresce. Um tiro no metal solta faísca, o fogo acende na poça, corre até o carro e, se ninguém apagar, o carro explode.',\n budget: 'Pingos: 12 por segundo. Fogo de poça como o do molotov. A explosão final é o efeito seguinte.',\n network: 'Poça e fogo são entidades no servidor (podem queimar zumbis e jogadores).',\n },\n car_explosion: {\n name: 'Explosão do carro',\n short: 'Bola de fogo, carro pulando, capô e portas voando, carcaça queimando.',\n cost: '≤ 600 part.',\n costNote: 'evento raro',\n when: 'Tanque em chamas por mais de 4 s, ou dano acima do limite em carro com combustível.',\n look: 'Explosão grande (escala 1,8 da granada), o carro pula e cai torto, capô, portas e uma roda saem voando, vidro estoura em cacos. A lataria fica preta e a carcaça queima com fumaça preta por minutos.',\n budget: 'Pico de 600 partículas e 34 detritos. É evento raro e pode passar do teto por evento, nunca do teto global.',\n network: 'Explosão no servidor. O carro vira carcaça (outro prefab) para todos.',\n },\n windshield: {\n name: 'Para-brisa: sangue e rachadura',\n short: 'Visão de dentro do carro: pancadas, trincas em 4 estágios e mão de sangue.',\n cost: '0 partículas',\n costNote: 'máscara 4 estágios',\n when: 'Zumbi batendo no para-brisa ou no vidro lateral (brief da frota: vidro aguenta 8 golpes).',\n look: 'Cada golpe treme a câmera e soma uma trinca em estrela. Estágio 2 e 4 deixam mancha de sangue com marca de mão escorrendo. No estágio 4 o vidro está branco de rachadura e o próximo golpe quebra.',\n budget: 'Só textura: uma máscara de rachadura com 4 estágios e um decal de sangue no vidro. Pó de vidro: 6 partículas por golpe.',\n network: 'O servidor manda a vida do vidro. Sangue no vidro é local.',\n },\n headlights_in_rain: {\n name: 'Faróis na chuva',\n short: 'Feixe visível, gotas acesas dentro do cone e a horda entrando na luz.',\n cost: '≤ 600 gotas acesas',\n costNote: '2 spots sem sombra',\n when: 'Faróis ligados à noite, com chuva ou neblina.',\n look: 'Dois cones de luz visíveis no ar, gotas de chuva que brilham só dentro do feixe e silhuetas de zumbi que acendem quando entram na luz. Fora do cone, a chuva quase some.',\n budget: '2 spotlights sem sombra, 2 cones aditivos e até 600 gotas extras só dentro do cone. Faróis de carros a mais de 40 m ficam só como cone, sem luz real.',\n network: 'Estado do farol já é replicado com o carro.',\n },\n damage_and_bite: {\n name: 'Dano e mordida',\n short: 'Borda vermelha do lado do golpe, sangue na lente e o pulso da mordida.',\n cost: 'pós-processo',\n costNote: 'só local',\n when: 'Jogador recebe golpe (com direção) ou mordida (infecção 100%, regra dos personagens).',\n look: 'Golpe: borda vermelha do lado de onde veio, tremor curto e respingos na lente. Mordida: clarão vermelho na tela inteira, a cor some por 2 s, batimento pulsando no túnel escuro e sangue grande na lente que escorre.',\n budget: 'Efeito de pós-processo e uma camada de lente. Nenhuma partícula no mundo além do sangue normal.',\n network: 'Só na tela de quem levou. Os outros veem o sangue no mundo.',\n },\n infection_phases: {\n name: 'Fases da infecção',\n short: 'Mal-estar, febre com borda fria, delírio com fantasmas e o fim.',\n cost: 'pós-processo',\n costNote: 'só local',\n paramLabel: 'Fase',\n when: 'Fases do brief de personagens: 2–12 h mal-estar, 12–30 h febre, 30–48 h delírio, 48 h morte.',\n look: 'Mal-estar: cor um pouco lavada e vinheta que respira devagar. Febre: borda fria azulada só para o infectado, desfoque em ondas, tosse que treme a câmera. Delírio: aberração de cor, matiz que oscila e zumbis falsos que piscam na neblina. 48 h: quase sem cor, tom verde, túnel escuro e batimento.',\n budget: 'Só pós-processo. Os fantasmas do delírio são 3 instâncias do zumbi sem colisão, só no cliente infectado.',\n network: 'Nunca vai pela rede. O outro jogador percebe só a tosse e a respiração (ver “Tosse e respiração”).',\n },\n panic: {\n name: 'Pânico',\n short: 'A mira treme, o túnel fecha conforme a horda chega e o coração acelera.',\n cost: 'pós-processo',\n costNote: 'só local',\n when: 'Medidor de pânico dos personagens (sobe com zumbis visíveis, corredores e companheiro morto). Efeitos acima de 60 e de 90.',\n look: 'Tremor fino e constante na câmera (a mira treme), vinheta escura que fecha conforme o zumbi mais próximo chega e pulsa com o batimento, que acelera de 70 para 150 bpm. A cor perde força.',\n budget: 'Só pós-processo e ruído de câmera.',\n network: 'Local. O pânico dos outros aparece na mira deles e no grito (som).',\n },\n bloater_blindness: {\n name: 'Cegueira do Inchado',\n short: 'Gás verde na cara: tela turva por 5 s e muco na lente.',\n cost: 'pós + 30 part.',\n costNote: 'entidade do servidor',\n when: 'Jogador dentro da nuvem do Inchado (raio 4 m) quando ela nasce.',\n look: 'Nuvem verde explode entre o zumbi e a câmera, a tela fica verde e desfocada e clareia em 5 s, com gotas de muco verde na lente que escorrem. Tosse treme a câmera duas vezes.',\n budget: 'Pós-processo + 30 sprites de gás perto da câmera, com fade para não cobrir a tela inteira de partícula.',\n network: 'A cegueira vem da entidade no servidor; o visual é local.',\n },\n campfire: {\n name: 'Fogueira do acampamento',\n short: 'Chama baixa, brasas subindo em espiral e luz quente tremendo.',\n cost: '≤ 120 part.',\n costNote: '1 luz de fogo',\n when: 'Fogueira acesa pelo grupo ou por NPCs. Esquenta, ilumina e atrai zumbis de longe (luz).',\n look: 'Chamas pequenas e baixas, brasas que sobem girando e somem, fumaça fina e uma luz quente que treme e pinta o sobrevivente.',\n budget: '120 partículas e 1 luz de fogo. Várias fogueiras do acampamento dividem a mesma luz se estiverem a menos de 6 m.',\n network: 'Objeto de cena replicado (aceso ou apagado). Visual local.',\n },\n cough_and_breath: {\n name: 'Tosse, respiração e lanterna',\n short: 'Vapor da respiração no frio, tosse da febre visível para os outros e feixe da lanterna.',\n cost: '≤ 40 part.',\n costNote: '1 spot com sombra',\n when: 'Noite fria: respiração sempre. Febre da infecção: tosse a cada poucos segundos, audível a 10 m.',\n look: 'Sopro curto de vapor a cada 2,4 s. Na tosse, três sopros rápidos e maiores e o corpo dá um tranco. A lanterna mostra o feixe na neblina e é a única luz com sombra.',\n budget: 'Vapor: 8 partículas por sopro, só a menos de 15 m. Lanterna: 1 spot com sombra por jogador, só a do jogador local projeta sombra.',\n network: 'A tosse é o sinal social da infecção: o servidor manda o evento de tosse (sem dizer que é infecção) e os outros veem o vapor e ouvem.',\n },\n}\n","import { en } from './locales/en'\nimport { ptBR } from './locales/pt-BR'\n\n/** Locales shipped with the package. Unknown locales fall back to `en`. */\nexport type VfxLocale = 'en' | 'pt-BR'\n\n/** The texts of one Workshop effect: its card in the list and its sheet (when, how it looks, what it costs, what goes over the network). */\nexport interface EffectTexts {\n readonly name: string\n readonly short: string\n /** The cost shown on the card, and what comes with it. */\n readonly cost: string\n readonly costNote: string\n /** The label of the effect's parameter, when it has one. */\n readonly paramLabel?: string\n readonly when: string\n readonly look: string\n readonly budget: string\n readonly network: string\n}\n\n/** The words around the sheet (`renderSheet`, W1266). */\nexport interface SheetLabels {\n readonly when: string\n readonly look: string\n readonly budget: string\n readonly network: string\n readonly quality: string\n readonly replay: string\n readonly auto: string\n}\n\nexport interface VfxStrings {\n readonly effects: Readonly<Record<string, EffectTexts>>\n readonly sheet: SheetLabels\n readonly groups: Readonly<Record<string, string>>\n}\n\nexport const locales: Record<VfxLocale, VfxStrings> = {\n en: {\n effects: en,\n sheet: { when: 'When', look: 'Look', budget: 'Budget', network: 'Network', quality: 'Quality', replay: 'Replay', auto: 'Auto replay' },\n groups: { explosions: 'Explosions', creatures: 'Zombies', weapons: 'Weapons and explosives', world: 'Map, weather and destruction', destruction: 'Scenery destruction', vehicles: 'Vehicles', screen: 'Characters and screen effects' },\n },\n 'pt-BR': {\n effects: ptBR,\n sheet: { when: 'Quando', look: 'Visual', budget: 'Orçamento', network: 'Rede', quality: 'Qualidade', replay: 'Repetir', auto: 'Repetição automática' },\n groups: { explosions: 'Explosões', creatures: 'Zumbis', weapons: 'Armas e explosivos', world: 'Mapa, clima e destruição', destruction: 'Destruição de cenário', vehicles: 'Veículos', screen: 'Personagens e efeitos de tela' },\n },\n}\n\nexport const resolveStrings = (locale: string | undefined): VfxStrings => locales[(locale as VfxLocale) ?? 'en'] ?? locales.en\n","import type { Vec3Tuple } from '../workshop/types'\n\nconst ANIMATION_SPEED = 1.4\nconst START_POSITION: Vec3Tuple = [4, 2.4, 7]\nconst START_TARGET: Vec3Tuple = [0, 1, 0]\n\ntype Vector = [number, number, number]\n\nfunction subtract(a: Vec3Tuple, b: Vec3Tuple): Vector {\n return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]\n}\n\nfunction unit(vector: Vector): Vector {\n const length = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]) || 1\n const inverse = 1 / length\n return [vector[0] * inverse, vector[1] * inverse, vector[2] * inverse]\n}\n\nfunction crossProduct(a: Vector, b: Vector): Vector {\n return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]\n}\n\nfunction ease(value: number): number {\n return value < 0 ? 0 : value > 1 ? 1 : value * value * (3 - 2 * value)\n}\n\nfunction mix(from: Vec3Tuple, to: Vec3Tuple, amount: number): Vec3Tuple {\n return [from[0] + (to[0] - from[0]) * amount, from[1] + (to[1] - from[1]) * amount, from[2] + (to[2] - from[2]) * amount]\n}\n\n/** Rotates a vector by the quaternion `(x, y, z, w)` as three.js `applyQuaternion` does. */\nfunction rotate(local: readonly number[], x: number, y: number, z: number, w: number): Vec3Tuple {\n const [vx, vy, vz] = local as Vec3Tuple\n const ix = w * vx + y * vz - z * vy\n const iy = w * vy + z * vx - x * vz\n const iz = w * vz + x * vy - y * vx\n const iw = -x * vx - y * vy - z * vz\n return [ix * w + iw * -x + iy * -z - iz * -y, iy * w + iw * -y + iz * -x - ix * -z, iz * w + iw * -z + ix * -y - iy * -x]\n}\n\n/** The quaternion three.js builds from `lookAt(eye, target)` with the y axis up (`setFromRotationMatrix`). */\nfunction lookAtQuaternion(eye: Vec3Tuple, target: Vec3Tuple): readonly [number, number, number, number] {\n const z = unit(subtract(eye, target))\n const x = unit(crossProduct([0, 1, 0], z))\n const y = crossProduct(z, x)\n const [m11, m21, m31, m12, m22, m32, m13, m23, m33] = [x[0], x[1], x[2], y[0], y[1], y[2], z[0], z[1], z[2]]\n const trace = m11 + m22 + m33\n if (trace > 0) {\n const scale = 0.5 / Math.sqrt(trace + 1)\n return [(m32 - m23) * scale, (m13 - m31) * scale, (m21 - m12) * scale, 0.25 / scale]\n }\n if (m11 > m22 && m11 > m33) {\n const scale = 2 * Math.sqrt(1 + m11 - m22 - m33)\n return [0.25 * scale, (m12 + m21) / scale, (m13 + m31) / scale, (m32 - m23) / scale]\n }\n if (m22 > m33) {\n const scale = 2 * Math.sqrt(1 + m22 - m11 - m33)\n return [(m12 + m21) / scale, 0.25 * scale, (m23 + m32) / scale, (m13 - m31) / scale]\n }\n const scale = 2 * Math.sqrt(1 + m33 - m11 - m22)\n return [(m13 + m31) / scale, (m23 + m32) / scale, 0.25 * scale, (m21 - m12) / scale]\n}\n\n/**\n * The Workshop's view (W1259-1262): it glides to each effect's `view` (`camTo`) in about 0.7 s and persists between effects, so an effect that reads the\n * camera (a ring that faces the viewer, a mist at the lens) sees where the previous one left it.\n */\nexport class EffectCamera {\n position: Vec3Tuple = START_POSITION\n target: Vec3Tuple = START_TARGET\n private animation: { from: Vec3Tuple; fromTarget: Vec3Tuple; to: Vec3Tuple; toTarget: Vec3Tuple; progress: number } | undefined\n\n goTo(eye: Vec3Tuple, target: Vec3Tuple): void {\n this.animation = { from: this.position, fromTarget: this.target, to: eye, toTarget: target, progress: 0 }\n }\n\n place(eye: Vec3Tuple, target: Vec3Tuple): void {\n this.position = eye\n this.target = target\n this.animation = undefined\n }\n\n hold(): void {\n this.animation = undefined\n }\n\n update(dt: number): void {\n const animation = this.animation\n if (!animation) return\n animation.progress = Math.min(1, animation.progress + dt * ANIMATION_SPEED)\n const eased = ease(animation.progress)\n this.position = mix(animation.from, animation.to, eased)\n this.target = mix(animation.fromTarget, animation.toTarget, eased)\n if (animation.progress >= 1) this.animation = undefined\n }\n\n /** A direction given in the camera's own axes, in world axes. */\n forward(local: readonly number[]): Vec3Tuple {\n const [x, y, z, w] = lookAtQuaternion(this.position, this.target)\n return rotate(local, x, y, z, w)\n }\n}\n","import type { RandomStream } from '../random'\nimport type { WorkshopEngine } from '../workshop/engine'\nimport type { Vec3Tuple } from '../workshop/types'\nimport type { Behavior } from './behavior'\nimport { EffectCamera } from './camera'\nimport { EffectInstance } from './instance'\nimport type { CueRequest, EffectDef, EffectStep } from './types'\n\nconst SHAKE_SCALE = 0.06\nconst SHAKE_DEPTH = 0.5\n\nexport interface EffectRunnerOptions {\n readonly engine: WorkshopEngine\n readonly effects: Readonly<Record<string, EffectDef>>\n readonly behaviors?: Readonly<Record<string, Behavior>>\n readonly onCue?: (cue: CueRequest) => void\n}\n\n/**\n * The Workshop's one-effect-at-a-time view (W1253-1295): choosing an effect clears the set and runs it; each frame fires the timers due, runs the tick, ages\n * the pools, glides the camera and draws the camera shake. This is what the bench shows and what the golden replays; `createEffectPlayer` plays many at once.\n */\nexport class EffectRunner {\n readonly camera = new EffectCamera()\n readonly instance: EffectInstance\n /** The camera shake of the last frame, three draws per frame as in the page. */\n shake: Vec3Tuple = [0, 0, 0]\n\n constructor(private readonly options: EffectRunnerOptions) {\n const behaviors = options.behaviors ?? {}\n this.instance = new EffectInstance({\n engine: options.engine,\n view: this.camera,\n behavior: (id) => behaviors[id]!,\n behaviors: () => Object.values(behaviors),\n ...(options.onCue ? { onCue: options.onCue } : {}),\n })\n }\n\n get current(): EffectDef | undefined {\n return this.instance.def\n }\n\n get variables(): Map<string, unknown> {\n return this.instance.variables\n }\n\n get param(): number {\n return this.instance.param\n }\n\n get fresh(): boolean {\n return this.instance.fresh\n }\n\n get time(): number {\n return this.instance.time\n }\n\n /** Seconds since the effect was chosen: unlike `time`, a restart of the loop does not reset it (`el`). */\n get elapsed(): number {\n return this.instance.elapsed\n }\n\n get random(): RandomStream {\n return this.options.engine.random\n }\n\n /** `select` + `startLoop` (W1253-1257): clears everything, then runs the effect's setup and timeline. */\n start(id: string, param?: number): boolean {\n const def = this.options.effects[id]\n if (!def) return false\n if (def.view) this.camera.goTo(def.view.eye, def.view.target)\n else this.camera.hold()\n this.options.engine.clear()\n this.instance.begin(def, param)\n return true\n }\n\n /** `startLoop` again: the same effect from its start (the replay button and the end of the loop). */\n restart(): void {\n if (!this.instance.def) return\n this.options.engine.clear()\n this.instance.restart()\n }\n\n /** One frame of the page (W1280-1295). */\n update(dt: number): void {\n const engine = this.options.engine\n engine.post = {}\n this.instance.step(dt)\n engine.update(dt)\n this.camera.update(dt)\n const amplitude = engine.screen.shake * SHAKE_SCALE + this.instance.tremor\n const random = this.random\n this.shake = [random.between(-1, 1) * amplitude, random.between(-1, 1) * amplitude, random.between(-1, 1) * amplitude * SHAKE_DEPTH]\n }\n\n execute(steps: readonly EffectStep[]): void {\n this.instance.execute(steps)\n }\n}\n","import type { RandomStream } from './random'\n\nexport interface GroundFieldOptions {\n /** Side of the deformable patch in metres (the Lab's 60). */\n readonly size?: number\n /** Cells per side (the Lab's 180: 33 cm cells). */\n readonly segments?: number\n}\n\n/** The deformable ground the Lab's craters dig into (L60-83): a height field, in float32 like the page, centred on the origin. */\nexport interface GroundField {\n readonly size: number\n readonly segments: number\n /** Vertices per side. */\n readonly stride: number\n readonly cellSize: number\n /** World coordinate of the first row and column. */\n readonly origin: number\n readonly heights: Float32Array\n /** Goes up on every dent or reset: a renderer redraws its mesh when it changes. */\n readonly version: number\n heightAt(x: number, z: number): number\n /** A bowl with an uneven floor and a raised rim, drawing from `random` like the Lab's `dent`. */\n dent(random: RandomStream, x: number, z: number, radius: number, depth: number): void\n reset(): void\n}\n\nconst DEFAULT_SIZE = 60\nconst DEFAULT_SEGMENTS = 180\nconst MAX_DEPTH = 3\nconst RIM_REACH = 1.45\nconst REACH_FACTOR = 1.5\n\nexport function createGroundField(options: GroundFieldOptions = {}): GroundField {\n const size = options.size ?? DEFAULT_SIZE\n const segments = options.segments ?? DEFAULT_SEGMENTS\n const stride = segments + 1\n const cellSize = size / segments\n const origin = -size / 2\n const heights = new Float32Array(stride * stride)\n let version = 0\n\n function heightAt(x: number, z: number): number {\n const fx = (x - origin) / cellSize\n const fz = (z - origin) / cellSize\n if (fx < 0 || fz < 0 || fx >= segments || fz >= segments) return 0\n const column = fx | 0\n const row = fz | 0\n const tx = fx - column\n const tz = fz - row\n const index = row * stride + column\n return (heights[index]! * (1 - tx) + heights[index + 1]! * tx) * (1 - tz) + (heights[index + stride]! * (1 - tx) + heights[index + stride + 1]! * tx) * tz\n }\n\n function dent(random: RandomStream, x: number, z: number, radius: number, depth: number): void {\n const reach = radius * REACH_FACTOR\n const column0 = Math.max(0, Math.floor((x - reach - origin) / cellSize))\n const column1 = Math.min(segments, Math.ceil((x + reach - origin) / cellSize))\n const row0 = Math.max(0, Math.floor((z - reach - origin) / cellSize))\n const row1 = Math.min(segments, Math.ceil((z + reach - origin) / cellSize))\n const phaseA = random.next() * 6\n const phaseB = random.next() * 6\n for (let row = row0; row <= row1; row++) {\n for (let column = column0; column <= column1; column++) {\n const px = origin + column * cellSize\n const pz = origin + row * cellSize\n const distance = Math.hypot(px - x, pz - z)\n const angle = Math.atan2(pz - z, px - x)\n const q = distance / (radius * (1 + 0.13 * Math.sin(angle * 5 + phaseA) + 0.07 * Math.sin(angle * 11 + phaseB)))\n let change = 0\n if (q < 1) change = -depth * (1 - q * q) * (0.85 + random.next() * 0.3)\n else if (q < RIM_REACH) {\n const k = (q - 1) / 0.45\n change = depth * 0.3 * Math.sin(k * Math.PI) * (1 - k * 0.3) * (0.6 + random.next() * 0.8)\n }\n if (!change) continue\n const index = row * stride + column\n heights[index] = Math.max(-MAX_DEPTH, heights[index]! + change)\n }\n }\n version++\n }\n\n function reset(): void {\n heights.fill(0)\n version++\n }\n\n return {\n size,\n segments,\n stride,\n cellSize,\n origin,\n heights,\n get version() {\n return version\n },\n heightAt,\n dent,\n reset,\n }\n}\n","import type { Puddle, VfxWorld } from '@adatechnology/game-vfx'\nimport { inPuddle } from '@adatechnology/game-vfx'\nimport type { WeatherSample } from '@adatechnology/game-weather'\nimport type { Vec3 } from './types'\n\n/** The rain (mm/h) from which a drop that lands splashes (the Workshop's garoa, 2.5, does not; its chuva, 15, does). */\nexport const SPLASH_FROM_MM = 5\n\nexport type NearRainOptions = {\n /** Ground ellipses where a drop leaves a ring (the Workshop's four dark puddles; a game has its own). None by default. */\n readonly puddles?: readonly Puddle[]\n /** Replaces the puddle test. */\n readonly puddleAt?: (x: number, z: number) => boolean\n readonly splashFrom?: number\n}\n\nexport type NearRain = {\n update(sample: WeatherSample, dt: number, camera: Vec3): void\n /** Streaks alive. */\n count(): number\n clear(): void\n}\n\n/**\n * The Workshop's near rain (W313-331) driven by the weather: the streaks live in the world's Workshop engine (`engine.rain`), game-vfx-three draws them, and the\n * splashes and rings they leave go to the `normal` sprite pool and the rings. `drops` (900 garoa, 2 200 chuva, 4 000 temporal) and the wind along x are the Workshop's own\n * numbers for its three intensities.\n */\nexport function createNearRain(vfx: VfxWorld | undefined, options: NearRainOptions = {}): NearRain {\n const puddles = options.puddles ?? []\n return {\n update(sample, dt, camera) {\n const raining = sample.rain > 0\n const engine = raining ? vfx?.workshop() : vfx?.peekWorkshop()\n if (!engine) return\n const field = engine.rain\n if (!raining) {\n if (field.on) field.clear()\n return\n }\n field.puddleAt = options.puddleAt ?? ((x, z) => inPuddle(puddles, x, z))\n field.drive(sample.drops, sample.windX, camera[0], camera[2])\n field.update(dt, sample.rain >= (options.splashFrom ?? SPLASH_FROM_MM), camera)\n },\n count: () => vfx?.peekWorkshop()?.rain.count ?? 0,\n clear() {\n vfx?.peekWorkshop()?.rain.clear()\n },\n }\n}\n","import type * as THREE_NS from 'three'\nimport type { WeatherSample } from '@adatechnology/game-weather'\nimport type { ThreeLib, WeatherProfileName } from './types'\n\n/**\n * The game's rain curtain (jogo.js `const RAIN = 14000`): a box of points that follows the camera, 70 m wide and 25 m high, falling 13 m/s. The shader texts and the\n * numbers are the page's, character for character (a golden cuts them from the page and compares).\n */\nexport const RAIN_CURTAIN = { count: 14000, width: 70, height: 25, speed: 13, pointSize: 1.6 } as const\n\nexport const RAIN_CURTAIN_VERTEX =\n 'uniform float uTime; uniform vec3 uCam; void main(){ vec3 p = position; p.y = mod(p.y - uTime*13., 25.); p.xz = uCam.xz + mod(p.xz - uCam.xz + 35., 70.) - 35.; gl_Position = projectionMatrix*viewMatrix*vec4(p,1.); gl_PointSize = 1.6; }'\nexport const RAIN_CURTAIN_FRAGMENT = 'uniform float uDay; void main(){ gl_FragColor = vec4(vec3(.68,.72,.78)*(0.25+0.75*uDay), .22+.1*uDay); }'\n\n/** The share of the 14 000 points each quality draws. */\nexport const RAIN_CURTAIN_PROFILES: Readonly<Record<WeatherProfileName, number>> = { low: 0.35, medium: 0.7, high: 1 }\n/** The weather's `drops` at which the whole curtain is drawn (the storm's 4 000). */\nexport const RAIN_CURTAIN_FULL_AT = 4000\n\n/** The page's loop: x and z in -35..35, y in 0..25, three draws a point in that order, stored as Float32. */\nexport function rainCurtainPositions(random: () => number = Math.random, count: number = RAIN_CURTAIN.count): Float32Array {\n const positions = new Float32Array(count * 3)\n for (let index = 0; index < count; index++) {\n positions[index * 3] = (random() - 0.5) * RAIN_CURTAIN.width\n positions[index * 3 + 1] = random() * RAIN_CURTAIN.height\n positions[index * 3 + 2] = (random() - 0.5) * RAIN_CURTAIN.width\n }\n return positions\n}\n\nexport type RainCurtainOptions = {\n readonly random?: () => number\n readonly profile?: WeatherProfileName\n /** Drops (`WeatherSample.drops`) that draw the whole curtain. */\n readonly fullAt?: number\n readonly count?: number\n}\n\nexport type RainCurtain = {\n readonly object: THREE_NS.Points\n /** Draws `sample.drops` of the curtain around `camera`; `daylight` is the game's `s` (0 night, 1 day). */\n update(sample: WeatherSample, camera: { readonly x: number; readonly y: number; readonly z: number }, time: number, daylight: number): void\n setProfile(profile: WeatherProfileName): void\n /** How many points are drawn now. */\n drawn(): number\n dispose(): void\n}\n\nexport function createRainCurtain(THREE: ThreeLib, options: RainCurtainOptions = {}): RainCurtain {\n const count = options.count ?? RAIN_CURTAIN.count\n const geometry = new THREE.BufferGeometry()\n geometry.setAttribute('position', new THREE.BufferAttribute(rainCurtainPositions(options.random, count), 3))\n const uniforms = { uTime: { value: 0 }, uCam: { value: new THREE.Vector3() }, uDay: { value: 1 } }\n const material = new THREE.ShaderMaterial({ uniforms, transparent: true, depthWrite: false, vertexShader: RAIN_CURTAIN_VERTEX, fragmentShader: RAIN_CURTAIN_FRAGMENT })\n const object = new THREE.Points(geometry, material)\n object.frustumCulled = false\n object.name = 'weather-rain-curtain'\n object.visible = false\n let share = RAIN_CURTAIN_PROFILES[options.profile ?? 'high']\n let drawn = 0\n return {\n object,\n update(sample, camera, time, daylight) {\n const fullAt = options.fullAt ?? RAIN_CURTAIN_FULL_AT\n drawn = sample.rain > 0 ? Math.round(count * share * Math.min(1, sample.drops / fullAt)) : 0\n object.visible = drawn > 0\n geometry.setDrawRange(0, drawn)\n uniforms.uTime.value = time\n uniforms.uDay.value = daylight\n uniforms.uCam.value.set(camera.x, camera.y, camera.z)\n },\n setProfile(profile) {\n share = RAIN_CURTAIN_PROFILES[profile]\n },\n drawn: () => drawn,\n dispose() {\n geometry.dispose()\n material.dispose()\n },\n }\n}\n","import type * as THREE_NS from 'three'\nimport type { VfxWorld } from '@adatechnology/game-vfx'\nimport type { Puddle } from '@adatechnology/game-vfx'\nimport { createFog, emitAirborne } from './fog'\nimport type { Fog } from './fog'\nimport { createHeadlights } from './headlights'\nimport type { HeadlightRules, Headlights } from './headlights'\nimport { createHeatHaze } from './heat-haze'\nimport type { HeatHaze } from './heat-haze'\nimport { createLightning } from './lightning'\nimport type { Lightning } from './lightning'\nimport { createNearRain } from './rain-near'\nimport type { NearRain } from './rain-near'\nimport { createRainCurtain } from './rain-curtain'\nimport type { RainCurtain } from './rain-curtain'\nimport { createSky } from './sky'\nimport type { Sky } from './sky'\nimport type { ThreeLib, Vec3, WeatherProfileName, WeatherSource, ZoneFog } from './types'\n\n/** The parts the renderer draws, each of which a layer switches. */\nexport const WEATHER_LAYERS = ['far_rain', 'near_rain', 'mist', 'fog', 'sky', 'lightning', 'headlights', 'haze'] as const\nexport type WeatherLayerId = (typeof WEATHER_LAYERS)[number]\n\nexport type WeatherRendererOptions = {\n readonly scene: THREE_NS.Scene\n readonly camera: THREE_NS.Camera\n readonly weather: WeatherSource\n /** The effects world: near rain, splashes, mist, ash, the white of lightning and the lit drops of headlights live in its Workshop pools (game-vfx-three draws them). */\n readonly vfx?: VfxWorld\n readonly profile?: WeatherProfileName\n /** Seeds the rain curtain (default `Math.random`). */\n readonly random?: () => number\n readonly zone?: ZoneFog\n readonly puddles?: readonly Puddle[]\n readonly puddleAt?: (x: number, z: number) => boolean\n readonly headlights?: HeadlightRules\n readonly layers?: Partial<Record<WeatherLayerId, boolean>>\n}\n\nexport type WeatherRenderer = {\n readonly curtain: RainCurtain\n readonly nearRain: NearRain\n readonly fog: Fog\n readonly sky: Sky\n readonly lightning: Lightning\n readonly headlights: Headlights\n readonly haze: HeatHaze\n /** Draws the weather of the source for a frame of `dt` seconds. Call once per frame, after the weather advanced. */\n update(dt: number): void\n setLayer(id: WeatherLayerId, on: boolean): void\n layer(id: WeatherLayerId): boolean\n setZoneFog(zone: ZoneFog | undefined): void\n setProfile(profile: WeatherProfileName): void\n stats(): { readonly rainCurtain: number; readonly rainNear: number; readonly cars: number }\n dispose(): void\n}\n\n/** The whole weather picture: rain curtain, near rain with splashes, mist and ash, fog, sky, lightning, headlights and heat haze, from one `WeatherSource`. */\nexport function createWeatherRenderer(THREE: ThreeLib, options: WeatherRendererOptions): WeatherRenderer {\n const { scene, camera, weather, vfx } = options\n const group = new THREE.Group()\n group.name = 'weather'\n scene.add(group)\n const curtain = createRainCurtain(THREE, { ...(options.random ? { random: options.random } : {}), ...(options.profile ? { profile: options.profile } : {}) })\n const haze = createHeatHaze(THREE)\n group.add(curtain.object, haze.object)\n const sky = createSky(THREE, scene)\n const fog = createFog(THREE, scene)\n const lightning = createLightning(THREE, scene, vfx)\n const headlights = createHeadlights(THREE, options.headlights)\n const nearRain = createNearRain(vfx, { ...(options.puddles ? { puddles: options.puddles } : {}), ...(options.puddleAt ? { puddleAt: options.puddleAt } : {}) })\n const on = new Map<WeatherLayerId, boolean>(WEATHER_LAYERS.map((id) => [id, options.layers?.[id] ?? true]))\n let zone = options.zone\n let time = 0\n const eye: { x: number; y: number; z: number } = { x: 0, y: 0, z: 0 }\n\n const switchedOff = (id: WeatherLayerId): void => {\n if (id === 'far_rain') curtain.object.visible = false\n else if (id === 'near_rain') nearRain.clear()\n else if (id === 'fog') fog.clear()\n else if (id === 'lightning') lightning.light.intensity = 0\n else if (id === 'haze') haze.object.visible = false\n }\n\n return {\n curtain,\n nearRain,\n fog,\n sky,\n lightning,\n headlights,\n haze,\n update(dt) {\n time += dt\n const sample = weather.sample()\n const position = camera.position\n eye.x = position.x\n eye.y = position.y\n eye.z = position.z\n const where: Vec3 = [eye.x, eye.y, eye.z]\n const background = (scene.background as THREE_NS.Color | null) ?? undefined\n const ownSky = on.get('sky') === true\n const color = ownSky ? sky.update(sample, zone) : background && (background as { isColor?: boolean }).isColor ? background : sky.color\n if (on.get('fog')) fog.update(sample, color, zone)\n if (on.get('lightning')) lightning.update(sample, ownSky ? (scene.background as THREE_NS.Color) : undefined)\n if (on.get('far_rain')) curtain.update(sample, eye, time, sample.sun)\n if (on.get('haze')) haze.update(sample, time)\n if (vfx) {\n if (on.get('near_rain')) nearRain.update(sample, dt, where)\n if (on.get('mist')) emitAirborne(vfx.workshop(), sample, dt, where)\n }\n headlights.update(sample, dt, where, vfx && sample.rain > 0 ? vfx.workshop() : undefined, on.get('headlights'))\n },\n setLayer(id, value) {\n on.set(id, value)\n if (!value) switchedOff(id)\n },\n layer: (id) => on.get(id) === true,\n setZoneFog(next) {\n zone = next\n },\n setProfile(profile) {\n curtain.setProfile(profile)\n },\n stats: () => ({ rainCurtain: curtain.drawn(), rainNear: nearRain.count(), cars: headlights.count() }),\n dispose() {\n scene.remove(group)\n curtain.dispose()\n haze.dispose()\n lightning.dispose()\n headlights.dispose()\n nearRain.clear()\n },\n }\n}\n","import type { Vec3 } from './destruction'\n\n/**\n * The numbers of the shove a blast gives to a body that moves (a loose item, a piece of furniture, a person): all of them a game value, with a default that reads well in a room,\n * not a physical table. A host reads them once and lends them to the modules that move their own bodies (`ModuleHost.blastShove`).\n */\nexport interface BlastShoveOptions {\n /**\n * The share of the blast energy that reaches a body as motion: the speed at the centre is `sqrt(2 * energy * coupling / mass)`, before the falloff (default 0.02).\n */\n readonly coupling?: number\n /** Fastest a body is thrown, in m/s, at the centre of the blast (default 20). */\n readonly maxSpeed?: number\n /** How much of the push goes up: the direction is `(away, lift)` normalised from the horizontal outward direction (default 0.3). */\n readonly lift?: number\n /** Below this speed (m/s) nothing moves: a heavy thing far from a small blast stays where it is (default 0.3). */\n readonly minSpeed?: number\n /** Exponent of the falloff `(1 - distance / radius) ^ power`, the same as the damage of the destruction world (default 2). */\n readonly falloffPower?: number\n}\n\n/** The defaults of `BlastShoveOptions`. */\nexport const DEFAULT_BLAST_SHOVE: Readonly<Required<BlastShoveOptions>> = { coupling: 0.02, maxSpeed: 20, lift: 0.3, minSpeed: 0.3, falloffPower: 2 }\n\n/** A blast as the shove reads it: where, how much energy (J) and how far it reaches (m). `reach` (0..1) is the share that got through what stood in the way. */\nexport interface BlastSource {\n readonly point: Vec3\n readonly energy: number\n readonly radius: number\n readonly reach?: number\n}\n\n/** What a blast does to one body: how fast it leaves (m/s), the unit direction it leaves in, and how far from the centre it was. */\nexport interface BlastShove {\n readonly speed: number\n readonly direction: Vec3\n readonly distance: number\n /** The falloff applied, 0..1. */\n readonly falloff: number\n}\n\n/**\n * The shove a blast gives to a body of `mass` kg whose centre is at `position`: away from the centre, lifted a little, with the speed\n * `min(maxSpeed, sqrt(2 * energy * reach * coupling / mass)) * (1 - distance / radius) ^ 2`. Undefined when the body is outside the radius or the speed is under `minSpeed`.\n * The same rule moves the loose items and furniture of the test room and the people of the characters module, so a game that tunes the options tunes all of them.\n */\nexport function blastShove(blast: BlastSource, target: { readonly position: Vec3; readonly mass: number }, options: BlastShoveOptions = {}): BlastShove | undefined {\n const o = { ...DEFAULT_BLAST_SHOVE, ...options }\n const dx = target.position[0] - blast.point[0]\n const dy = target.position[1] - blast.point[1]\n const dz = target.position[2] - blast.point[2]\n const distance = Math.hypot(dx, dy, dz)\n if (!(blast.radius > 0) || distance > blast.radius) return undefined\n const falloff = (1 - distance / blast.radius) ** o.falloffPower\n const mass = Math.max(target.mass, 0.05)\n const speed = Math.min(o.maxSpeed, Math.sqrt((2 * blast.energy * (blast.reach ?? 1) * o.coupling) / mass)) * falloff\n if (!(speed >= o.minSpeed)) return undefined\n // straight above or below the centre there is no \"away\": the push goes up\n const direction: Vec3 = distance > 1e-4 ? [dx / distance, dy / distance + o.lift, dz / distance] : [0, 1, 0]\n const length = Math.hypot(direction[0], direction[1], direction[2])\n return { speed, direction: [direction[0] / length, direction[1] / length, direction[2] / length], distance, falloff }\n}\n","import type { DamageType, DestructibleSpec, DestructionEvent, Hit, JointSpec, ObjectState, ToolKind, Vec3 } from './destruction'\n\n/**\n * What a game module implements for an object whose behaviour it owns (a vehicle with its own panel deformation, a creature, a machine). The\n * world (`DestructionWorld.register`) hosts it next to its own objects: raycast, shoot, explode, hit, disassemble, extinguish, update, state,\n * reset and `on` all route to it.\n *\n * Space: the impl receives hits in WORLD space and returns events in WORLD space (it owns its transform; the transform given to `register` or\n * `setTransform` is only used by the world for raycasts and blast range). Every event must carry `object === id` and name parts listed by\n * `describe()`.\n */\nexport interface DestructibleImpl {\n readonly id: string\n readonly name?: string\n /** Parts, joints and structure in the impl's local space, for raycast, inspector and log. Called often: keep it cheap. */\n describe(): DestructibleSpec\n /** Apply one hit (see `IMPACT_CONTRACT` for the fields per type) and return the events it caused. */\n hit(hit: Hit): DestructionEvent[]\n /** Advance fire, melting, collapse and other timed behaviour. */\n update?(dt: number): DestructionEvent[]\n state(): ObjectState\n reset?(): void\n /** Same contract as `DestructionWorld.disassemble`: progress 0..1, or -1 when the tool cannot undo the joint. */\n disassemble?(jointId: string, tool: ToolKind, dt: number): number\n extinguish?(partId?: string): void\n /** Optional: an animation moved a part; `position` / `rotation` (Euler XYZ, radians) are object-local. Keep raycast and hit shapes in sync. */\n setPartPose?(partId: string, position: Vec3, rotation?: Vec3): void\n /** Plain JSON of the impl's own state, stored in `DestructionSnapshot.impls`. */\n serialize?(): unknown\n load?(data: unknown): void\n}\n\n/** The fields a weapon or hazard must put in a `Hit`, per damage type. THE impact contract. */\nexport interface ImpactContract {\n required: string[]\n optional: string[]\n note: string\n}\n\nexport const IMPACT_CONTRACT: Record<DamageType, ImpactContract> = {\n ballistic: { required: ['type', 'point', 'direction', 'energy', 'caliber'], optional: ['space', 'source'], note: 'bullets and pellets: joules and diameter in millimetres' },\n blunt: { required: ['type', 'point', 'energy'], optional: ['direction', 'radius', 'space', 'source'], note: 'contact radius in metres (default 3 cm)' },\n slash: { required: ['type', 'point', 'energy'], optional: ['direction', 'radius', 'space', 'source'], note: 'axes, knives, machetes' },\n explosive: { required: ['type', 'point', 'energy', 'radius'], optional: ['lethalRadius', 'space', 'source'], note: 'point is the centre of the blast, radius the range in metres' },\n fire: { required: ['type', 'point', 'temperature', 'radius'], optional: ['duration', 'space', 'source'], note: 'flame temperature in °C; duration in seconds (call every frame with `dt`)' },\n heat: { required: ['type', 'point', 'temperature', 'radius'], optional: ['duration', 'space', 'source'], note: 'heat without flame (cutting torch, hot plate)' },\n}\n\n/** Problems with a hit according to the impact contract (empty when it is valid). */\nexport const checkHit = (hit: Hit): string[] => {\n const c = IMPACT_CONTRACT[hit.type]\n if (!c) return [`unknown damage type \"${String(hit.type)}\"`]\n const out: string[] = []\n for (const f of c.required) {\n const v = (hit as unknown as Record<string, unknown>)[f]\n if (v === undefined || (Array.isArray(v) ? v.some((n) => !Number.isFinite(n)) : typeof v === 'number' && !Number.isFinite(v))) out.push(`${hit.type} hit needs \"${f}\"`)\n }\n return out\n}\n\nexport interface CheckOptions {\n /**\n * Ids of every material the impl may use (the contracts package does not know the materials of your world: pass the ids of\n * `world.materials`, or the built-in plus your own). When omitted, the material check is skipped.\n */\n materials?: Iterable<string>\n}\n\nconst finiteJson = (v: unknown, path: string, out: string[]) => {\n if (typeof v === 'number') {\n if (!Number.isFinite(v)) out.push(`${path} is not a finite number`)\n } else if (Array.isArray(v)) v.forEach((x, i) => finiteJson(x, `${path}[${i}]`, out))\n else if (v && typeof v === 'object') for (const [k, x] of Object.entries(v)) finiteJson(x, `${path}.${k}`, out)\n else if (typeof v === 'function' || typeof v === 'symbol' || typeof v === 'bigint') out.push(`${path} is not JSON`)\n}\n\n/**\n * Contract checker for the test room (it needs no world): `describe()` is valid, a 500 J ballistic hit at a part centre returns well-formed events of this impl, `state()`\n * matches `describe()`, hit points never go negative, `update(0.016)` returns an array and every event is JSON-serializable. It hits the impl\n * (and calls `reset()` afterwards when there is one).\n */\nexport const checkDestructible = (impl: DestructibleImpl, options: CheckOptions = {}): { ok: boolean; problems: string[] } => {\n const problems: string[] = []\n const known = options.materials ? new Set(options.materials) : null\n let spec: DestructibleSpec | undefined\n try {\n spec = impl.describe()\n } catch (e) {\n return { ok: false, problems: [`describe() threw: ${String(e)}`] }\n }\n if (!spec || !Array.isArray(spec.parts) || spec.parts.length === 0) return { ok: false, problems: ['describe() must return at least one part'] }\n const ids = new Set<string>()\n for (const p of spec.parts) {\n if (ids.has(p.id)) problems.push(`duplicate part id \"${p.id}\"`)\n ids.add(p.id)\n if (known && !known.has(p.material)) problems.push(`part \"${p.id}\" has unknown material \"${p.material}\"`)\n if (!p.shape || !['box', 'cylinder', 'sphere'].includes(p.shape.kind)) problems.push(`part \"${p.id}\" has no valid shape`)\n if (!Array.isArray(p.position) || p.position.length !== 3) problems.push(`part \"${p.id}\" has no position`)\n }\n const jointIds = new Set<string>()\n for (const j of (spec.joints ?? []) as JointSpec[]) {\n if (!ids.has(j.a) || !ids.has(j.b)) problems.push(`joint \"${j.id ?? `${j.a}~${j.b}`}\" references an unknown part`)\n const jid = j.id ?? `${j.a}~${j.b}`\n if (jointIds.has(jid)) problems.push(`duplicate joint id \"${jid}\"`)\n jointIds.add(jid)\n }\n const checkState = (label: string) => {\n let st: ObjectState\n try {\n st = impl.state()\n } catch (e) {\n problems.push(`state() threw ${label}: ${String(e)}`)\n return\n }\n const sids = st.parts.map((p) => p.id).sort().join(',')\n if (sids !== [...ids].sort().join(',')) problems.push(`state() part ids do not match describe() ${label}`)\n for (const p of st.parts) if (!(p.hp >= 0)) problems.push(`part \"${p.id}\" has negative or invalid hp ${label}`)\n }\n const checkEvents = (events: unknown, label: string) => {\n if (!Array.isArray(events)) {\n problems.push(`${label} must return an array`)\n return\n }\n const evs = events as DestructionEvent[]\n evs.forEach((e, i) => {\n if (e.object !== impl.id) problems.push(`${label} event ${i} (${e.type}) has object \"${String(e.object)}\", expected \"${impl.id}\"`)\n const part = (e as { part?: string }).part\n if (part !== undefined && !ids.has(part)) problems.push(`${label} event ${i} (${e.type}) names unknown part \"${part}\"`)\n })\n const bad: string[] = []\n finiteJson(evs, 'events', bad)\n problems.push(...bad.map((b) => `${label}: ${b}`))\n try {\n const s = JSON.stringify(evs)\n if (JSON.stringify(JSON.parse(s)) !== s) problems.push(`${label} events do not survive a JSON round trip`)\n } catch (e) {\n problems.push(`${label} events are not JSON-serializable: ${String(e)}`)\n }\n }\n checkState('before the hit')\n const target = spec.parts[0]\n try {\n const hit: Hit = { type: 'ballistic', point: [target.position[0], target.position[1], target.position[2]], direction: [0, 0, -1], energy: 500, caliber: 9 }\n checkEvents(impl.hit(hit), 'hit()')\n } catch (e) {\n problems.push(`hit() threw: ${String(e)}`)\n }\n checkState('after the hit')\n if (impl.update) {\n try {\n checkEvents(impl.update(0.016), 'update()')\n } catch (e) {\n problems.push(`update() threw: ${String(e)}`)\n }\n checkState('after update')\n }\n try {\n impl.reset?.()\n } catch (e) {\n problems.push(`reset() threw: ${String(e)}`)\n }\n return { ok: problems.length === 0, problems }\n}\n","/** The slots of a body an item can cover (the item slot vocabulary: game-items imports it from here). */\nexport type BodySlot = 'head' | 'eyes' | 'torso' | 'arms' | 'hands' | 'legs' | 'feet' | 'over'\n\n/** The 11 segments of the detailed body model (the Arsenal ids). */\nexport type SegmentKind = 'head' | 'jaw' | 'neck' | 'torso' | 'pelvis' | 'upperArm' | 'forearm' | 'hand' | 'thigh' | 'shin' | 'foot'\n\n/** The 6 regions of the crowd model (the game regions 0..5). */\nexport type BodyRegion = 'head' | 'torso' | 'armL' | 'armR' | 'legL' | 'legR'\n\n/** Bits of the `missing` mask of a body. The instanced body shader reads them, so the values are part of the contract. */\nexport const MISSING_BITS = {\n head: 1,\n armL: 2,\n armR: 4,\n legL: 8,\n legR: 16,\n armLBroken: 32,\n armRBroken: 64,\n legLBroken: 128,\n legRBroken: 256,\n jaw: 512,\n} as const\n","import type { DestructibleSpec, PartSpec } from './destruction'\n\nexport interface CheckSpecOptions {\n /** Known material ids. When given, every part's material must be one of them. */\n materials?: Iterable<string>\n}\n\nexport interface SpecReport {\n /** False when there is at least one problem (warnings do not count). */\n ok: boolean\n problems: string[]\n /** Things that are legal but probably wrong, e.g. structural parts that cannot reach an anchored part. */\n warnings: string[]\n}\n\nconst STRUCTURE_ROLES = ['foundation', 'column', 'beam', 'slab', 'wall', 'partition', 'roof', 'stair', 'none']\nconst BEARS = [true, false, 'a_on_b', 'b_on_a']\n\nconst finite = (n: unknown): n is number => typeof n === 'number' && Number.isFinite(n)\nconst positive = (n: unknown): boolean => finite(n) && n > 0\nconst vec3 = (v: unknown): boolean => Array.isArray(v) && v.length === 3 && v.every(finite)\nconst nonNegative = (n: unknown): boolean => n === undefined || (finite(n) && n >= 0)\n\nconst FACES = ['both', '+', '-']\nconst BONDS = ['running', 'stack']\n\n/** Layers, reinforcement, units and breach: known materials, finite positive sizes. */\nconst checkStagedFields = (p: PartSpec | undefined, label: string, materials: Set<string> | null, problems: string[]): void => {\n const known = (m: unknown, what: string): void => {\n if (materials && !materials.has(m as string)) problems.push(`${label} ${what} has unknown material \"${String(m)}\"`)\n }\n ;(p?.layers ?? []).forEach((l, i) => {\n known(l?.material, `layer ${i}`)\n if (!positive(l?.thickness)) problems.push(`${label} layer ${i} thickness must be finite and positive`)\n if (!FACES.includes(l?.faces)) problems.push(`${label} layer ${i} has an invalid faces \"${String(l?.faces)}\"`)\n })\n const r = p?.reinforcement\n if (r) {\n known(r.material, 'reinforcement')\n if (!positive(r.spacing) || !positive(r.diameter)) problems.push(`${label} reinforcement spacing and diameter must be finite and positive`)\n if (r.residualCapacity !== undefined && !(finite(r.residualCapacity) && r.residualCapacity >= 0 && r.residualCapacity <= 1)) problems.push(`${label} reinforcement residualCapacity must be between 0 and 1`)\n }\n const u = p?.units\n if (u) {\n known(u.mortar, 'units mortar')\n if (!vec3(u.size) || !u.size.every(positive)) problems.push(`${label} units size must be three finite positive numbers`)\n if (!BONDS.includes(u.bond)) problems.push(`${label} units has an invalid bond \"${String(u.bond)}\"`)\n }\n if (p?.breach && p.breach.area !== undefined && !positive(p.breach.area)) problems.push(`${label} breach area must be finite and positive`)\n}\n\n/**\n * Static checks on a `DestructibleSpec` (no world needed), for items that give a `spec` instead of an `impl`: unique part ids, known materials\n * (when `options.materials` is given), joints that reference existing, different parts, finite positive sizes, non-negative mass / hp / capacity,\n * valid structure roles and `bears` directions. A structural part with no path of joints to an anchored part is a warning, not a problem: it\n * is allowed, it just falls.\n */\nexport const checkSpec = (spec: DestructibleSpec, options: CheckSpecOptions = {}): SpecReport => {\n const problems: string[] = []\n const warnings: string[] = []\n const materials = options.materials ? new Set(options.materials) : null\n const parts = Array.isArray(spec?.parts) ? spec.parts : []\n if (!Array.isArray(spec?.parts) || parts.length === 0) problems.push('the spec needs at least one part')\n const ids = new Set<string>()\n for (const p of parts) {\n const id = String(p?.id ?? '')\n const label = `part \"${id}\"`\n if (id === '') problems.push('a part has no id')\n else if (ids.has(id)) problems.push(`duplicate part id \"${id}\"`)\n ids.add(id)\n if (materials && !materials.has(p?.material)) problems.push(`${label} has unknown material \"${String(p?.material)}\"`)\n else if (typeof p?.material !== 'string' || p.material === '') problems.push(`${label} needs a material`)\n const s = p?.shape\n if (s?.kind === 'box') {\n if (!Array.isArray(s.size) || s.size.length !== 3 || !s.size.every(positive)) problems.push(`${label} box size must be three finite positive numbers`)\n } else if (s?.kind === 'cylinder') {\n if (!positive(s.radius) || !positive(s.height)) problems.push(`${label} cylinder radius and height must be finite and positive`)\n } else if (s?.kind === 'sphere') {\n if (!positive(s.radius)) problems.push(`${label} sphere radius must be finite and positive`)\n } else problems.push(`${label} has an unknown shape`)\n if (!vec3(p?.position)) problems.push(`${label} needs a finite position [x, y, z]`)\n if (p?.rotation !== undefined && !vec3(p.rotation)) problems.push(`${label} has an invalid rotation`)\n for (const f of ['mass', 'hp', 'thickness'] as const) if (!nonNegative(p?.[f])) problems.push(`${label} ${f} must be a finite number >= 0`)\n if (p?.fill !== undefined && !(finite(p.fill) && p.fill >= 0 && p.fill <= 1)) problems.push(`${label} fill must be between 0 and 1`)\n if (p?.structure) {\n if (!STRUCTURE_ROLES.includes(p.structure.role)) problems.push(`${label} has an invalid structure role \"${String(p.structure.role)}\"`)\n if (!nonNegative(p.structure.capacity)) problems.push(`${label} structure capacity must be a finite number >= 0`)\n }\n if (p?.contains && !nonNegative(p.contains.liters)) problems.push(`${label} contains.liters must be a finite number >= 0`)\n checkStagedFields(p, label, materials, problems)\n }\n\n const jointIds = new Set<string>()\n const edges: Array<[string, string]> = []\n for (const j of spec?.joints ?? []) {\n const id = j.id ?? `${j.a}~${j.b}`\n const label = `joint \"${id}\"`\n if (jointIds.has(id)) problems.push(`duplicate joint id \"${id}\"`)\n jointIds.add(id)\n if (!ids.has(j.a)) problems.push(`${label} references unknown part \"${String(j.a)}\"`)\n if (!ids.has(j.b)) problems.push(`${label} references unknown part \"${String(j.b)}\"`)\n if (j.a === j.b) problems.push(`${label} joins part \"${String(j.a)}\" to itself`)\n if (j.bears !== undefined && !BEARS.includes(j.bears)) problems.push(`${label} has an invalid bears \"${String(j.bears)}\"`)\n if (!nonNegative(j.strength)) problems.push(`${label} strength must be a finite number >= 0`)\n if (!nonNegative(j.seconds)) problems.push(`${label} seconds must be a finite number >= 0`)\n if (j.bears !== false && ids.has(j.a) && ids.has(j.b) && j.a !== j.b) edges.push([j.a, j.b])\n }\n\n // structural parts must reach an anchored part over the joints (only when the spec lists its joints: otherwise they are inferred later)\n const structural = parts.filter((p) => p?.structure && p.structure.role !== 'none')\n if (structural.length && spec.joints) {\n const anchored = parts.filter((p) => p.anchored || p.structure?.role === 'foundation').map((p) => p.id)\n const adj = new Map<string, string[]>()\n for (const [a, b] of edges) {\n adj.set(a, [...(adj.get(a) ?? []), b])\n adj.set(b, [...(adj.get(b) ?? []), a])\n }\n const seen = new Set(anchored)\n const queue = [...anchored]\n while (queue.length) {\n for (const n of adj.get(queue.pop()!) ?? []) {\n if (seen.has(n)) continue\n seen.add(n)\n queue.push(n)\n }\n }\n const lost = structural.filter((p) => !seen.has(p.id)).map((p) => p.id)\n if (lost.length) warnings.push(`structural part(s) ${lost.map((i) => `\"${i}\"`).join(', ')} cannot reach an anchored part, so they will fall`)\n }\n return { ok: problems.length === 0, problems, warnings }\n}\n","import type { BallisticExtras, Hit, HitIgnition, Vec3 } from './destruction'\n\n/**\n * A ballistic shot (one per pellet) as a weapon module produces it. Plain data: any damage model can consume it (`shotToHit` makes the `Hit`).\n */\nexport interface Shot {\n origin: Vec3\n /** Unit vector. */\n direction: Vec3\n /** Muzzle energy in joules. */\n energy: number\n /** Bullet diameter in millimetres. */\n caliber: number\n /** snake_case calibre id, e.g. `9x19`. */\n caliberId: string\n /** Muzzle speed in m/s. */\n speed: number\n /** Projectile mass in grams (one pellet for a shotgun load). */\n mass?: number\n /** Energy at a distance in metres, with the calibre's drag. */\n energyAt(distance: number): number\n pellet: number\n /** snake_case weapon id. */\n weapon: string\n source?: string\n /** Kind of the round that was loaded (`fmj`, `hp`, `ap`, `buck`, `slug`, `incendiary`, `tracer`...). */\n ammo?: string\n /** Millimetres of steel, concrete, brick and wood the round goes through at the muzzle (the ammo table of the weapons module). `shotToHit` scales it by the energy left. */\n penetration?: Readonly<Record<string, number>>\n incendiary?: boolean\n tracer?: boolean\n /** Chances that the round lights flammable vapour or fuel. */\n ignition?: HitIgnition\n}\n\n/**\n * The fields of the round that a damage model takes besides the geometry: the penetration by material scaled by the energy that is left at `distance`\n * (linear in the energy: half the energy goes through half as much), the incendiary and tracer flags and the ignition chances. Pass it as the sixth\n * argument of `DestructionWorld.shoot` or spread it in a `Hit`.\n */\nexport const shotExtras = (shot: Shot, distance = 0): BallisticExtras => {\n const out: { -readonly [K in keyof BallisticExtras]: BallisticExtras[K] } = {}\n if (shot.penetration) {\n const k = shot.energy > 0 ? Math.min(1, Math.max(0, shot.energyAt(distance) / shot.energy)) : 1\n const scaled: Record<string, number> = {}\n for (const key of Object.keys(shot.penetration)) scaled[key] = shot.penetration[key]! * k\n out.penetration = scaled\n }\n if (shot.incendiary) out.incendiary = true\n if (shot.tracer) out.tracer = true\n if (shot.ignition) out.ignition = { spark: shot.ignition.spark, flame: shot.ignition.flame }\n return out\n}\n\n/** One use of a non-firearm (melee swing, flame tick, heat tick, throw, tool tick, extinguisher tick). */\nexport type Use =\n | { kind: 'melee'; damage: 'blunt' | 'slash'; energy: number; radius: number; reach: number; origin: Vec3; direction: Vec3 }\n | { kind: 'flame'; temperature: number; radius: number; range: number; cone: boolean; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'heat'; temperature: number; radius: number; range: number; duration: number; origin: Vec3; direction: Vec3 }\n | {\n kind: 'throw'\n energy: number\n radius: number\n fuse: number\n velocity: Vec3\n origin: Vec3\n fire?: { temperature: number; radius: number; seconds: number }\n /** Explosive id (`m67`, `molotov`, `pg7v`...): the host looks the numbers up in the arsenal. Absent on a plain thrown object. */\n explosive?: string\n /** What sets it off: `timer` after `fuse` seconds, `impact` on the first hit once armed. */\n trigger?: 'timer' | 'impact'\n /** Metres the projectile must fly before an impact sets it off; closer, it hits without exploding (rocket launchers). */\n armDistance?: number\n /** Radius in metres of the wounding area (`radius` is the lethal one). */\n injuryRadius?: number\n /** Seconds the blast stuns whoever is inside the injury radius (flashbang). */\n stunSeconds?: number\n /** Seconds the thrower held it after pulling the pin: `fuse` is what was left of the full fuse when it left the hand. */\n cooked?: number\n /** Went off in the thrower's hand (held past the fuse): no velocity, `fuse` is 0 and the blast is at `origin`. */\n inHand?: boolean\n }\n /** An explosive set down where the trigger is pulled (mine, C4, charge): it waits for its trigger instead of flying. */\n | {\n kind: 'place'\n explosive: string\n /** `wire`: something trips it; `remote`: the player fires it; `timer`: after `fuse` seconds. */\n triggers: readonly ('timer' | 'wire' | 'remote')[]\n energy: number\n /** Lethal radius in metres. */\n radius: number\n injuryRadius: number\n /** Cone the blast goes through, in front of `direction` (mine); absent for a blast in every direction. */\n cone?: { halfAngleDeg: number; length: number }\n /** The wire a `wire` charge stretches from where it is set down: metres out, degrees from its facing (counter-clockwise from above) and metres above it. */\n tripwire?: { length: number; angleDeg: number; height: number }\n fuse?: number\n origin: Vec3\n direction: Vec3\n }\n /** The blast behind a rocket launcher: dangerous inside `radius` metres, in the cone around `direction` (pointing away from the target). */\n | { kind: 'backblast'; radius: number; halfAngleDeg: number; origin: Vec3; direction: Vec3 }\n | { kind: 'tool'; tool: string; reach: number; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'extinguish'; range: number; coolC: number; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'hand'; reach: number; origin: Vec3; direction: Vec3 }\n\n/**\n * The `Hit` a ballistic shot makes at `point` (world space): the energy is what is left of the shot at that distance from its origin\n * (`shot.energyAt`), the direction and calibre come from the shot. `normal` (the surface normal at the point, when the caller knows it) does not\n * change the hit; it is accepted so that callers can pass what their raycast returned and so that a stricter implementation could use it.\n */\nexport const shotToHit = (shot: Shot, point: Vec3, normal?: Vec3): Hit => {\n void normal\n const dx = point[0] - shot.origin[0]\n const dy = point[1] - shot.origin[1]\n const dz = point[2] - shot.origin[2]\n const distance = Math.hypot(dx, dy, dz)\n return {\n type: 'ballistic',\n point: [point[0], point[1], point[2]],\n direction: [shot.direction[0], shot.direction[1], shot.direction[2]],\n energy: shot.energyAt(distance),\n caliber: shot.caliber,\n ...(shot.mass === undefined ? {} : { mass: shot.mass }),\n ...shotExtras(shot, distance),\n source: shot.source ?? shot.weapon,\n }\n}\n\n/**\n * The `Hit` a non-firearm `Use` makes at `point` (world space), or `null` for uses that do not damage by themselves.\n *\n * - `melee` -> `blunt` / `slash` with the swing's `energy`, contact `radius` and `direction`;\n * - `flame` -> `fire` and `heat` -> `heat`, with `temperature`, `duration` and a contact radius of at most 0.3 m (one ray of the cone);\n * - `throw` -> the blast (`explosive`, `energy`, `radius`, centred on `point`, the place it detonates). A thrown item with no blast energy but\n * a `fire` patch (a molotov) gives that patch as a `fire` hit (`temperature`, `radius`, `seconds` as `duration`). No energy and no fire -> `null`;\n * - `tool`, `extinguish` and `hand` -> `null` (tools work on joints, the extinguisher cools, the hand only grabs).\n *\n * `normal` does not change the hit (same as `shotToHit`).\n */\nexport const useToHit = (use: Use, point: Vec3, normal?: Vec3): Hit | null => {\n void normal\n const p: Vec3 = [point[0], point[1], point[2]]\n switch (use.kind) {\n case 'melee':\n return { type: use.damage, point: p, direction: [use.direction[0], use.direction[1], use.direction[2]], energy: use.energy, radius: use.radius }\n case 'flame':\n case 'heat':\n return {\n type: use.kind === 'flame' ? 'fire' : 'heat',\n point: p,\n direction: [use.direction[0], use.direction[1], use.direction[2]],\n temperature: use.temperature,\n radius: Math.min(use.radius, 0.3),\n duration: use.duration,\n }\n case 'throw':\n if (use.energy > 0) return { type: 'explosive', point: p, energy: use.energy, radius: use.radius }\n if (use.fire) return { type: 'fire', point: p, temperature: use.fire.temperature, radius: use.fire.radius, duration: use.fire.seconds }\n return null\n default:\n return null\n }\n}\n","import type { Hit } from './destruction'\n\n/**\n * How a container of flammable fluid (a fuel tank, a jerrycan, a gas bottle) is set alight by what hits it. A normal bullet that goes through only\n * makes a leak; the leak lights when something supplies a flame: a spark of a hard round on metal, an incendiary or tracer round, a fire nearby\n * or a blast. These are the numbers of the target side; the numbers of the round (core, jacket, incendiary, tracer) are in the weapons module\n * (`rules.ignition` of `@adatechnology/game-weapons`) and reach the target in `Hit.ignition`. Every number is a default to be changed per game.\n */\nexport interface IgnitionRules {\n /**\n * How much of the spark and of the flame of a round reaches the vapour, by how full the container is: points `[filled, factor]`, `filled` 0..1\n * (litres over capacity), `factor` 0..1, linear between points. A full container holds a mixture too rich to burn (the vapour has no air) and\n * mostly leaks; a nearly empty one holds the vapour/air mixture of a flammable range and lights the easiest; a dry one has little vapour left.\n * Default: 0.6 empty, 1 at 10 percent, 0.6 at 35, 0.3 at 65, 0.15 at 90 and 0.12 full.\n */\n fillCurve: ReadonlyArray<readonly [number, number]>\n /** 0..1: how much of the fill factor also weighs on the flame of an incendiary or tracer (1: as much as on a spark; 0: not at all). Default 0.5. */\n flameFillInfluence: number\n /** Chance of the flame of a round that is flagged `incendiary` or `tracer` but carries no `ignition` numbers (a host that passes only the flags). */\n flameDefaults: { incendiary: number; tracer: number }\n /**\n * What a spark needs to strike: a factor 0..1 by material id of the struck part. A material that is not listed but is metal takes\n * `sparkMetalDefault`; a part that is not metal never sparks. Default: steel and iron 1, aluminium 0.25.\n */\n sparkTarget: Readonly<Record<string, number>>\n sparkMetalDefault: number\n /**\n * Factor 0..1 on the spark by fluid (default 1): diesel has a flash point of 52 degrees C and more, so a spark that lights petrol vapour does\n * not light it (a flame does). Default: diesel 0.15.\n */\n sparkFluid: Readonly<Record<string, number>>\n /** A leaking container of flammable fluid within `radius` metres of a burning part or a burning fuel stain lights at `chancePerSecond`. */\n fireNear: { radius: number; chancePerSecond: number }\n}\n\n/** Defaults of `IgnitionRules`. */\nexport const DEFAULT_IGNITION_RULES: IgnitionRules = {\n fillCurve: [\n [0, 0.6],\n [0.1, 1],\n [0.35, 0.6],\n [0.65, 0.3],\n [0.9, 0.15],\n [1, 0.12],\n ],\n flameFillInfluence: 0.5,\n flameDefaults: { incendiary: 0.7, tracer: 0.2 },\n sparkTarget: { steel: 1, iron: 1, aluminium: 0.25 },\n sparkMetalDefault: 0.5,\n sparkFluid: { diesel: 0.15 },\n fireNear: { radius: 1.5, chancePerSecond: 0.5 },\n}\n\n/** Merges a partial `IgnitionRules` over the defaults (an object is merged one level deep). */\nexport const resolveIgnitionRules = (rules?: Partial<IgnitionRules>): IgnitionRules => ({\n ...DEFAULT_IGNITION_RULES,\n ...rules,\n flameDefaults: { ...DEFAULT_IGNITION_RULES.flameDefaults, ...rules?.flameDefaults },\n sparkTarget: { ...DEFAULT_IGNITION_RULES.sparkTarget, ...rules?.sparkTarget },\n sparkFluid: { ...DEFAULT_IGNITION_RULES.sparkFluid, ...rules?.sparkFluid },\n fireNear: { ...DEFAULT_IGNITION_RULES.fireNear, ...rules?.fireNear },\n})\n\n/** Linear interpolation of the fill curve at `fill` (0..1); outside the points it keeps the first or the last. */\nexport const fillFactor = (fill: number, curve: IgnitionRules['fillCurve'] = DEFAULT_IGNITION_RULES.fillCurve): number => {\n if (curve.length === 0) return 1\n const f = Math.min(1, Math.max(0, fill))\n if (f <= curve[0]![0]) return curve[0]![1]\n for (let i = 1; i < curve.length; i++) {\n const [x1, y1] = curve[i]!\n const [x0, y0] = curve[i - 1]!\n if (f <= x1) return x1 === x0 ? y1 : y0 + ((y1 - y0) * (f - x0)) / (x1 - x0)\n }\n return curve[curve.length - 1]![1]\n}\n\n/** The factor of what a spark strikes: 0 when the struck part is not metal. */\nexport const sparkTargetFactor = (materialId: string, metal: boolean, rules: IgnitionRules = DEFAULT_IGNITION_RULES): number =>\n !metal ? 0 : rules.sparkTarget[materialId] ?? rules.sparkMetalDefault\n\n/**\n * Chance that one round that went through the wall of a container lights the fluid that leaks: the spark (only on metal) and the flame (any\n * wall), each weighed by the fill of the container; the two are independent. 0 when the round carries nothing (`Hit.ignition` absent and not\n * flagged `incendiary` or `tracer`): a plain bullet on a plain wall only leaks.\n */\nexport const ignitionChance = (hit: Pick<Hit, 'incendiary' | 'tracer' | 'ignition'>, target: { sparkTarget: number; fill: number; fluid?: string }, rules: IgnitionRules = DEFAULT_IGNITION_RULES): number => {\n const k = fillFactor(target.fill, rules.fillCurve)\n const spark = (hit.ignition?.spark ?? 0) * target.sparkTarget * (target.fluid === undefined ? 1 : rules.sparkFluid[target.fluid] ?? 1) * k\n const flameBase = hit.ignition?.flame ?? Math.max(hit.incendiary ? rules.flameDefaults.incendiary : 0, hit.tracer ? rules.flameDefaults.tracer : 0)\n const flame = flameBase * (1 - rules.flameFillInfluence * (1 - k))\n return 1 - (1 - Math.min(1, Math.max(0, spark))) * (1 - Math.min(1, Math.max(0, flame)))\n}\n","import type { Vec3 } from './destruction'\n\n/** Pose of one part (or of the whole item) at a key: position offset in metres and Euler XYZ rotation in radians, both relative to the rest pose. */\nexport interface AnimationPose {\n position?: Vec3\n rotation?: Vec3\n}\n\nexport interface AnimationKey {\n /** Seconds from the start of the clip. */\n t: number\n /** Pose of the item's root. */\n position?: Vec3\n rotation?: Vec3\n /** Poses of named parts: the key is a destruction part id (a pistol `reload` moves `mag`; a car door `open` rotates `door_l`). */\n parts?: Record<string, AnimationPose>\n}\n\nexport interface AnimationEventSpec {\n /** Seconds from the start of the clip. */\n t: number\n /** snake_case type the host maps (`sound`, `eject_casing`, `door_latch`...). */\n type: string\n data?: Record<string, unknown>\n}\n\n/** A use animation a module brings with its item. Poses between keys are interpolated by the player (the test room does it linearly). */\nexport interface AnimationClipSpec {\n /** snake_case: `reload`, `open`, `drink`... Unique within the item. */\n id: string\n /** Seconds. */\n duration: number\n keys: AnimationKey[]\n events?: AnimationEventSpec[]\n}\n\n/** What an item exposes when it brings its own animations. */\nexport interface ItemAnimationContract {\n animations: AnimationClipSpec[]\n}\n\nconst finite3 = (v: unknown): boolean => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === 'number' && Number.isFinite(n))\n\n/**\n * Problems with a clip (empty when it is valid): a snake_case id, a positive finite duration, at least one key, key and event times inside\n * `[0, duration]` and not going backwards, finite vectors, every named part one of `partIds`, event types that are non-empty strings and\n * JSON-serializable event data.\n */\nexport const checkAnimation = (clip: AnimationClipSpec, partIds: Iterable<string>): { ok: boolean; problems: string[] } => {\n const problems: string[] = []\n const parts = new Set(partIds)\n const id = String(clip?.id ?? '')\n if (!/^[a-z][a-z0-9_]*$/.test(id)) problems.push(`clip id \"${id}\" must be snake_case`)\n const label = `clip \"${id}\"`\n if (!(typeof clip?.duration === 'number' && Number.isFinite(clip.duration) && clip.duration > 0)) problems.push(`${label} needs a positive finite duration`)\n const dur = typeof clip?.duration === 'number' && clip.duration > 0 ? clip.duration : Infinity\n if (!Array.isArray(clip?.keys) || clip.keys.length === 0) {\n problems.push(`${label} needs at least one key`)\n return { ok: false, problems }\n }\n let prev = -Infinity\n clip.keys.forEach((k, i) => {\n if (!(typeof k.t === 'number' && Number.isFinite(k.t)) || k.t < 0 || k.t > dur + 1e-9) problems.push(`${label} key ${i} has t=${String(k.t)} outside 0..${dur}`)\n else if (k.t < prev) problems.push(`${label} key ${i} goes back in time (${k.t} < ${prev})`)\n else prev = k.t\n for (const f of ['position', 'rotation'] as const) if (k[f] !== undefined && !finite3(k[f])) problems.push(`${label} key ${i} has an invalid ${f}`)\n for (const [pid, pose] of Object.entries(k.parts ?? {})) {\n if (!parts.has(pid)) problems.push(`${label} key ${i} moves unknown part \"${pid}\"`)\n for (const f of ['position', 'rotation'] as const) if (pose?.[f] !== undefined && !finite3(pose[f])) problems.push(`${label} key ${i} part \"${pid}\" has an invalid ${f}`)\n }\n })\n let prevEv = -Infinity\n ;(clip.events ?? []).forEach((e, i) => {\n if (!(typeof e.t === 'number' && Number.isFinite(e.t)) || e.t < 0 || e.t > dur + 1e-9) problems.push(`${label} event ${i} has t=${String(e.t)} outside 0..${dur}`)\n else if (e.t < prevEv) problems.push(`${label} event ${i} goes back in time`)\n else prevEv = e.t\n if (typeof e.type !== 'string' || e.type === '') problems.push(`${label} event ${i} needs a type`)\n try {\n JSON.stringify(e.data ?? {})\n } catch {\n problems.push(`${label} event ${i} data is not JSON`)\n }\n })\n return { ok: problems.length === 0, problems }\n}\n","import type { AnimationClipSpec, AnimationEventSpec, AnimationPose } from './animation'\nimport { checkAnimation } from './animation'\nimport type { HostEvent } from './module'\n\n/** Clips a tool can bring, one per move of its cycle. A tool brings the ones it has; a missing clip is simply not played. */\nexport const TOOL_CLIP = {\n DRAW: 'draw',\n SHOT: 'shot',\n MISFIRE: 'misfire',\n EMPTY: 'empty',\n CYCLE: 'cycle',\n RELOAD: 'reload',\n FEED: 'feed',\n FAULT: 'fault',\n CLEAR: 'clear',\n SAFETY_ON: 'safety_on',\n SAFETY_OFF: 'safety_off',\n MODE: 'mode',\n /** A throwable with the pin out, held until it is thrown. */\n COOK: 'cook',\n /** The stock folds, unfolds, slides in or out (variants `fold`, `unfold`, `collapse`, `extend`). */\n STOCK: 'stock',\n /** A light or a laser is switched (variants `on`, `off`). */\n TOGGLE: 'toggle',\n} as const\nexport type ToolClipId = (typeof TOOL_CLIP)[keyof typeof TOOL_CLIP]\n\n/** What a renderer does when a rule event arrives. `variant` picks `<clip>_<variant>` when the tool has it, else `<clip>`. */\nexport type ToolAnimationStep =\n | { readonly type: 'play'; readonly clip: ToolClipId; readonly variant?: string; readonly duration?: number; readonly repeat?: number }\n /** Move the clip that is playing to the marker (a clip event `stage` with `data.stage`) the rule just reached. */\n | { readonly type: 'sync'; readonly stage: string }\n /** The rule says the phase is over: the clip jumps to its end if it is still running. */\n | { readonly type: 'finish'; readonly clip: ToolClipId }\n\nconst stockVariant = (event: Extract<HostEvent, { type: 'stock' }>): string => {\n const isFolding = event.kind === undefined ? false : event.kind.includes('fold')\n if (event.state === 'collapsed') return isFolding ? 'fold' : 'collapse'\n return isFolding ? 'unfold' : 'extend'\n}\n\n/** The move a host event asks of the tool in hand, or null when it has no motion. */\nexport const toolAnimationStep = (event: HostEvent): ToolAnimationStep | null => {\n switch (event.type) {\n case 'tool':\n return { type: 'play', clip: TOOL_CLIP.DRAW, ...(event.duration === undefined ? {} : { duration: event.duration }) }\n case 'shot':\n return { type: 'play', clip: TOOL_CLIP.SHOT, repeat: Math.max(1, event.rounds ?? 1) }\n case 'misfire':\n return { type: 'play', clip: TOOL_CLIP.MISFIRE }\n case 'empty':\n return { type: 'play', clip: TOOL_CLIP.EMPTY }\n case 'mode':\n return { type: 'play', clip: TOOL_CLIP.MODE, variant: event.mode }\n case 'fault':\n return { type: 'play', clip: TOOL_CLIP.FAULT, variant: event.fault }\n case 'clear':\n if (event.phase === 'end') return { type: 'finish', clip: TOOL_CLIP.CLEAR }\n return { type: 'play', clip: TOOL_CLIP.CLEAR, variant: event.fault, ...(event.duration === undefined ? {} : { duration: event.duration }) }\n case 'safety':\n return { type: 'play', clip: event.on ? TOOL_CLIP.SAFETY_ON : TOOL_CLIP.SAFETY_OFF }\n case 'cycle':\n if (event.phase === 'end') return { type: 'finish', clip: TOOL_CLIP.CYCLE }\n return { type: 'play', clip: TOOL_CLIP.CYCLE, ...(event.duration === undefined ? {} : { duration: event.duration }) }\n case 'reload':\n if (event.phase === 'end') return { type: 'finish', clip: TOOL_CLIP.RELOAD }\n return { type: 'play', clip: TOOL_CLIP.RELOAD, ...(event.kind === undefined ? {} : { variant: event.kind }), ...(event.duration === undefined ? {} : { duration: event.duration }) }\n case 'stage':\n return { type: 'sync', stage: event.stage }\n case 'feed':\n return { type: 'play', clip: TOOL_CLIP.FEED }\n case 'cook':\n if (event.phase === 'start') return { type: 'play', clip: TOOL_CLIP.COOK, duration: event.fuse }\n return { type: 'finish', clip: TOOL_CLIP.COOK }\n case 'stock':\n return { type: 'play', clip: TOOL_CLIP.STOCK, variant: stockVariant(event), ...(event.duration === undefined ? {} : { duration: event.duration }) }\n case 'toggle':\n return { type: 'play', clip: TOOL_CLIP.TOGGLE, variant: event.on ? 'on' : 'off' }\n default:\n return null\n }\n}\n\n/** Where a renderer puts the poses: every part of the tool by id (the ids the clips move), and the whole tool. Poses are offsets from rest. */\nexport interface ToolRig {\n setPartPose(partId: string, pose: AnimationPose): void\n setRootPose?(pose: AnimationPose): void\n}\n\n/** Clips by id, with the variants as `<clip>_<variant>`. */\nexport type ToolClipSet = Readonly<Record<string, AnimationClipSpec>>\n\n/** A renderer's tool animator: feed it the host events and the frame time. */\nexport interface ToolAnimator {\n handle(event: HostEvent): void\n update(dt: number): void\n /** Ids of the clips playing now. */\n readonly playing: readonly string[]\n dispose(): void\n}\n\nexport const STAGE_EVENT = 'stage'\n\n/** The clip with its times (keys and events) scaled so that it lasts `duration` seconds. */\nexport const scaleClip = (clip: AnimationClipSpec, duration: number): AnimationClipSpec => {\n const factor = clip.duration > 0 ? duration / clip.duration : 1\n return {\n ...clip,\n duration,\n keys: clip.keys.map((key) => ({ ...key, t: key.t * factor })),\n ...(clip.events ? { events: clip.events.map((event) => ({ ...event, t: event.t * factor })) } : {}),\n }\n}\n\nconst ZERO: [number, number, number] = [0, 0, 0]\nconst mix = (from: readonly number[], to: readonly number[], fraction: number): [number, number, number] => [\n (from[0] ?? 0) + ((to[0] ?? 0) - (from[0] ?? 0)) * fraction,\n (from[1] ?? 0) + ((to[1] ?? 0) - (from[1] ?? 0)) * fraction,\n (from[2] ?? 0) + ((to[2] ?? 0) - (from[2] ?? 0)) * fraction,\n]\n\n/** Pose of the whole tool (`partId` undefined) or of one part at `time`, linear between keys; a key that does not name it means rest. */\nexport const sampleClip = (clip: AnimationClipSpec, partId: string | undefined, time: number): { position: [number, number, number]; rotation: [number, number, number] } => {\n const points = clip.keys.map((key) => {\n const pose = partId === undefined ? key : key.parts?.[partId]\n return { t: key.t, position: pose?.position ?? ZERO, rotation: pose?.rotation ?? ZERO }\n })\n const first = points[0]\n if (!first || first.t > 0) points.unshift({ t: 0, position: ZERO, rotation: ZERO })\n const last = points[points.length - 1]\n if (!last) return { position: ZERO, rotation: ZERO }\n if (time >= last.t) return { position: mix(last.position, last.position, 0), rotation: mix(last.rotation, last.rotation, 0) }\n const index = Math.max(0, points.findIndex((point, i) => time >= point.t && time < (points[i + 1]?.t ?? Infinity)))\n const from = points[index]\n const to = points[index + 1]\n if (!from || !to) return { position: ZERO, rotation: ZERO }\n const fraction = to.t > from.t ? (time - from.t) / (to.t - from.t) : 0\n return { position: mix(from.position, to.position, fraction), rotation: mix(from.rotation, to.rotation, fraction) }\n}\n\n/** Problems with a clip set (empty when valid): each key is the clip id (or `<clip>_<variant>`), every clip passes `checkAnimation`, and a `stage` event names its stage. */\nexport const checkToolClips = (clips: ToolClipSet, partIds: Iterable<string>): { ok: boolean; problems: string[] } => {\n const parts = [...partIds]\n const problems: string[] = []\n for (const [key, clip] of Object.entries(clips)) {\n if (clip.id !== key) problems.push(`clip \"${key}\" has the id \"${clip.id}\"`)\n if (!(clip.duration > 0)) problems.push(`clip \"${key}\" has no duration`)\n problems.push(...checkAnimation(clip, parts).problems)\n for (const event of clip.events ?? []) if (event.type === STAGE_EVENT && typeof event.data?.stage !== 'string') problems.push(`clip \"${key}\" has a stage event without data.stage`)\n }\n return { ok: problems.length === 0, problems }\n}\n\ninterface Instance {\n readonly name: string\n readonly clip: AnimationClipSpec\n time: number\n delay: number\n fired: number\n}\n\nexport interface ToolClipPlayerOptions {\n clips: ToolClipSet\n rig: ToolRig\n /** Called for each clip event as its time passes (`eject_casing`, `drop_magazine`, `stage`...). */\n onEvent?(event: AnimationEventSpec, clip: string): void\n /** Seconds between the rounds of a `repeat` (a burst): default 0.1. */\n repeatInterval?(): number\n}\n\n/** The default player of a tool's clips: runs the steps of `toolAnimationStep` against a rig, scaled to the duration the rule gives. */\nexport interface ToolClipPlayer {\n apply(step: ToolAnimationStep): void\n update(dt: number): void\n readonly playing: readonly string[]\n /** The clips being played with the time each has reached and the clip itself (scaled to the duration the rule gave), for a renderer that has to follow what the clip does now (the hands working a part). */\n readonly running: ReadonlyArray<{ readonly name: string; readonly time: number; readonly clip: AnimationClipSpec }>\n}\n\nexport const createToolClipPlayer = (options: ToolClipPlayerOptions): ToolClipPlayer => {\n const { clips, rig, onEvent } = options\n let instances: Instance[] = []\n\n const find = (clip: string, variant?: string): { name: string; clip: AnimationClipSpec } | undefined => {\n const name = variant !== undefined && clips[`${clip}_${variant}`] ? `${clip}_${variant}` : clip\n const found = clips[name]\n return found ? { name, clip: found } : undefined\n }\n\n const render = (instance: Instance): void => {\n const time = Math.min(instance.time, instance.clip.duration)\n const root = sampleClip(instance.clip, undefined, time)\n if (instance.clip.keys.some((key) => key.position || key.rotation)) rig.setRootPose?.(root)\n const touched = new Set(instance.clip.keys.flatMap((key) => Object.keys(key.parts ?? {})))\n for (const id of touched) rig.setPartPose(id, sampleClip(instance.clip, id, time))\n }\n\n const fireEvents = (instance: Instance, upTo: number): void => {\n const events = instance.clip.events ?? []\n while (instance.fired < events.length && (events[instance.fired]?.t ?? Infinity) <= upTo + 1e-9) {\n const event = events[instance.fired]\n instance.fired += 1\n if (event) onEvent?.(event, instance.name)\n }\n }\n\n return {\n apply(step) {\n if (step.type === 'sync') {\n for (const instance of instances) {\n const marker = (instance.clip.events ?? []).find((event) => event.type === STAGE_EVENT && event.data?.stage === step.stage)\n if (marker && instance.time < marker.t) instance.time = marker.t\n }\n return\n }\n if (step.type === 'finish') {\n for (const instance of instances) {\n if (instance.name !== step.clip && !instance.name.startsWith(`${step.clip}_`)) continue\n fireEvents(instance, instance.clip.duration)\n instance.time = instance.clip.duration\n }\n return\n }\n const found = find(step.clip, step.variant)\n if (!found) return\n const clip = step.duration && step.duration > 0 ? scaleClip(found.clip, step.duration) : found.clip\n const interval = options.repeatInterval?.() ?? 0.1\n instances = instances.filter((existing) => existing.name !== found.name || existing.delay > 0)\n for (let round = 0; round < (step.repeat ?? 1); round++) {\n instances.push({ name: found.name, clip, time: 0, delay: round * interval, fired: 0 })\n }\n },\n update(dt) {\n for (const instance of instances) {\n const carried = instance.delay > 0 ? dt - instance.delay : dt\n if (instance.delay > 0) {\n instance.delay = Math.max(0, instance.delay - dt)\n if (instance.delay > 0) continue\n }\n instance.time += carried\n fireEvents(instance, instance.time)\n render(instance)\n }\n instances = instances.filter((instance) => instance.delay > 0 || instance.time < instance.clip.duration)\n },\n get playing() {\n return instances.map((instance) => instance.name)\n },\n get running() {\n return instances.filter((instance) => instance.delay <= 0).map((instance) => ({ name: instance.name, time: instance.time, clip: instance.clip }))\n },\n }\n}\n","type PlainObject = Record<string, unknown>\ntype Entry = { readonly id: string }\n\n/** Keys that would reach the prototype of the result when a patch comes from parsed JSON. */\nconst UNSAFE_KEYS: ReadonlySet<string> = new Set(['__proto__', 'constructor', 'prototype'])\n\nconst isPlainObject = (value: unknown): value is PlainObject =>\n typeof value === 'object' && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype\n\n/** Recursively optional; arrays and functions are replaced as a whole, objects are merged. */\nexport type DeepPartial<TValue> = TValue extends (...args: never[]) => unknown\n ? TValue\n : TValue extends readonly unknown[]\n ? TValue\n : TValue extends object\n ? { [TKey in keyof TValue]?: DeepPartial<TValue[TKey]> }\n : TValue\n\n/** Merges `patch` over `base`: plain objects merge field by field, arrays, functions and primitives replace. `undefined` in the patch is ignored. */\nexport const deepMerge = <TBase extends object>(base: TBase, patch: object | undefined): TBase => {\n if (!patch) return base\n const result: PlainObject = { ...(base as PlainObject) }\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined || UNSAFE_KEYS.has(key)) continue\n const current = result[key]\n result[key] = isPlainObject(current) && isPlainObject(value) ? deepMerge(current, value) : value\n }\n return result as TBase\n}\n\n/** The shipped entries with the ones from `extra` added or, when the id exists, replaced. */\nexport const mergeById = <TEntry extends Entry>(shipped: readonly TEntry[], extra: readonly TEntry[] | undefined): Map<string, TEntry> => {\n const entries = new Map<string, TEntry>()\n for (const entry of [...shipped, ...(extra ?? [])]) entries.set(entry.id, entry)\n return entries\n}\n\n/** Applies `patch` by id; a patch for an id that does not exist is ignored. */\nexport const applyPatches = <TEntry extends Entry>(entries: Map<string, TEntry>, patches: Readonly<Record<string, DeepPartial<TEntry> | Partial<TEntry>>> | undefined): void => {\n for (const [id, patch] of Object.entries(patches ?? {})) {\n const entry = entries.get(id)\n if (entry) entries.set(id, deepMerge(entry, patch))\n }\n}\n\nexport const removeIds = (entries: Map<string, unknown>, ids: readonly string[] | undefined): void => {\n for (const id of ids ?? []) entries.delete(id)\n}\n","import type { Vec3 } from './destruction'\n\n/**\n * The bench of a module: controls, layers and counters it declares as data. The host keeps the values, draws them in a generic way and hands\n * every change back through `ModuleHost.bench`. Nothing here is specific to a kind of module.\n */\nexport type ControlValue = number | boolean | string\n\nexport interface ChoiceOption {\n readonly id: string\n readonly name: string\n /** Heading the host lists the option under (`optgroup`, a grouped row of buttons). Options of a group must sit next to each other. */\n readonly group?: string\n}\n\ninterface ControlBase {\n /** snake_case, unique across the module's controls, layers and metrics. */\n readonly id: string\n /** Display name, already translated for the locale the host asked for. */\n readonly name: string\n readonly hint?: string\n readonly hidden?: boolean\n /**\n * The change (or the press, for an action) takes a noticeable time, a third of a second or more: the host shows its loading indicator while it works, with this text, or the control's name for `true`.\n * Only the host's own controls panel does it; a change made through `TestRoom.bench().set / act` runs at once.\n */\n readonly slow?: boolean | string\n}\n\nexport interface NumberControl extends ControlBase {\n readonly kind: 'number'\n readonly min: number\n readonly max: number\n readonly step?: number\n readonly unit?: string\n readonly default: number\n}\n\nexport interface BooleanControl extends ControlBase {\n readonly kind: 'boolean'\n readonly default: boolean\n}\n\nexport interface ChoiceControl extends ControlBase {\n readonly kind: 'choice'\n readonly options: readonly ChoiceOption[]\n readonly default: string\n}\n\n/** A button: no value, only the press. */\nexport interface ActionControl extends ControlBase {\n readonly kind: 'action'\n}\n\nexport type ModuleControl = NumberControl | BooleanControl | ChoiceControl | ActionControl\n\n/** Something of the module the viewer turns on or off (smoke, flash, casings). */\nexport interface ModuleLayer {\n readonly id: string\n readonly name: string\n /** On unless `false`. */\n readonly default?: boolean\n}\n\n/** Pulled: the room calls read only when it samples (~4 Hz, or on TestRoom.metrics()). */\nexport interface ModuleMetric {\n readonly id: string\n readonly name: string\n readonly unit?: string\n /** Budget: the host shows a bar and warns past it. */\n readonly max?: number\n readonly digits?: number\n read(): number | string\n}\n\nexport type BenchChange =\n | { readonly kind: 'control'; readonly id: string; readonly value: ControlValue }\n | { readonly kind: 'layer'; readonly id: string; readonly on: boolean }\n | { readonly kind: 'action'; readonly id: string }\n\nexport interface ControlPatch {\n readonly hidden?: boolean\n readonly disabled?: boolean\n readonly options?: readonly ChoiceOption[]\n readonly value?: ControlValue\n}\n\n/** What the host lends a module that declares a bench: the current values and the changes. */\nexport interface ModuleBench {\n value(id: string): ControlValue | undefined\n layer(id: string): boolean\n /** Right after attach returns, the host sends one change per control (not actions) and per layer, in declaration order. */\n onChange(listener: (change: BenchChange) => void): () => void\n /** `value` sets without emitting (no echo loops); `options` falls back to the first option if the value is no longer offered. */\n patch(id: string, patch: ControlPatch): void\n /** Move the host camera (a near / middle / far choice). */\n look(eye: Vec3, target: Vec3): void\n /** Shows a sheet on the host's panel, or takes it down with `null`. A host without a sheet area ignores it. */\n sheet?(sheet: BenchSheet | null): void\n}\n\n/**\n * A card of text the module puts on the host's panel (the sheet of an effect, of a weapon): what it is, its numbers, its steps, its colours and,\n * optionally, a strip of segments over time. Names and texts are already translated for the host's locale.\n */\nexport interface BenchSheet {\n readonly kicker?: string\n readonly title: string\n readonly body?: string\n readonly facts?: readonly { readonly label: string; readonly value: string }[]\n readonly steps?: readonly { readonly at: string; readonly text: string }[]\n readonly swatches?: readonly { readonly color: string; readonly name: string }[]\n readonly note?: string\n /** A strip of named segments (seconds), with tick marks and a playhead that follows the module metric `metric`. */\n readonly timeline?: {\n readonly segments: readonly { readonly name: string; readonly start: number; readonly end: number; readonly color: string }[]\n readonly ticks: readonly number[]\n readonly metric: string\n }\n}\n\nexport interface BenchReport {\n readonly ok: boolean\n readonly problems: readonly string[]\n}\n\nexport interface BenchDeclaration {\n readonly controls?: readonly ModuleControl[]\n readonly layers?: readonly ModuleLayer[]\n readonly metrics?: readonly ModuleMetric[]\n}\n\nconst SNAKE_CASE = /^[a-z][a-z0-9_]*$/\nconst finite = (n: unknown): n is number => typeof n === 'number' && Number.isFinite(n)\n\nfunction checkControl(control: ModuleControl, problems: string[]): void {\n const label = `control \"${control.id}\"`\n if (control.kind === 'number') {\n if (!finite(control.min) || !finite(control.max) || control.min >= control.max) problems.push(`${label} needs min < max`)\n if (control.step !== undefined && !(finite(control.step) && control.step > 0)) problems.push(`${label} needs a step > 0`)\n if (!finite(control.default) || control.default < control.min || control.default > control.max) problems.push(`${label} default ${String(control.default)} is outside ${control.min}..${control.max}`)\n return\n }\n if (control.kind !== 'choice') return\n if (control.options.length === 0) {\n problems.push(`${label} needs at least one option`)\n return\n }\n const seen = new Set<string>()\n const groups = new Set<string>()\n let previousGroup: string | undefined\n for (const option of control.options) {\n if (seen.has(option.id)) problems.push(`${label} repeats option \"${option.id}\"`)\n seen.add(option.id)\n if (option.group !== undefined && (typeof option.group !== 'string' || option.group === '')) problems.push(`${label} option \"${option.id}\" has an empty group`)\n else if (option.group !== undefined && option.group !== previousGroup && groups.has(option.group)) problems.push(`${label} group \"${option.group}\" is split: its options must be next to each other`)\n if (typeof option.group === 'string' && option.group !== '') groups.add(option.group)\n previousGroup = option.group\n }\n if (!seen.has(control.default)) problems.push(`${label} default \"${control.default}\" is not among its options`)\n}\n\n/** snake_case ids unique across controls, layers and metrics; min < max; step > 0; default in range; options non-empty, unique, default among them, each group in one run; metric max > 0. */\nexport function checkBench(declared: BenchDeclaration): BenchReport {\n const problems: string[] = []\n const ids = new Set<string>()\n const claim = (kind: string, id: string): void => {\n if (!SNAKE_CASE.test(id)) problems.push(`${kind} id \"${id}\" must be snake_case`)\n if (ids.has(id)) problems.push(`duplicate id \"${id}\" across controls, layers and metrics`)\n ids.add(id)\n }\n for (const control of declared.controls ?? []) {\n claim('control', control.id)\n checkControl(control, problems)\n }\n for (const layer of declared.layers ?? []) claim('layer', layer.id)\n for (const metric of declared.metrics ?? []) {\n claim('metric', metric.id)\n if (metric.max !== undefined && !(finite(metric.max) && metric.max > 0)) problems.push(`metric \"${metric.id}\" needs a max > 0`)\n }\n return { ok: problems.length === 0, problems }\n}\n","/** The weather, light and ground as the world is right now. Written by whoever drives the weather, read by whoever reacts to it. */\nexport interface EnvironmentState {\n /** Rain in mm/h; 0 is dry. */\n readonly rain: number\n /** Wind in m/s along x and z, and its strength (the length of that vector). */\n readonly windX: number\n readonly windZ: number\n readonly windSpeed: number\n /** Strongest gust in m/s. */\n readonly gust: number\n /** Render fog far distance in metres. */\n readonly fogFar: number\n /** How far one can see for gameplay, in metres (not the render fog). */\n readonly visibility: number\n /** 0 is full daylight, 1 is the darkest night. */\n readonly nightFactor: number\n readonly temperatureC: number\n /** Wet ground, 0..1: rises with rain and dries after it stops. */\n readonly wetness: number\n}\n\nexport type EnvironmentKey = keyof EnvironmentState\n\n/** A calm dry day (the normal world, before anything went wrong). */\nexport const DEFAULT_ENVIRONMENT: EnvironmentState = {\n rain: 0,\n windX: 0,\n windZ: 0,\n windSpeed: 0,\n gust: 0,\n fogFar: 220,\n visibility: 220,\n nightFactor: 0,\n temperatureC: 20,\n wetness: 0,\n}\n\n/** The host's environment (`ModuleHost.environment`): a store the weather module writes to and the others read. */\nexport interface EnvironmentBus {\n get(): EnvironmentState\n /** Merges `patch` into the state; listeners hear it only when a value really changed. */\n set(patch: Partial<EnvironmentState>): void\n /** Hears every change with the keys that changed. Returns the unsubscribe function. */\n on(listener: (state: EnvironmentState, changed: readonly EnvironmentKey[]) => void): () => void\n}\n\n/** A plain store that implements the bus; the test room and tests use it, a game may bring its own. */\nexport function createEnvironmentBus(initial: Partial<EnvironmentState> = {}): EnvironmentBus {\n let state: EnvironmentState = { ...DEFAULT_ENVIRONMENT, ...initial }\n const listeners = new Set<(state: EnvironmentState, changed: readonly EnvironmentKey[]) => void>()\n return {\n get: () => state,\n set(patch) {\n const changed = (Object.keys(patch) as EnvironmentKey[]).filter((key) => patch[key] !== undefined && patch[key] !== state[key])\n if (changed.length === 0) return\n const next = { ...state } as { -readonly [Key in EnvironmentKey]: number }\n for (const key of changed) next[key] = patch[key] as number\n state = next\n for (const listener of [...listeners]) listener(state, changed)\n },\n on(listener) {\n listeners.add(listener)\n return () => void listeners.delete(listener)\n },\n }\n}\n","import { applyPatches, deepMerge, mergeById, removeIds } from './registry'\nimport type { DeepPartial } from './registry'\n\n/** One category (kind) of items. Defaults and required contracts are inherited from the `extends` chain. */\nexport interface CategoryDef<TDefaults extends object = Record<string, unknown>> {\n readonly id: string\n /** Parent category id; the child inherits its defaults and its required contracts. */\n readonly extends?: string\n /** Field values every entry of this kind starts with; objects merge down the chain, arrays replace. */\n readonly defaults?: DeepPartial<TDefaults>\n /** Contract ids (`ContractChecker.id`) every entry of this kind, or of a descendant, must satisfy. */\n readonly requires?: readonly string[]\n /** An abstract category only groups: entries cannot declare it directly, only its descendants. */\n readonly abstract?: boolean\n}\n\n/** Where a checker is being run: the entry's kind and the chain of categories root first. */\nexport interface ContractContext {\n readonly kind: string\n readonly chain: readonly string[]\n}\n\n/** A named contract: returns the problems of an entry, each a short phrase like `missing leftover.despawnSeconds`. Empty means it holds. */\nexport interface ContractChecker<TEntry> {\n readonly id: string\n check(entry: TEntry, ctx: ContractContext): string[]\n}\n\nexport interface CategoryOptions {\n /** Categories added to the shipped ones; the same id replaces. */\n readonly extra?: readonly CategoryDef[]\n /** Partial changes by id, applied after `extra`. */\n readonly patches?: Readonly<Record<string, DeepPartial<CategoryDef> | Partial<CategoryDef>>>\n /** Ids taken out of the tree (their children are then reported by `check()`). */\n readonly remove?: readonly string[]\n}\n\nexport interface CategoryRegistry {\n get(id: string): CategoryDef | undefined\n has(id: string): boolean\n ids(): string[]\n /** Root first, ending at `id`. Throws on an unknown id, an unknown parent or a cycle. */\n chain(id: string): string[]\n /** `defaults` merged along the chain, root first, so the closest category wins. */\n defaults(id: string): Record<string, unknown>\n /** Union of `requires` along the chain, order kept, without duplicates. */\n requires(id: string): string[]\n /** True when `id` is `ancestorId` or descends from it. */\n isA(id: string, ancestorId: string): boolean\n /** Problems in the tree itself: duplicate ids, unknown `extends`, cycles. */\n check(): string[]\n}\n\nexport interface CategorizedOptions {\n /** Field of the entry that names its category. Default `kind`. */\n readonly kindField?: string\n /** Known gaps by entry id: problems starting with one of these texts go to `allowed`, so the list can shrink over time. */\n readonly allow?: Readonly<Record<string, readonly string[]>>\n}\n\nexport interface CategorizedReport {\n readonly ok: boolean\n readonly problems: string[]\n readonly allowed: string[]\n}\n\nexport const defineCategories = (defs: readonly CategoryDef[], options: CategoryOptions = {}): CategoryRegistry => {\n const problems: string[] = []\n const duplicates = (list: readonly CategoryDef[]): void => {\n const ids = new Set<string>()\n for (const def of list) {\n if (ids.has(def.id)) problems.push(`duplicate category \"${def.id}\"`)\n ids.add(def.id)\n }\n }\n duplicates(defs)\n duplicates(options.extra ?? [])\n\n const entries = mergeById(defs, options.extra)\n applyPatches(entries, options.patches)\n removeIds(entries, options.remove)\n\n const chain = (id: string): string[] => {\n const path: string[] = []\n let current: string | undefined = id\n while (current !== undefined) {\n const def = entries.get(current)\n if (!def) throw new Error(path.length === 0 ? `unknown category \"${current}\"` : `category \"${path[0]}\" has unknown parent \"${current}\"`)\n if (path.includes(current)) throw new Error(`category cycle: ${[...path, current].reverse().join(' -> ')}`)\n path.push(current)\n current = def.extends\n }\n return path.reverse()\n }\n\n const registry: CategoryRegistry = {\n get: (id) => entries.get(id),\n has: (id) => entries.has(id),\n ids: () => [...entries.keys()],\n chain,\n defaults: (id) => chain(id).reduce<Record<string, unknown>>((acc, ancestor) => deepMerge(acc, entries.get(ancestor)?.defaults), {}),\n requires: (id) => {\n const out: string[] = []\n for (const ancestor of chain(id)) for (const contract of entries.get(ancestor)?.requires ?? []) if (!out.includes(contract)) out.push(contract)\n return out\n },\n isA: (id, ancestorId) => {\n try {\n return chain(id).includes(ancestorId)\n } catch {\n return false\n }\n },\n check: () => {\n const found = [...problems]\n for (const [id, def] of entries) {\n if (def.extends !== undefined && !entries.has(def.extends)) found.push(`category \"${id}\" extends unknown \"${def.extends}\"`)\n else {\n try {\n chain(id)\n } catch (error) {\n const message = (error as Error).message\n // The same cycle is found from each of its members; the dedupe keeps one line per distinct message.\n if (message.startsWith('category cycle') && !found.includes(message)) found.push(message)\n }\n }\n }\n return found\n },\n }\n return registry\n}\n\n/** The entry with the defaults of its category underneath: the entry wins, objects merge, arrays replace. */\nexport const resolveEntry = <TEntry extends object>(registry: CategoryRegistry, entry: TEntry, kindField = 'kind'): TEntry =>\n deepMerge(registry.defaults(String((entry as Record<string, unknown>)[kindField])) as TEntry, entry)\n\n/** Checks that each entry has a known, concrete kind and satisfies every contract its chain requires. */\nexport const checkCategorized = <TEntry extends { readonly id: string }>(\n entries: readonly TEntry[],\n registry: CategoryRegistry,\n checkers: readonly ContractChecker<TEntry>[],\n options: CategorizedOptions = {},\n): CategorizedReport => {\n const kindField = options.kindField ?? 'kind'\n const byId = new Map(checkers.map((checker) => [checker.id, checker]))\n const problems: string[] = [...registry.check()]\n const allowed: string[] = []\n\n for (const entry of entries) {\n const kind = String((entry as Record<string, unknown>)[kindField])\n if (!registry.has(kind)) {\n problems.push(`\"${entry.id}\" has unknown kind \"${kind}\"`)\n continue\n }\n if (registry.get(kind)?.abstract) {\n problems.push(`\"${entry.id}\" has abstract kind \"${kind}\"`)\n continue\n }\n let path: string[]\n try {\n path = registry.chain(kind)\n } catch (error) {\n problems.push(`\"${entry.id}\" (${kind}): ${(error as Error).message}`)\n continue\n }\n const resolved = resolveEntry(registry, entry, kindField)\n for (const contract of registry.requires(kind)) {\n const owner = path.find((ancestor) => registry.get(ancestor)?.requires?.includes(contract)) ?? kind\n const checker = byId.get(contract)\n if (!checker) {\n problems.push(`kind \"${kind}\" requires contract \"${contract}\" but no checker is registered`)\n continue\n }\n const prefix = `\"${entry.id}\" (${kind}): `\n for (const problem of checker.check(resolved, { kind, chain: path })) {\n const text = `${prefix}${problem}, required by ${owner}`\n if ((options.allow?.[entry.id] ?? []).some((start) => problem.startsWith(start))) allowed.push(text)\n else problems.push(text)\n }\n }\n }\n return { ok: problems.length === 0, problems: [...new Set(problems)], allowed }\n}\n","/**\n * Where a drawn object comes from. Every builder of the kit that draws a piece (a car of the fleet, a house or a wall, a piece of furniture, a person, an\n * animal, a plant, an item, a weapon) marks the root it returns, and every renderer that draws for the room (effects, debris, liquids, the room itself)\n * marks its own roots, so a host can tell a piece of the kit from something drawn on the spot. Duck-typed on three.js objects (`userData`, `parent`,\n * `children`), so the contracts stay free of three.js.\n */\nexport const KIT_ORIGIN_KEY = 'kitOrigin'\n\n/** What drew an object: the package and the kind of piece (`vehicle`, `structure`, `furniture`, `person`... or `effect`, `room` for what is not a piece). */\nexport interface KitOrigin {\n readonly owner: string\n readonly kind: string\n /** The catalogue id of the piece, when it has one. */\n readonly id?: string\n /** Drawn as a simple stand-in shape because the kit has no model of the piece yet (an item with no team model): a model still to make. */\n readonly standIn?: boolean\n}\n\ninterface Node {\n userData?: Record<string, unknown>\n parent?: Node | null\n children?: readonly Node[]\n name?: string\n visible?: boolean\n isMesh?: boolean\n isPoints?: boolean\n isLine?: boolean\n isSprite?: boolean\n}\n\n/** Marks `object` as drawn by `origin.owner` and returns it. */\nexport function markKitOrigin<T>(object: T, origin: KitOrigin): T {\n const node = object as unknown as Node\n node.userData = node.userData ?? {}\n node.userData[KIT_ORIGIN_KEY] = { ...origin }\n return object\n}\n\n/** The origin of the object or of its nearest marked ancestor; null when nothing up the tree is marked. */\nexport function kitOriginOf(object: unknown): KitOrigin | null {\n for (let node = object as Node | null | undefined; node; node = node.parent) {\n const origin = node.userData?.[KIT_ORIGIN_KEY] as KitOrigin | undefined\n if (origin && typeof origin.owner === 'string') return origin\n }\n return null\n}\n\n/** A drawn thing that no builder of the kit marked: what was drawn on the spot. */\nexport interface UnmarkedDrawing {\n /** The names of the object and its named ancestors, outermost first. */\n readonly path: string\n readonly object: unknown\n}\n\n/** Every visible mesh, line, point cloud or sprite under `root` with no marked ancestor (`root` itself not counted as one). */\nexport function unmarkedDrawings(root: unknown): UnmarkedDrawing[] {\n const out: UnmarkedDrawing[] = []\n const pathOf = (node: Node): string => {\n const names: string[] = []\n for (let n: Node | null | undefined = node; n && n !== root; n = n.parent) if (n.name) names.push(n.name)\n return names.reverse().join(' > ') || '(no name)'\n }\n const walk = (node: Node, marked: boolean): void => {\n if (node.visible === false) return\n const here = marked || (node !== root && Boolean((node.userData?.[KIT_ORIGIN_KEY] as KitOrigin | undefined)?.owner))\n if (!here && (node.isMesh || node.isPoints || node.isLine || node.isSprite)) out.push({ path: pathOf(node), object: node })\n for (const child of node.children ?? []) walk(child, here)\n }\n walk(root as Node, false)\n return out\n}\n","/** A floor rectangle in an object's own space, metres (x across, z along). */\nexport interface Footprint {\n readonly minX: number\n readonly maxX: number\n readonly minZ: number\n readonly maxZ: number\n}\n\n/** What `footprintBlocker` reads of an object: its world matrix (three.js `Object3D`, duck-typed so the contracts stay free of three.js). */\nexport interface FootprintObject {\n readonly matrixWorld: { readonly elements: ArrayLike<number> }\n readonly parent?: unknown\n updateWorldMatrix?(updateParents: boolean, updateChildren: boolean): void\n}\n\n/**\n * The `ModuleHost.addBlocker` function of a solid object standing on the floor: the walking player stays out of `footprint`, a rectangle in the object's own space,\n * so the object blocks turned with it and follows it when it moves. An object out of the scene (no parent) blocks nothing.\n */\nexport function footprintBlocker(object: FootprintObject | (() => FootprintObject | null | undefined), footprint: Footprint | (() => Footprint | null | undefined)): (x: number, z: number, radius: number) => boolean {\n return (x, z, radius) => {\n const o = typeof object === 'function' ? object() : object\n const fp = typeof footprint === 'function' ? footprint() : footprint\n if (!o || !fp || !o.parent) return false\n o.updateWorldMatrix?.(true, false)\n const e = o.matrixWorld.elements\n // the world point in the object's own floor axes: its x and z columns are at right angles, so a projection on each (over its squared length, for a scale) inverts the matrix\n const dx = x - (e[12] as number)\n const dz = z - (e[14] as number)\n const ax = (e[0] as number) ** 2 + (e[1] as number) ** 2 + (e[2] as number) ** 2\n const az = (e[8] as number) ** 2 + (e[9] as number) ** 2 + (e[10] as number) ** 2\n const lx = (dx * (e[0] as number) + dz * (e[2] as number)) / (ax || 1)\n const lz = (dx * (e[8] as number) + dz * (e[10] as number)) / (az || 1)\n return lx > fp.minX - radius && lx < fp.maxX + radius && lz > fp.minZ - radius && lz < fp.maxZ + radius\n }\n}\n","import type { WeatherLayerId } from '../renderer'\n\n/** The names of the visual layers in the bench; any other locale falls back to English. */\nexport type WeatherVisualText = { readonly layers: Readonly<Record<Exclude<WeatherLayerId, 'fog'>, string>> }\n\nconst TEXTS: Readonly<Record<string, WeatherVisualText>> = {\n en: { layers: { far_rain: 'Rain curtain', near_rain: 'Near rain and splashes', mist: 'Mist and ash', sky: 'Sky', lightning: 'Lightning', headlights: 'Headlights', haze: 'Heat haze' } },\n 'pt-BR': { layers: { far_rain: 'Cortina de chuva', near_rain: 'Chuva perto e respingos', mist: 'Névoa e cinza', sky: 'Céu', lightning: 'Relâmpago', headlights: 'Faróis', haze: 'Névoa de calor' } },\n}\n\nexport const weatherVisualTextOf = (locale: string): WeatherVisualText => TEXTS[locale] ?? TEXTS[locale.split('-')[0] ?? 'en'] ?? TEXTS.en!\n","import { footprintBlocker } from '@adatechnology/game-contracts'\nimport type { Footprint, ModuleHost, ModuleLayer } from '@adatechnology/game-contracts'\nimport type { VfxWorld } from '@adatechnology/game-vfx'\nimport type { WeatherVisual, WeatherVisualContext, WeatherVisualFactory } from '@adatechnology/game-weather'\nimport type * as THREE_NS from 'three'\nimport { HEADLIGHTS } from '../headlights'\nimport { createWeatherRenderer } from '../renderer'\nimport type { WeatherLayerId, WeatherRenderer, WeatherRendererOptions } from '../renderer'\nimport type { ThreeLib } from '../types'\nimport { weatherVisualTextOf } from './text'\n\nexport type WeatherVisualOptions = Pick<WeatherRendererOptions, 'profile' | 'random' | 'zone' | 'puddles' | 'puddleAt' | 'headlights'> & {\n /** The effects world of the room (`room.vfx`); without it the near rain, mist, ash, white flash and lit drops are left out. */\n readonly vfx?: VfxWorld\n /** Where the lamp rig stands (the two headlights of the Workshop's car, to see the beams); `false` leaves it out. Default 2.5 m ahead of the test room camera, lamps toward it. */\n readonly rig?: readonly [x: number, z: number, turn: number] | false\n /**\n * The car the lamps are mounted on, built by the vehicles SDK for the host's three.js (`createVehiclesThree` of game-vehicles-three with the fleet installed), so the rig follows the fleet when it\n * changes. Without it the rig has only the lamps and their beams: no car drawn here.\n */\n readonly vehicles?: (THREE: ThreeLib) => RigVehicles\n /** Fleet id of the rig's car (default `midsize_sedan`). */\n readonly rigVehicle?: string\n /**\n * The rig's car as an item of the host's catalogue (`sedan` of the vehicles test module): the host puts it in its world (`ModuleHost.addObject`), so shots, blows, blasts and fire damage it, it\n * blocks the player and its doors open, and the lamps go on its nose. Wins over `vehicles` when the host can place it; until a loaded module has the item the rig has only the lamps.\n */\n readonly rigItem?: string\n /** Which layers start on (all do by default); the room that is not about weather starts the sky and the fog off to keep its own look. */\n readonly layers?: Partial<Record<WeatherLayerId, boolean>>\n}\n\n/** What the rig needs of the vehicles SDK (`VehiclesThree` of game-vehicles-three): a car in the game frame (front on +z, wheels on y = 0) and its length. */\nexport interface RigVehicles {\n build(key: string, options?: { quality?: 'high' | 'lite' }): { readonly group: THREE_NS.Object3D; readonly length: number; readonly width?: number; dispose(): void }\n}\n\nconst RIG_VEHICLE = 'midsize_sedan'\nconst LAYER_ORDER: readonly Exclude<WeatherLayerId, 'fog'>[] = ['far_rain', 'near_rain', 'mist', 'sky', 'lightning', 'headlights', 'haze']\nconst RIG_DEFAULT = [1.6, -2.5, 0] as const\n\n/**\n * The lamps of the Workshop's car and, when the vehicles SDK is lent, a car of the fleet under them, its nose just ahead of the lamps: what the headlights are attached to when the viewer has no\n * car in the room.\n */\nfunction buildRig(THREE: ThreeLib, vehicles: RigVehicles | undefined, key: string): { readonly group: THREE_NS.Group; readonly footprint?: Footprint; dispose(): void } {\n const group = new THREE.Group()\n group.name = 'weather-lamp-rig'\n const car = vehicles?.build(key, { quality: 'lite' })\n if (car) {\n car.group.position.z = HEADLIGHTS.lamp[2] + 0.1 - car.length / 2\n group.add(car.group)\n }\n // the car is solid: the walking player stops at its body (its length and width, measured by the vehicles SDK) instead of walking through it\n const footprint = car ? footprintOf(car) : undefined\n return {\n group,\n ...(footprint ? { footprint } : {}),\n dispose() {\n car?.dispose()\n },\n }\n}\n\n/**\n * The picture of the weather for the weather bench: `createWeatherTestModule({ visual: createWeatherVisual({ vfx: room.vfx }) })`. It draws the weather the bench shows with\n * `createWeatherRenderer` on the room's scene and camera, switches each part with the bench layers and puts a lamp rig in front of the camera for the headlights.\n */\nexport function createWeatherVisual(options: WeatherVisualOptions = {}): WeatherVisualFactory {\n return {\n layers(locale): readonly ModuleLayer[] {\n const text = weatherVisualTextOf(locale)\n return LAYER_ORDER.map((id) => ({ id, name: text.layers[id], default: options.layers?.[id] ?? true }))\n },\n create({ host, weather }: WeatherVisualContext): WeatherVisual {\n const THREE = host.THREE as ThreeLib\n const renderer: WeatherRenderer = createWeatherRenderer(THREE, {\n scene: host.scene as THREE_NS.Scene,\n camera: host.camera as THREE_NS.Camera,\n weather,\n ...(options.vfx ? { vfx: options.vfx } : {}),\n ...(options.profile ? { profile: options.profile } : {}),\n ...(options.random ? { random: options.random } : {}),\n ...(options.zone ? { zone: options.zone } : {}),\n ...(options.puddles ? { puddles: options.puddles } : {}),\n ...(options.puddleAt ? { puddleAt: options.puddleAt } : {}),\n ...(options.headlights ? { headlights: options.headlights } : {}),\n ...(options.layers ? { layers: options.layers } : {}),\n })\n const item = options.rigItem && host.addObject ? options.rigItem : undefined\n return attachRig(THREE, host, renderer, options.rig === undefined ? RIG_DEFAULT : options.rig, item ? undefined : options.vehicles?.(THREE), options.rigVehicle ?? RIG_VEHICLE, item)\n },\n }\n}\n\nfunction footprintOf(car: { readonly group: THREE_NS.Object3D; readonly length: number; readonly width?: number }): Footprint | undefined {\n if (!car.width) return undefined\n const z = car.group.position.z\n return { minX: -car.width / 2, maxX: car.width / 2, minZ: z - car.length / 2, maxZ: z + car.length / 2 }\n}\n\n/** Length (m, along its own z) of what an object draws, measured in its own space. */\nfunction lengthOf(THREE: ThreeLib, root: THREE_NS.Object3D): number {\n root.updateMatrixWorld(true)\n const inverse = new THREE.Matrix4().copy(root.matrixWorld).invert()\n const box = new THREE.Box3()\n const piece = new THREE.Box3()\n const matrix = new THREE.Matrix4()\n root.traverse((mesh) => {\n const m = mesh as THREE_NS.Mesh\n if (!m.isMesh || !m.geometry) return\n if (!m.geometry.boundingBox) m.geometry.computeBoundingBox()\n piece.copy(m.geometry.boundingBox!).applyMatrix4(matrix.multiplyMatrices(inverse, m.matrixWorld))\n box.union(piece)\n })\n return box.isEmpty() ? 0 : box.max.z - box.min.z\n}\n\nfunction attachRig(\n THREE: ThreeLib,\n host: ModuleHost,\n renderer: WeatherRenderer,\n place: readonly [number, number, number] | false,\n vehicles: RigVehicles | undefined,\n key: string,\n item?: string,\n): WeatherVisual {\n const scene = host.scene as THREE_NS.Scene\n const rig = place === false ? undefined : buildRig(THREE, vehicles, key)\n let detach: (() => void) | undefined\n let unblock: (() => void) | undefined\n if (rig && place) {\n rig.group.position.set(place[0], 0, place[1])\n rig.group.rotation.y = place[2]\n scene.add(rig.group)\n detach = renderer.headlights.attach(rig.group)\n if (rig.footprint) unblock = host.addBlocker?.(footprintBlocker(rig.group, rig.footprint))\n }\n // the car as a real object of the host: placed as soon as a loaded module has the item (the vehicles module may load after this one), centred on the rig's place, and the lamps then\n // move to its nose (the car's centre is where the rig's car stood, the lamps just behind its front)\n let car: { remove(): void } | null = null\n let stopWaiting: (() => void) | undefined\n if (item && place) {\n const tryPlace = (): void => {\n if (car || !host.addObject) return\n const placed = host.addObject(item, { position: [place[0], 0, place[1]], rotationY: place[2] })\n if (!placed) return\n car = placed\n stopWaiting?.()\n stopWaiting = undefined\n const ahead = lengthOf(THREE, placed.root as THREE_NS.Object3D) / 2 - HEADLIGHTS.lamp[2] - 0.1\n rig!.group.position.set(place[0] + Math.sin(place[2]) * ahead, 0, place[1] + Math.cos(place[2]) * ahead)\n }\n tryPlace()\n if (!car) stopWaiting = host.onFrame(tryPlace)\n }\n return {\n update: (dt) => renderer.update(dt),\n setLayer(id, on) {\n if ((WEATHER_IDS as readonly string[]).includes(id)) renderer.setLayer(id as WeatherLayerId, on)\n },\n dispose() {\n detach?.()\n unblock?.()\n stopWaiting?.()\n car?.remove()\n if (rig) {\n scene.remove(rig.group)\n rig.dispose()\n }\n renderer.dispose()\n },\n }\n}\n\nconst WEATHER_IDS: readonly WeatherLayerId[] = ['far_rain', 'near_rain', 'mist', 'fog', 'sky', 'lightning', 'headlights', 'haze']\n"],"mappings":"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","names":["camera","lens","camera","flash","screen","lerp","behavior","spawn","call","UP","FRESH_BLOOD","local","BLOOD","SPEED","FORWARD","BODY_Z","TIP","PORT","WALL_Z","FACING","CENTRE","call","UP","FALL_SECONDS","BODIES","SPLINTER_DIRECTION","at","SCRAPE_DIRECTION","CAR_ORIGIN","CAR_ROTATION","WALK_SPEED","DROPS","SLANT","SPEED","SPARK_DIRECTION","call","UP","HITS","HIT_X","HIT_Y","FRESH_BLOOD","blow","WALKERS","FACING","FORWARD","ORIGIN","puff","at","MUZZLE_FLASH","QUARTER_TURN","QUARTER_TURN","camera","camera","camera","mesh"]}