@adatechnology/game-weather 0.2.8 → 0.3.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.
- package/dist/chunk-EODWEOJG.js +1301 -0
- package/dist/chunk-EODWEOJG.js.map +1 -0
- package/dist/index.d.ts +4 -452
- package/dist/index.global.js +803 -124
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +51 -1248
- package/dist/index.js.map +1 -1
- package/dist/testing-B4pjKlOJ.d.ts +453 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +9 -0
- package/dist/testing.js.map +1 -0
- package/package.json +7 -3
package/dist/index.global.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/blend.ts","../src/daylight.ts","../src/heat.ts","../src/effects.ts","../src/rng.ts","../src/lightning.ts","../src/data/climates.json","../src/data/states.json","../src/registry.ts","../src/scheduler.ts","../src/wetness.ts","../src/weather.ts","../../game-audio/src/rng.ts","../../game-audio/src/recipe.ts","../../game-audio/src/falloff.ts","../../game-audio/src/listener.ts","../../game-audio/src/data/voice-groups.json","../../game-audio/src/voices.ts","../../game-audio/src/plan.ts","../../game-audio/src/loops.ts","../../game-audio/src/data/mix-profiles.json","../../game-audio/src/mix.ts","../../game-audio/src/bank.ts","../../game-audio/src/data/game-sfx.json","../../game-audio/src/recipes/noise-class.ts","../../game-audio/src/recipes/scene.ts","../../game-audio/src/library/crowd_rumble.json","../../game-audio/src/library/engine_saw.json","../../game-audio/src/library/horn.json","../../game-audio/src/library/radio_static.json","../../game-audio/src/library/rotor_blades.json","../../game-audio/src/library/siren.json","../../game-audio/src/library/index.ts","../../game-audio/src/webaudio/noise.ts","../../game-audio/src/webaudio/chain.ts","../../game-audio/src/webaudio/context.ts","../../game-audio/src/webaudio/nodes.ts","../../game-audio/src/webaudio/realize-loop.ts","../../game-audio/src/webaudio/engine-loop.ts","../../game-audio/src/webaudio/engine-noise.ts","../../game-audio/src/webaudio/engine-queue.ts","../../game-audio/src/webaudio/realize-game.ts","../../game-audio/src/webaudio/realize-layered.ts","../../game-audio/src/webaudio/realize-weapon.ts","../../game-audio/src/webaudio/realize.ts","../../game-audio/src/webaudio/engine.ts","../../game-audio/src/webaudio/sink.ts","../../game-audio/src/bench/locale.ts","../../game-audio/src/bench/module.ts","../src/audio/recipes.ts","../src/audio/sound.ts","../src/bridges.ts","../src/bench/locale.ts","../src/bench/module.ts"],"sourcesContent":["/**\n * @adatechnology/game-weather: pure, seeded weather. States (clear to typhoon) with the numbers of the teams' pages, climates that move between\n * them in game time (2 to 6 hours each, fronts from the west), lightning with the thunder delay, ground wetness, what the weather means for\n * sight, hearing and footsteps, the heat index and the day-night formula, plus the bridges that push it to fluids, interactables, vegetation, vfx, the environment bus and the sound (rain, wind and thunder recipes of game-audio). No three.js, no game content: renderers read it.\n */\nexport { createWeather, createTimeline, DEFAULT_START_HOUR } from './weather'\nexport type { TimelineOptions, Weather, WeatherOptions, WeatherTimeline } from './weather'\nexport { CLIMATES, WEATHER_STATES, createWeatherRegistry } from './registry'\nexport type { RegistryOptions, WeatherRegistry } from './registry'\nexport { blendValues, STATE_VALUE_KEYS, valuesOf } from './blend'\nexport { createScheduler } from './scheduler'\nexport type { Scheduler } from './scheduler'\nexport { FLASH_END_SECONDS, SPEED_OF_SOUND, createLightningStream, flashIntensity, thunderDelay } from './lightning'\nexport type { LightningStream } from './lightning'\nexport { DEFAULT_DRY_OUT_SECONDS, DEFAULT_WET_RISE_SECONDS, createWetness } from './wetness'\nexport type { Wetness, WetnessOptions } from './wetness'\nexport { DRY_VISIBILITY_METERS, NIGHT_SIGHT_LOSS, deriveEffects } from './effects'\nexport type { EffectsInput } from './effects'\nexport { heatIndexC, heatStress } from './heat'\nexport { DAY_REAL_SECONDS, DEFAULT_TIME_SCALE, GAME_DAY_SECONDS, dayFractionAt, daylight } from './daylight'\nexport type { Daylight } from './daylight'\nexport { createRng, createStream, streamSeed } from './rng'\nexport type {\n Climate,\n DeepPartial,\n LightningConfig,\n StateValueKey,\n StateValues,\n Strike,\n WeatherEffects,\n WeatherEventMap,\n WeatherSample,\n WeatherState,\n} from './types'\n\nexport { createWeatherBridges, environmentOf, DEFAULT_BRIDGE_RULES } from './bridges'\nexport type { FilmTarget, InteractablesTarget, VegetationTarget, VfxTarget, WeatherBridgeOptions, WeatherBridgeRules, WeatherBridges, WeatherTargets } from './bridges'\nexport { RAIN_HISS_GAME, RAIN_LOOP, THUNDER, WEATHER_BANK, WEATHER_LOOPS, WEATHER_SOUNDS, WIND_LOOP } from './audio/recipes'\nexport { DEFAULT_RAIN_SOUND, DEFAULT_WIND_SOUND, createWeatherSound, rainVolume, strikePosition, windVolume } from './audio/sound'\nexport type { WeatherSound, WeatherSoundOptions, WeatherSoundStyle } from './audio/sound'\n\nexport { createWeatherTestModule } from './bench/module'\nexport type { WeatherScenarioLike, WeatherTestModule, WeatherTestModuleOptions, WeatherVisual, WeatherVisualContext, WeatherVisualFactory } from './bench/module'\nexport { weatherBenchTextOf } from './bench/locale'\nexport type { WeatherBenchText } from './bench/locale'\n","import type { StateValueKey, StateValues, WeatherState } from './types'\n\nexport const STATE_VALUE_KEYS: readonly StateValueKey[] = [\n 'rain',\n 'drops',\n 'sheets',\n 'windX',\n 'windZ',\n 'gust',\n 'fogFar',\n 'fogDensity',\n 'skyScale',\n 'visibility',\n 'humidity',\n 'temperatureDeltaC',\n 'wetness',\n 'hearingScale',\n 'footstepScale',\n 'dryingScale',\n 'debrisPush',\n 'haze',\n 'mistPerSecond',\n 'ashPerSecond',\n]\n\nexport function valuesOf(state: WeatherState): StateValues {\n const values: Record<string, number> = {}\n for (const key of STATE_VALUE_KEYS) values[key] = state[key]\n return values as unknown as StateValues\n}\n\n/** Every number between two states: `progress` 0 is `from`, 1 is `to`. */\nexport function blendValues(from: StateValues, to: StateValues, progress: number): StateValues {\n const amount = Math.min(1, Math.max(0, progress))\n const blended: Record<string, number> = {}\n for (const key of STATE_VALUE_KEYS) blended[key] = from[key] + (to[key] - from[key]) * amount\n return blended as unknown as StateValues\n}\n","/** A game day lasts 20 real minutes in the slice (jogo.js L69127: `dayT + dt / 1200`). */\nexport const DAY_REAL_SECONDS = 1200\nexport const GAME_DAY_SECONDS = 86400\n/** Game seconds per real second of the 20-minute day. */\nexport const DEFAULT_TIME_SCALE = GAME_DAY_SECONDS / DAY_REAL_SECONDS\n\nconst NIGHT_SPAN = 2.2\n\nconst clamp = (value: number, low: number, high: number): number => (value < low ? low : value > high ? high : value)\n\nexport type Daylight = {\n /** Sun height 0..1 (`s` of the game). */\n readonly sun: number\n /** 0 in full daylight, 1 in the darkest night. */\n readonly nightFactor: number\n}\n\n/** jogo.js L69128-L69130: `s = clamp(sin(dayT·2π − π/2)·0.5 + 0.5, 0, 1)`, `nightF = 1 − clamp(s·2.2, 0, 1)`. `dayFraction` 0 is midnight. */\nexport function daylight(dayFraction: number): Daylight {\n const sun = clamp(Math.sin(dayFraction * Math.PI * 2 - Math.PI / 2) * 0.5 + 0.5, 0, 1)\n return { sun, nightFactor: 1 - clamp(sun * NIGHT_SPAN, 0, 1) }\n}\n\n/** The fraction of the day after `gameSeconds` from `startHour`. */\nexport function dayFractionAt(startHour: number, gameSeconds: number): number {\n const fraction = (startHour / 24 + gameSeconds / GAME_DAY_SECONDS) % 1\n return fraction < 0 ? fraction + 1 : fraction\n}\n","const HEAT_STRESS_BANDS: ReadonlyArray<readonly [indexC: number, stress: number]> = [\n [27, 0],\n [32, 1 / 3],\n [41, 2 / 3],\n [54, 1],\n]\nconst NOAA_REGRESSION_FROM_F = 80\n\nconst toFahrenheit = (celsius: number): number => (celsius * 9) / 5 + 32\nconst toCelsius = (fahrenheit: number): number => ((fahrenheit - 32) * 5) / 9\n\n/**\n * NOAA heat index in °C: the Steadman estimate averaged with the temperature, and from 80 °F up the Rothfusz regression with the low-humidity and\n * high-humidity adjustments. Built from + - * / and `sqrt` only, so it is the same on every engine.\n */\nexport function heatIndexC(temperatureC: number, relativeHumidityPercent: number): number {\n const t = toFahrenheit(temperatureC)\n const r = relativeHumidityPercent\n const steadman = 0.5 * (t + 61 + (t - 68) * 1.2 + r * 0.094)\n const simple = (steadman + t) / 2\n if (simple < NOAA_REGRESSION_FROM_F) return toCelsius(simple)\n let index =\n -42.379 +\n 2.04901523 * t +\n 10.14333127 * r -\n 0.22475541 * t * r -\n 0.00683783 * t * t -\n 0.05481717 * r * r +\n 0.00122874 * t * t * r +\n 0.00085282 * t * r * r -\n 0.00000199 * t * t * r * r\n if (r < 13 && t >= 80 && t <= 112) index -= ((13 - r) / 4) * Math.sqrt((17 - Math.abs(t - 95)) / 17)\n else if (r > 85 && t >= 80 && t <= 87) index += ((r - 85) / 10) * ((87 - t) / 5)\n return toCelsius(index)\n}\n\n/** 0..1 across the NOAA bands (caution 27, extreme caution 32, danger 41, extreme danger 54 °C), linear between them. */\nexport function heatStress(heatIndex: number): number {\n const first = HEAT_STRESS_BANDS[0]!\n const last = HEAT_STRESS_BANDS[HEAT_STRESS_BANDS.length - 1]!\n if (heatIndex <= first[0]) return first[1]\n if (heatIndex >= last[0]) return last[1]\n for (let index = 1; index < HEAT_STRESS_BANDS.length; index++) {\n const [upperC, upperStress] = HEAT_STRESS_BANDS[index]!\n const [lowerC, lowerStress] = HEAT_STRESS_BANDS[index - 1]!\n if (heatIndex <= upperC) return lowerStress + ((heatIndex - lowerC) / (upperC - lowerC)) * (upperStress - lowerStress)\n }\n return last[1]\n}\n","import { heatIndexC, heatStress } from './heat'\nimport type { StateValues, WeatherEffects } from './types'\n\n/** Gameplay visibility of a dry day (the game's dry fog far, jogo.js L69141): `sightScale` is the visibility against it. */\nexport const DRY_VISIBILITY_METERS = 220\n/** jogo.js L66872: sight is `perc[0] × (1 − 0.4 × nightF)`. */\nexport const NIGHT_SIGHT_LOSS = 0.4\n\nexport type EffectsInput = {\n readonly values: StateValues\n readonly nightFactor: number\n readonly temperatureC: number\n readonly humidityPercent: number\n readonly lightningReveal: number\n}\n\n/** What the weather means for gameplay, as numbers for whoever has eyes, ears or feet in the world (perception, footsteps, heat). */\nexport function deriveEffects(input: EffectsInput): WeatherEffects {\n const { values, nightFactor } = input\n const weatherSight = Math.min(1, Math.max(0, values.visibility / DRY_VISIBILITY_METERS))\n const heatIndex = heatIndexC(input.temperatureC, input.humidityPercent)\n return {\n visibility: values.visibility,\n sightScale: weatherSight * (1 - NIGHT_SIGHT_LOSS * nightFactor),\n hearingScale: values.hearingScale,\n footstepScale: values.footstepScale,\n heatIndexC: heatIndex,\n heatStress: heatStress(heatIndex),\n lightningReveal: input.lightningReveal,\n dryingScale: values.dryingScale,\n debrisPush: values.debrisPush,\n }\n}\n","const FNV_OFFSET = 0x811c9dc5\nconst FNV_PRIME = 0x01000193\nconst MIX = 0x9e3779b1\n\n/** Deterministic mulberry32 generator; returns floats in [0, 1). */\nexport function createRng(seed: number): () => number {\n let state = seed >>> 0\n return () => {\n state = (state + 0x6d2b79f5) >>> 0\n let mixed = Math.imul(state ^ (state >>> 15), state | 1)\n mixed ^= mixed + Math.imul(mixed ^ (mixed >>> 7), mixed | 61)\n return ((mixed ^ (mixed >>> 14)) >>> 0) / 4294967296\n }\n}\n\n/** A seed for the stream called `name`: the same seed gives each name its own sequence, so lightning never shifts the schedule. */\nexport function streamSeed(seed: number, name: string): number {\n let hash = FNV_OFFSET\n for (let index = 0; index < name.length; index++) hash = Math.imul(hash ^ name.charCodeAt(index), FNV_PRIME) >>> 0\n return Math.imul((seed >>> 0) ^ hash, MIX) >>> 0\n}\n\nexport function createStream(seed: number, name: string): () => number {\n return createRng(streamSeed(seed, name))\n}\n\nexport function drawBetween(rng: () => number, range: readonly [min: number, max: number]): number {\n return range[0] + rng() * (range[1] - range[0])\n}\n","import { createStream, drawBetween } from './rng'\nimport type { LightningConfig } from './types'\n\n/** Speed of sound in air, m/s: a physical constant, so it is not configurable. */\nexport const SPEED_OF_SOUND = 343\n\n/** Seconds a flash goes on after a strike before the scene is back to normal (Workshop W1068: `k > .4`). */\nexport const FLASH_END_SECONDS = 0.4\n\n/** Seconds from the flash to the thunder for a strike `distance` metres away. */\nexport function thunderDelay(distance: number): number {\n return distance / SPEED_OF_SOUND\n}\n\n/** The Workshop double flash (W1068): full for 0.08 s, 0.2 until 0.16 s, 0.8 until 0.26 s, then dark. `elapsed` is seconds since the strike. */\nexport function flashIntensity(elapsed: number): number {\n if (elapsed < 0 || elapsed >= FLASH_END_SECONDS) return 0\n if (elapsed < 0.08) return 1\n if (elapsed < 0.16) return 0.2\n if (elapsed < 0.26) return 0.8\n return 0\n}\n\nexport type LightningStream = {\n /** Seconds from entering a state with lightning to its first strike. */\n first(config: LightningConfig): number\n /** Seconds from a strike to the next one. */\n interval(config: LightningConfig): number\n /** Where a strike lands: metres and degrees clockwise from north. Draws the distance, then the bearing. */\n place(config: LightningConfig): { readonly distance: number; readonly bearing: number }\n}\n\n/** The lightning's own seeded stream, apart from the scheduler's: the server can send the seed and every client sees the same strikes. */\nexport function createLightningStream(seed: number): LightningStream {\n const rng = createStream(seed, 'lightning')\n return {\n first: (config) => drawBetween(rng, config.first),\n interval: (config) => drawBetween(rng, config.interval),\n place(config) {\n const distance = drawBetween(rng, config.distance)\n return { distance, bearing: rng() * 360 }\n },\n }\n}\n","[\n {\n \"id\": \"temperate\",\n \"sources\": [\"galeria-vale-cinzento:L1393 (changes every 2-6 h, arrives from the west)\", \"design:T6 3.2 kit default: mostly clear and overcast, no extremes\"],\n \"start\": \"clear\",\n \"dwellHours\": [2, 6],\n \"blendMinutes\": 10,\n \"frontDirection\": [1, 0],\n \"temperatureC\": { \"mean\": 16, \"swing\": 6 },\n \"humidityOffset\": 0,\n \"transitions\": {\n \"clear\": { \"overcast\": 4, \"windy\": 1, \"fog\": 1, \"light_rain\": 1 },\n \"overcast\": { \"clear\": 3, \"light_rain\": 3, \"heavy_rain\": 1, \"fog\": 1, \"windy\": 1 },\n \"light_rain\": { \"overcast\": 3, \"clear\": 2, \"heavy_rain\": 2 },\n \"heavy_rain\": { \"light_rain\": 3, \"overcast\": 2, \"storm\": 1 },\n \"storm\": { \"heavy_rain\": 3, \"light_rain\": 2, \"overcast\": 1 },\n \"fog\": { \"clear\": 3, \"overcast\": 2 },\n \"windy\": { \"clear\": 3, \"overcast\": 3, \"light_rain\": 1 }\n }\n },\n {\n \"id\": \"tropical\",\n \"sources\": [\"galeria-vale-cinzento:L1393 (changes every 2-6 h, arrives from the west)\", \"design:T6 3.2 and Q3: extremes rare and enabled\"],\n \"start\": \"clear\",\n \"dwellHours\": [2, 6],\n \"blendMinutes\": 10,\n \"frontDirection\": [1, 0],\n \"temperatureC\": { \"mean\": 29, \"swing\": 3 },\n \"humidityOffset\": 0.1,\n \"transitions\": {\n \"clear\": { \"overcast\": 3, \"light_rain\": 2, \"windy\": 1, \"extreme_heat\": 0.8 },\n \"overcast\": { \"clear\": 3, \"light_rain\": 3, \"heavy_rain\": 2, \"fog\": 0.5 },\n \"light_rain\": { \"overcast\": 3, \"clear\": 2, \"heavy_rain\": 2 },\n \"heavy_rain\": { \"light_rain\": 3, \"overcast\": 2, \"storm\": 1.5 },\n \"storm\": { \"heavy_rain\": 3, \"light_rain\": 2, \"overcast\": 1, \"cyclone\": 0.5 },\n \"fog\": { \"clear\": 3, \"overcast\": 2 },\n \"windy\": { \"clear\": 3, \"overcast\": 2, \"light_rain\": 1, \"storm\": 0.5 },\n \"extreme_heat\": { \"clear\": 4, \"overcast\": 2, \"storm\": 0.8 },\n \"cyclone\": { \"storm\": 3, \"heavy_rain\": 2, \"typhoon\": 0.5 },\n \"typhoon\": { \"cyclone\": 3, \"storm\": 2 }\n }\n }\n]\n","[\n {\n \"id\": \"clear\",\n \"sources\": [\"jogo:L69141 (fog far 220 dry)\", \"showroom.js:L1693-1710 (breeze 3.5, 1.5 m/s)\"],\n \"rain\": 0, \"drops\": 0, \"sheets\": 0,\n \"windX\": 3.5, \"windZ\": 1.5, \"gust\": 5,\n \"fogFar\": 220, \"fogDensity\": 0, \"skyScale\": 1, \"visibility\": 220,\n \"humidity\": 0.5, \"temperatureDeltaC\": 0, \"wetness\": 0,\n \"hearingScale\": 1, \"footstepScale\": 1, \"dryingScale\": 1, \"debrisPush\": 0,\n \"haze\": 0, \"mistPerSecond\": 0, \"ashPerSecond\": 0\n },\n {\n \"id\": \"overcast\",\n \"sources\": [\"cena-de-arte:L413 (wind .35, .12)\", \"cena-de-arte:L36 (FogExp2 .03)\", \"design:T6 sky x0.9\"],\n \"rain\": 0, \"drops\": 0, \"sheets\": 0,\n \"windX\": 0.35, \"windZ\": 0.12, \"gust\": 0.6,\n \"fogFar\": 220, \"fogDensity\": 0.03, \"skyScale\": 0.9, \"visibility\": 200,\n \"humidity\": 0.65, \"temperatureDeltaC\": -1, \"wetness\": 0,\n \"hearingScale\": 1, \"footstepScale\": 1, \"dryingScale\": 1, \"debrisPush\": 0,\n \"haze\": 0, \"mistPerSecond\": 0, \"ashPerSecond\": 0\n },\n {\n \"id\": \"light_rain\",\n \"sources\": [\"oficina-vfx:W1065 (Garoa: 900 drops, slant .3)\", \"jogo:L70416-70427 (rain level .035)\", \"design:T6 2.5 mm/h, wetness .35, visibility 160, hearing x0.8 (Q4)\"],\n \"rain\": 2.5, \"drops\": 900, \"sheets\": 0,\n \"windX\": 0.3, \"windZ\": 0, \"gust\": 0.6,\n \"fogFar\": 180, \"fogDensity\": 0, \"skyScale\": 0.9, \"visibility\": 160,\n \"humidity\": 0.85, \"temperatureDeltaC\": -2, \"wetness\": 0.35,\n \"hearingScale\": 0.8, \"footstepScale\": 0.85, \"dryingScale\": 1, \"debrisPush\": 0,\n \"haze\": 0, \"mistPerSecond\": 0, \"ashPerSecond\": 0\n },\n {\n \"id\": \"heavy_rain\",\n \"sources\": [\"oficina-vfx:W1065 (Chuva: 2200 drops, slant 1.2)\", \"cena-de-arte:L1314 (rain gain .06)\", \"jogo:L69141 (fog far 140, sky x0.8)\", \"galeria-vale-cinzento:L1393 (vision 120 m, quieter steps)\", \"design:T6 15 mm/h, hearing x0.6 (Q4)\"],\n \"rain\": 15, \"drops\": 2200, \"sheets\": 0,\n \"windX\": 1.2, \"windZ\": 0, \"gust\": 2,\n \"fogFar\": 140, \"fogDensity\": 0, \"skyScale\": 0.8, \"visibility\": 120,\n \"humidity\": 0.9, \"temperatureDeltaC\": -4, \"wetness\": 0.8,\n \"hearingScale\": 0.6, \"footstepScale\": 0.7, \"dryingScale\": 1, \"debrisPush\": 0,\n \"haze\": 0, \"mistPerSecond\": 0, \"ashPerSecond\": 0\n },\n {\n \"id\": \"storm\",\n \"sources\": [\"oficina-vfx:W1065-1069 (Temporal: 4000 drops, slant 4.5, lightning 3.5-6 s, first at 2-4 s, sheets)\", \"showroom.js (60 mm/h)\", \"design:T6 hearing x0.5 (Q4)\"],\n \"rain\": 60, \"drops\": 4000, \"sheets\": 1,\n \"windX\": 4.5, \"windZ\": 0, \"gust\": 6,\n \"fogFar\": 110, \"fogDensity\": 0, \"skyScale\": 0.7, \"visibility\": 100,\n \"humidity\": 0.92, \"temperatureDeltaC\": -5, \"wetness\": 1,\n \"hearingScale\": 0.5, \"footstepScale\": 0.6, \"dryingScale\": 1, \"debrisPush\": 0,\n \"haze\": 0, \"mistPerSecond\": 0, \"ashPerSecond\": 0,\n \"lightning\": { \"interval\": [3.5, 6], \"first\": [2, 4], \"distance\": [200, 4000], \"reveal\": 0.2 }\n },\n {\n \"id\": \"fog\",\n \"sources\": [\"oficina-vfx:W1073-1080 (density .075, ground mist 5/s, ash 14/s)\", \"jogo:L69101 (zone fog far 60)\"],\n \"rain\": 0, \"drops\": 0, \"sheets\": 0,\n \"windX\": 0.3, \"windZ\": 0.1, \"gust\": 0.5,\n \"fogFar\": 60, \"fogDensity\": 0.075, \"skyScale\": 0.85, \"visibility\": 40,\n \"humidity\": 0.95, \"temperatureDeltaC\": -3, \"wetness\": 0.15,\n \"hearingScale\": 1, \"footstepScale\": 1, \"dryingScale\": 1, \"debrisPush\": 0,\n \"haze\": 0, \"mistPerSecond\": 5, \"ashPerSecond\": 14\n },\n {\n \"id\": \"windy\",\n \"sources\": [\"cena-de-arte:L1315 (wind loop)\", \"design:T6 Q3 8 m/s, gust 12\"],\n \"rain\": 0, \"drops\": 0, \"sheets\": 0,\n \"windX\": 8, \"windZ\": 0, \"gust\": 12,\n \"fogFar\": 220, \"fogDensity\": 0, \"skyScale\": 1, \"visibility\": 220,\n \"humidity\": 0.45, \"temperatureDeltaC\": -1, \"wetness\": 0,\n \"hearingScale\": 0.85, \"footstepScale\": 0.9, \"dryingScale\": 1, \"debrisPush\": 0,\n \"haze\": 0, \"mistPerSecond\": 0, \"ashPerSecond\": 0\n },\n {\n \"id\": \"cyclone\",\n \"sources\": [\"design:T6 Q3 physics preset, no team source: tropical cyclone 33 m/s sustained, gust x1.3, storm rain, debris push\"],\n \"rain\": 60, \"drops\": 4000, \"sheets\": 1,\n \"windX\": 33, \"windZ\": 0, \"gust\": 42.9,\n \"fogFar\": 80, \"fogDensity\": 0, \"skyScale\": 0.6, \"visibility\": 60,\n \"humidity\": 0.9, \"temperatureDeltaC\": 0, \"wetness\": 1,\n \"hearingScale\": 0.3, \"footstepScale\": 0.5, \"dryingScale\": 1, \"debrisPush\": 8,\n \"haze\": 0, \"mistPerSecond\": 0, \"ashPerSecond\": 0,\n \"lightning\": { \"interval\": [3.5, 6], \"first\": [2, 4], \"distance\": [200, 4000], \"reveal\": 0.2 }\n },\n {\n \"id\": \"typhoon\",\n \"sources\": [\"design:T6 Q3 physics preset, no team source: typhoon 50 m/s sustained, gust x1.3, storm rain, debris push\"],\n \"rain\": 60, \"drops\": 4000, \"sheets\": 1,\n \"windX\": 50, \"windZ\": 0, \"gust\": 65,\n \"fogFar\": 60, \"fogDensity\": 0, \"skyScale\": 0.5, \"visibility\": 40,\n \"humidity\": 0.9, \"temperatureDeltaC\": 0, \"wetness\": 1,\n \"hearingScale\": 0.25, \"footstepScale\": 0.4, \"dryingScale\": 1, \"debrisPush\": 18,\n \"haze\": 0, \"mistPerSecond\": 0, \"ashPerSecond\": 0,\n \"lightning\": { \"interval\": [3.5, 6], \"first\": [2, 4], \"distance\": [200, 4000], \"reveal\": 0.2 }\n },\n {\n \"id\": \"extreme_heat\",\n \"sources\": [\"design:T6 Q3 no team source: 38 C at the tropical noon (+6 over the climate), 60 % humidity, haze, drying x2\"],\n \"rain\": 0, \"drops\": 0, \"sheets\": 0,\n \"windX\": 0.8, \"windZ\": 0.3, \"gust\": 1.5,\n \"fogFar\": 220, \"fogDensity\": 0, \"skyScale\": 1, \"visibility\": 200,\n \"humidity\": 0.6, \"temperatureDeltaC\": 6, \"wetness\": 0,\n \"hearingScale\": 1, \"footstepScale\": 1, \"dryingScale\": 2, \"debrisPush\": 0,\n \"haze\": 1, \"mistPerSecond\": 0, \"ashPerSecond\": 0\n }\n]\n","import climatesJson from './data/climates.json'\nimport statesJson from './data/states.json'\nimport type { Climate, DeepPartial, WeatherState } from './types'\n\nexport const WEATHER_STATES: readonly WeatherState[] = statesJson as unknown as readonly WeatherState[]\nexport const CLIMATES: readonly Climate[] = climatesJson as unknown as readonly Climate[]\n\nexport type RegistryOptions = {\n /** Added to the shipped states or, when the id exists, replacing it. */\n readonly states?: readonly WeatherState[]\n readonly climates?: readonly Climate[]\n /** Merged into a shipped or added entry by id; a patch for an id that does not exist is ignored. */\n readonly patch?: {\n readonly states?: Readonly<Record<string, DeepPartial<WeatherState>>>\n readonly climates?: Readonly<Record<string, DeepPartial<Climate>>>\n }\n readonly remove?: { readonly states?: readonly string[]; readonly climates?: readonly string[] }\n}\n\nexport type WeatherRegistry = {\n state(id: string): WeatherState | undefined\n climate(id: string): Climate | undefined\n stateIds(): string[]\n climateIds(): string[]\n registerState(state: WeatherState): void\n registerClimate(climate: Climate): void\n patchState(id: string, patch: DeepPartial<WeatherState>): void\n patchClimate(id: string, patch: DeepPartial<Climate>): void\n removeState(id: string): void\n removeClimate(id: string): void\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction mergeRecords(base: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...base }\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined) continue\n const current = merged[key]\n merged[key] = isPlainObject(current) && isPlainObject(value) ? mergeRecords(current, value) : value\n }\n return merged\n}\n\nfunction deepMerge<TValue>(base: TValue, patch: DeepPartial<TValue>): TValue {\n return mergeRecords(base as Record<string, unknown>, patch as Record<string, unknown>) as TValue\n}\n\ntype Entry = { readonly id: string }\n\nfunction createTable<TEntry extends Entry>(shipped: readonly TEntry[]): Map<string, TEntry> {\n return new Map(shipped.map((entry) => [entry.id, entry]))\n}\n\nfunction patchEntry<TEntry extends Entry>(table: Map<string, TEntry>, id: string, patch: DeepPartial<TEntry>): void {\n const entry = table.get(id)\n if (entry) table.set(id, deepMerge(entry, patch))\n}\n\n/** The shipped states and climates with yours added, patched or removed; change it later with the same verbs. */\nexport function createWeatherRegistry(options: RegistryOptions = {}): WeatherRegistry {\n const states = createTable(WEATHER_STATES)\n const climates = createTable(CLIMATES)\n for (const state of options.states ?? []) states.set(state.id, state)\n for (const climate of options.climates ?? []) climates.set(climate.id, climate)\n for (const [id, patch] of Object.entries(options.patch?.states ?? {})) patchEntry(states, id, patch)\n for (const [id, patch] of Object.entries(options.patch?.climates ?? {})) patchEntry(climates, id, patch)\n for (const id of options.remove?.states ?? []) states.delete(id)\n for (const id of options.remove?.climates ?? []) climates.delete(id)\n return {\n state: (id) => states.get(id),\n climate: (id) => climates.get(id),\n stateIds: () => [...states.keys()],\n climateIds: () => [...climates.keys()],\n registerState: (state) => void states.set(state.id, state),\n registerClimate: (climate) => void climates.set(climate.id, climate),\n patchState: (id, patch) => patchEntry(states, id, patch),\n patchClimate: (id, patch) => patchEntry(climates, id, patch),\n removeState: (id) => void states.delete(id),\n removeClimate: (id) => void climates.delete(id),\n }\n}\n","import { createStream, drawBetween } from './rng'\nimport type { Climate } from './types'\n\nconst SECONDS_PER_HOUR = 3600\n/** A state lasts at least this long, so a climate patched to a zero dwell cannot stall the clock. */\nconst MIN_DWELL_SECONDS = 1\n\nexport type Scheduler = {\n /** Game seconds at which the current state ends. */\n nextChangeAt(): number\n /** Picks the state after `current`, then draws how long it lasts from `at`. Returns the new state's id. */\n next(current: string, at: number): string\n /** Draws a new duration from `at` (after a forced change). */\n restart(at: number): void\n}\n\n/**\n * A seeded Markov chain in game time. Draw order, fixed so the timeline repeats: at start one dwell; at each change the next state first\n * (one draw, over the weights in the climate's order), then its dwell. States `isKnown` rejects are left out of the weights.\n */\nexport function createScheduler(climate: Climate, seed: number, isKnown: (id: string) => boolean): Scheduler {\n const rng = createStream(seed, 'schedule')\n let changeAt = 0\n const drawDwell = (from: number): number => from + Math.max(MIN_DWELL_SECONDS, drawBetween(rng, climate.dwellHours) * SECONDS_PER_HOUR)\n changeAt = drawDwell(0)\n return {\n nextChangeAt: () => changeAt,\n next(current, at) {\n const options = Object.entries(climate.transitions[current] ?? {}).filter(([id, weight]) => weight > 0 && isKnown(id))\n const total = options.reduce((sum, [, weight]) => sum + weight, 0)\n let chosen = current\n if (total > 0) {\n let pick = rng() * total\n chosen = options[options.length - 1]![0]\n for (const [id, weight] of options) {\n if (pick < weight) {\n chosen = id\n break\n }\n pick -= weight\n }\n }\n changeAt = drawDwell(at)\n return chosen\n },\n restart(at) {\n changeAt = drawDwell(at)\n },\n }\n}\n","export const DEFAULT_WET_RISE_SECONDS = 120\n/** The Workshop sheet: the water goes back to the colour of the ground 3 minutes after the rain stops. */\nexport const DEFAULT_DRY_OUT_SECONDS = 180\n\nexport type WetnessOptions = {\n /** Seconds of rain to wet the ground fully. */\n readonly riseSeconds?: number\n /** Seconds from the end of the rain until the ground is dry. */\n readonly dryOutSeconds?: number\n}\n\nexport type Wetness = {\n value(): number\n /** Moves `seconds` toward `target` (the wetness the weather asks for); `dryingScale` speeds the dry-out up. Returns the new value. */\n update(seconds: number, target: number, dryingScale?: number): number\n reset(value?: number): void\n}\n\n/** Ground wetness: rises while the weather asks for more, then falls in a straight line that ends `dryOutSeconds` after the start of the fall. */\nexport function createWetness(options: WetnessOptions = {}): Wetness {\n const riseSeconds = options.riseSeconds ?? DEFAULT_WET_RISE_SECONDS\n const dryOutSeconds = options.dryOutSeconds ?? DEFAULT_DRY_OUT_SECONDS\n let current = 0\n let peak = 0\n let isDrying = false\n return {\n value: () => current,\n update(seconds, target, dryingScale = 1) {\n if (target > current) {\n isDrying = false\n current = Math.min(target, current + seconds / riseSeconds)\n } else if (target < current) {\n if (!isDrying) {\n isDrying = true\n peak = current\n }\n current = Math.max(target, current - (peak * seconds * dryingScale) / dryOutSeconds)\n } else isDrying = false\n return current\n },\n reset(value = 0) {\n current = value\n peak = value\n isDrying = false\n },\n }\n}\n","import { blendValues, valuesOf } from './blend'\nimport { DEFAULT_TIME_SCALE, daylight, dayFractionAt } from './daylight'\nimport { deriveEffects } from './effects'\nimport { createLightningStream, flashIntensity, thunderDelay } from './lightning'\nimport { createWeatherRegistry } from './registry'\nimport type { RegistryOptions, WeatherRegistry } from './registry'\nimport { createScheduler } from './scheduler'\nimport type { Climate, StateValues, Strike, WeatherEventMap, WeatherSample, WeatherState } from './types'\nimport { createWetness } from './wetness'\nimport type { WetnessOptions } from './wetness'\n\nexport const DEFAULT_START_HOUR = 8\nconst SECONDS_PER_MINUTE = 60\nconst WETNESS_STEP_SECONDS = 1\n\nexport type WeatherOptions = RegistryOptions & {\n /** Seed of the schedule and of the lightning (each has its own stream). Default 1. */\n readonly seed?: number\n /** Climate id. Default `temperate`. */\n readonly climate?: string\n /** First state; default the climate's `start`, which is `clear`: the normal world comes first. */\n readonly start?: string\n /** Game seconds per real second. Default 72 (the 20-minute day). */\n readonly timeScale?: number\n /** Hour of the day the world starts at. Default 8. */\n readonly startHour?: number\n /** `false` keeps the state until `force` changes it. Default true. */\n readonly autoSchedule?: boolean\n readonly wetness?: WetnessOptions\n /** An existing registry, instead of building one from the registry options. */\n readonly registry?: WeatherRegistry\n}\n\nexport type WeatherTimeline = {\n readonly seed: number\n readonly climate: string\n readonly start: string\n readonly hours: number\n readonly sampleMinutes: number\n readonly transitions: ReadonlyArray<{ readonly gameSeconds: number; readonly from: string; readonly to: string }>\n readonly strikes: readonly Strike[]\n readonly samples: ReadonlyArray<Readonly<Record<string, number | string>>>\n}\n\nexport type Weather = {\n readonly registry: WeatherRegistry\n /** Moves the world forward by `dt` real seconds (game time moves `timeScale` times as fast). Events fire in order as they are crossed. */\n advance(dt: number): void\n sample(): WeatherSample\n /** The state being blended to, or settled in. */\n state(): string\n /** Changes state now, blending over `blendSeconds` real seconds (0 is at once); the new state then lasts a normal dwell. */\n force(id: string, blendSeconds?: number): void\n setTimeScale(timeScale: number): void\n setAutoSchedule(isOn: boolean): void\n /** What a fresh world with these options does over `hours` game hours; the live world is not touched. */\n timeline(hours: number, sampleMinutes?: number): WeatherTimeline\n on<TEvent extends keyof WeatherEventMap>(event: TEvent, listener: (payload: WeatherEventMap[TEvent]) => void): () => void\n}\n\ntype Blend = { startedAt: number; duration: number }\n\nconst clamp01 = (value: number): number => Math.min(1, Math.max(0, value))\n\nfunction requireClimate(registry: WeatherRegistry, id: string): Climate {\n const climate = registry.climate(id)\n if (!climate) throw new Error(`game-weather: unknown climate \"${id}\"`)\n return climate\n}\n\nfunction requireState(registry: WeatherRegistry, id: string): WeatherState {\n const state = registry.state(id)\n if (!state) throw new Error(`game-weather: unknown state \"${id}\"`)\n return state\n}\n\nexport function createWeather(options: WeatherOptions = {}): Weather {\n const registry = options.registry ?? createWeatherRegistry(options)\n const seed = options.seed ?? 1\n const climateId = options.climate ?? 'temperate'\n const startClimate = requireClimate(registry, climateId)\n const startState = requireState(registry, options.start ?? startClimate.start)\n const startHour = options.startHour ?? DEFAULT_START_HOUR\n const scheduler = createScheduler(startClimate, seed, (id) => registry.state(id) !== undefined)\n const lightning = createLightningStream(seed)\n const wetness = createWetness(options.wetness)\n const listeners = new Map<keyof WeatherEventMap, Set<(payload: never) => void>>()\n\n let timeScale = options.timeScale ?? DEFAULT_TIME_SCALE\n let isAutoSchedule = options.autoSchedule ?? true\n let gameSeconds = 0\n let simSeconds = 0\n let target = startState\n let targetValues = valuesOf(startState)\n let fromValues: StateValues = targetValues\n let previousId: string | undefined\n let blend: Blend = { startedAt: 0, duration: 0 }\n let nextStrikeAt: number | undefined\n let strikeCount = 0\n let lastStrike: Strike | undefined\n const pendingThunder: Strike[] = []\n\n const climate = (): Climate => registry.climate(climateId) ?? startClimate\n\n const emit = <TEvent extends keyof WeatherEventMap>(event: TEvent, payload: WeatherEventMap[TEvent]): void => {\n for (const listener of [...(listeners.get(event) ?? [])]) (listener as (value: WeatherEventMap[TEvent]) => void)(payload)\n }\n\n const progressAt = (at: number): number => (blend.duration <= 0 ? 1 : clamp01((at - blend.startedAt) / blend.duration))\n const valuesAt = (at: number): StateValues => blendValues(fromValues, targetValues, progressAt(at))\n\n const scheduleFirstStrike = (atSim: number): void => {\n nextStrikeAt = target.lightning ? atSim + lightning.first(target.lightning) : undefined\n }\n\n const enter = (id: string, atGame: number, atSim: number, blendGameSeconds: number): void => {\n const next = requireState(registry, id)\n fromValues = valuesAt(atGame)\n previousId = target.id\n target = next\n targetValues = valuesOf(next)\n blend = { startedAt: atGame, duration: blendGameSeconds }\n scheduleFirstStrike(atSim)\n emit('transition', { from: previousId, to: id, gameSeconds: atGame })\n }\n\n const strikeAt = (atSim: number, startGame: number, startSim: number): void => {\n const config = target.lightning!\n const place = lightning.place(config)\n const strike: Strike = {\n id: strikeCount++,\n time: atSim,\n gameSeconds: startGame + (atSim - startSim) * timeScale,\n distance: place.distance,\n bearing: place.bearing,\n thunderDelay: thunderDelay(place.distance),\n }\n lastStrike = strike\n pendingThunder.push(strike)\n pendingThunder.sort((a, b) => a.time + a.thunderDelay - (b.time + b.thunderDelay))\n nextStrikeAt = atSim + lightning.interval(config)\n emit('strike', strike)\n }\n\n scheduleFirstStrike(0)\n\n const runEvents = (startGame: number, startSim: number, endSim: number): void => {\n for (;;) {\n const changeSim = isAutoSchedule ? Math.max(startSim, startSim + (scheduler.nextChangeAt() - startGame) / timeScale) : Infinity\n const strikeSim = nextStrikeAt ?? Infinity\n const thunder = pendingThunder[0]\n const thunderSim = thunder ? thunder.time + thunder.thunderDelay : Infinity\n const earliest = Math.min(changeSim, strikeSim, thunderSim)\n if (earliest > endSim) return\n if (earliest === changeSim) {\n const atGame = Math.max(startGame, scheduler.nextChangeAt())\n const next = scheduler.next(target.id, atGame)\n enter(next, atGame, changeSim, climate().blendMinutes * SECONDS_PER_MINUTE)\n } else if (earliest === strikeSim) strikeAt(strikeSim, startGame, startSim)\n else emit('thunder', pendingThunder.shift()!)\n }\n }\n\n const wet = (startGame: number, endGame: number, seconds: number): void => {\n const steps = Math.max(1, Math.ceil(seconds / WETNESS_STEP_SECONDS))\n for (let step = 1; step <= steps; step++) {\n const values = valuesAt(startGame + ((endGame - startGame) * step) / steps)\n wetness.update(seconds / steps, values.wetness, values.dryingScale)\n }\n }\n\n const api: Weather = {\n registry,\n advance(dt) {\n if (!(dt > 0)) return\n const startGame = gameSeconds\n const startSim = simSeconds\n const endGame = startGame + dt * timeScale\n runEvents(startGame, startSim, startSim + dt)\n gameSeconds = endGame\n simSeconds = startSim + dt\n wet(startGame, endGame, dt)\n },\n sample() {\n const values = valuesAt(gameSeconds)\n const dayFraction = dayFractionAt(startHour, gameSeconds)\n const { sun, nightFactor } = daylight(dayFraction)\n const current = climate()\n const temperatureC = current.temperatureC.mean + current.temperatureC.swing * (2 * sun - 1) + values.temperatureDeltaC\n const humidityPercent = clamp01(values.humidity + current.humidityOffset) * 100\n const progress = progressAt(gameSeconds)\n return {\n ...values,\n gameSeconds,\n dayFraction,\n hour: dayFraction * 24,\n nightFactor,\n sun,\n state: target.id,\n previous: progress < 1 ? previousId : undefined,\n blend: progress,\n front: { direction: current.frontDirection, progress },\n windSpeed: Math.sqrt(values.windX * values.windX + values.windZ * values.windZ),\n temperatureC,\n humidityPercent,\n groundWetness: wetness.value(),\n flash: lastStrike ? flashIntensity(simSeconds - lastStrike.time) : 0,\n effects: deriveEffects({ values, nightFactor, temperatureC, humidityPercent, lightningReveal: target.lightning?.reveal ?? 0 }),\n }\n },\n state: () => target.id,\n force(id, blendSeconds = 0) {\n enter(id, gameSeconds, simSeconds, blendSeconds * timeScale)\n scheduler.restart(gameSeconds)\n },\n setTimeScale(next) {\n if (!(next > 0)) throw new Error('game-weather: timeScale must be above 0')\n timeScale = next\n },\n setAutoSchedule(isOn) {\n if (isOn && !isAutoSchedule) scheduler.restart(gameSeconds)\n isAutoSchedule = isOn\n },\n timeline: (hours, sampleMinutes) => createTimeline({ ...options, registry, hours, ...(sampleMinutes === undefined ? {} : { sampleMinutes }) }),\n on(event, listener) {\n const set = listeners.get(event) ?? new Set()\n set.add(listener as (payload: never) => void)\n listeners.set(event, set)\n return () => void set.delete(listener as (payload: never) => void)\n },\n }\n return api\n}\n\nexport type TimelineOptions = WeatherOptions & {\n readonly hours: number\n /** Game minutes between samples. Default 10. */\n readonly sampleMinutes?: number\n}\n\nconst TIMELINE_DECIMALS = 6\nconst round = (value: number): number => Math.round(value * 10 ** TIMELINE_DECIMALS) / 10 ** TIMELINE_DECIMALS + 0\n\n/**\n * What a fresh world does over `hours` game hours, stepping one game minute at a time: the state changes, the strikes (time, distance, bearing) and\n * a sample every `sampleMinutes`. Plain JSON, rounded to 6 decimals, so it can be a golden.\n */\nexport function createTimeline(options: TimelineOptions): WeatherTimeline {\n const weather = createWeather(options)\n const timeScale = options.timeScale ?? DEFAULT_TIME_SCALE\n const sampleMinutes = options.sampleMinutes ?? 10\n const transitions: Array<{ gameSeconds: number; from: string; to: string }> = []\n const strikes: Strike[] = []\n weather.on('transition', (event) => transitions.push({ gameSeconds: round(event.gameSeconds), from: event.from, to: event.to }))\n weather.on('strike', (strike) =>\n strikes.push({ id: strike.id, time: round(strike.time), gameSeconds: round(strike.gameSeconds), distance: round(strike.distance), bearing: round(strike.bearing), thunderDelay: round(strike.thunderDelay) }),\n )\n const samples: Array<Record<string, number | string>> = []\n const record = (minute: number): void => {\n const sample = weather.sample()\n samples.push({\n at: minute * SECONDS_PER_MINUTE,\n hour: round(sample.hour),\n state: sample.state,\n blend: round(sample.blend),\n rain: round(sample.rain),\n windSpeed: round(sample.windSpeed),\n gust: round(sample.gust),\n visibility: round(sample.effects.visibility),\n sightScale: round(sample.effects.sightScale),\n hearingScale: round(sample.effects.hearingScale),\n temperatureC: round(sample.temperatureC),\n heatIndexC: round(sample.effects.heatIndexC),\n heatStress: round(sample.effects.heatStress),\n wetness: round(sample.groundWetness),\n nightFactor: round(sample.nightFactor),\n })\n }\n record(0)\n const minutes = Math.round(options.hours * 60)\n for (let minute = 1; minute <= minutes; minute++) {\n weather.advance(SECONDS_PER_MINUTE / timeScale)\n if (minute % sampleMinutes === 0) record(minute)\n }\n return {\n seed: options.seed ?? 1,\n climate: options.climate ?? 'temperate',\n start: options.start ?? weather.registry.climate(options.climate ?? 'temperate')!.start,\n hours: options.hours,\n sampleMinutes,\n transitions,\n strikes,\n samples,\n }\n}\n","const FNV_OFFSET = 0x811c9dc5\nconst FNV_PRIME = 0x01000193\nconst MIX = 0x9e3779b1\n\n/** Deterministic mulberry32 generator; floats in [0, 1). The same generator the golden tool seeds `Math.random` with, so a team routine and its recipe draw the same numbers. */\nexport function createRng(seed: number): () => number {\n let state = seed >>> 0\n return () => {\n state = (state + 0x6d2b79f5) >>> 0\n let mixed = Math.imul(state ^ (state >>> 15), state | 1)\n mixed ^= mixed + Math.imul(mixed ^ (mixed >>> 7), mixed | 61)\n return ((mixed ^ (mixed >>> 14)) >>> 0) / 4294967296\n }\n}\n\n/** The seed of the stream called `name` (or numbered `name`): one seed gives each channel its own sequence, so one subject's sounds never shift another's. */\nexport function streamSeed(seed: number, name: string | number): number {\n const label = String(name)\n let hash = FNV_OFFSET\n for (let index = 0; index < label.length; index++) hash = Math.imul(hash ^ label.charCodeAt(index), FNV_PRIME) >>> 0\n return Math.imul((seed >>> 0) ^ hash, MIX) >>> 0\n}\n\nexport function streamFor(seed: number, name: string | number): () => number {\n return createRng(streamSeed(seed, name))\n}\n\n/** `min + random × (max − min)`: the form every team routine uses (`95 + Math.random() * 40`). */\nexport function drawBetween(random: () => number, min: number, max: number): number {\n return min + random() * (max - min)\n}\n","/**\n * Sound recipes are data. A recipe says which nodes a sound has and which numbers drive them; ranges are drawn when the sound is planned, in\n * the order the fields are written (see `planSound`), so a recipe ported from a team routine draws its random numbers in the routine's order.\n */\n\nexport const BUS_IDS = ['weapons', 'voices', 'world', 'ambience', 'weather', 'ui'] as const\nexport type BusId = (typeof BUS_IDS)[number]\n\n/** A named number: from `PlayOptions.params` or the recipe's `vars`, times `scale`, plus `add`. */\nexport type ParamRef = { readonly param: string; readonly scale?: number; readonly add?: number }\n\n/** A number, a `[min, max]` drawn uniformly (`min + random × (max − min)`), or a named number. */\nexport type Range = number | readonly [min: number, max: number] | ParamRef\n\n/** Seconds from the layer start: a range, the layer `'duration'`, or `'0.6d'` (0.6 × the layer duration). */\nexport type Time = Range | 'duration' | `${number}d`\n\nexport type Curve = 'linear' | 'exp'\n\n/**\n * How an AudioParam moves:\n * - `{ set }` assigns `.value`;\n * - `{ set, to, curve, over }` assigns `.value`, then ramps to `to` at `over`;\n * - `{ from }` calls `setValueAtTime(from)` at the start; with `to`, `curve` and `over` it then ramps to `to` (`'from'` is the value drawn for `from`: the art scene ramps a tone back to its own start when it gives no end).\n */\nexport type Automation =\n | { readonly set: Range; readonly to?: Range | 'from'; readonly curve?: Curve; readonly over?: Time }\n | { readonly from: Range; readonly to?: Range | 'from'; readonly curve?: Curve; readonly over?: Time }\n\n/**\n * One gain point: time (seconds, or `'0.6d'`), value and how it is reached (`set` = `setValueAtTime`, or a ramp). The value is multiplied by the\n * layer level (volume, distance and layer gain) except when the fourth item is `'fixed'` (the floor a ramp falls to, such as 0.001).\n */\nexport type EnvelopePoint =\n | readonly [time: number | `${number}d`, value: number, curve: 'set' | Curve]\n | readonly [time: number | `${number}d`, value: number, curve: 'set' | Curve, fixed: 'fixed']\nexport type Envelope = readonly EnvelopePoint[]\n\nexport type FilterSpec = {\n readonly type: BiquadFilterType\n readonly frequency: Automation\n readonly q?: Range\n /** The BiquadFilter gain (shelf and peaking types). */\n readonly gain?: Range\n /** A gain node after the filter, for a parallel bank where each band has its own level (the game's scream formants). */\n readonly level?: Range\n}\n\nexport type FilterBank = { readonly mode: 'serial' | 'parallel'; readonly bank: readonly FilterSpec[] }\n\n/** An LFO. `depth` is absolute, or a factor of the source start frequency. With `base`, gain modulation goes through its own gain stage (the game rotor). */\nexport type Modulator = {\n readonly target: 'gain' | 'frequency'\n /** Left out, the oscillator keeps its default (sine) and no type is set. */\n readonly wave?: OscillatorType\n readonly rate: Range\n readonly depth: Range | { readonly scaleOf: 'frequency'; readonly factor: number }\n readonly base?: number\n}\n\n/**\n * Noise from the engine's seeded buffer. `offset` is where playback starts in it: `'random'` (random × `offsetScale`, default 1), `'fit'`\n * (random × (buffer length − duration)) or seconds. `loop` sets the source to loop (the art scene sets it on every noise source).\n */\nexport type NoiseSource = {\n readonly kind: 'noise'\n readonly offset?: 'random' | 'fit' | number\n readonly offsetScale?: number\n readonly loop?: boolean\n}\n\nexport type OscSource = { readonly kind: 'osc'; readonly wave: OscillatorType; readonly frequency: Automation }\n\n/** The art scene's `voice`: an oscillator with vibrato through formant bands, plus a band of breath noise, summed and boosted. */\nexport type FormantVoiceSource = {\n readonly kind: 'formant_voice'\n readonly wave: OscillatorType\n readonly frequency: Automation\n /** Vibrato rate, and depth as a factor of the start frequency. */\n readonly vibrato: { readonly rate: Range; readonly depth: number }\n readonly formants: readonly { readonly frequency: Range; readonly q: number }[]\n readonly breath: { readonly frequency: Range; readonly q: number; readonly gain: Range }\n readonly mix: number\n}\n\nexport type LayerSource = NoiseSource | OscSource | FormantVoiceSource\n\nexport type Layer = {\n /** Start, seconds after the sound starts. */\n readonly at?: Range\n /** Overrides the recipe duration for this layer. */\n readonly duration?: Range\n /** Seconds the source keeps running after the duration (the `+ 0.05` of the team routines). Default: the recipe's, else 0.05. */\n readonly tail?: number\n /** The layer played `count` times, `spacing` seconds apart (the game's animal calls). */\n readonly repeat?: { readonly count: number; readonly spacing: number }\n /** Layer level before distance. Default 1. */\n readonly gain?: Range\n /** Send to the reverb, 0..1 or more (the art scene uses up to 1.2). */\n readonly wet?: number\n readonly source: LayerSource\n readonly filters?: FilterBank\n readonly modulators?: readonly Modulator[]\n readonly envelope: Envelope\n}\n\n/**\n * Distance falloff of a sound's level:\n * - `inverse_square`: `volume / (1 + d² × k)`; `cutoffK` also divides every lowpass cutoff by `1 + d × cutoffK` (the game's muffling);\n * - `linear`: `max(0, 1 − d / range)`, silent at or below `minGain`;\n * - `none`: no distance.\n * `scale` multiplies the level (the game's boom is 1.4 at the source), and below `minGain` the sound is not played.\n */\nexport type Falloff =\n | { readonly model: 'inverse_square'; readonly k: number; readonly minGain?: number; readonly cutoffK?: number; readonly scale?: number }\n | { readonly model: 'linear'; readonly range: number; readonly minGain?: number; readonly scale?: number }\n | { readonly model: 'none' }\n\n/**\n * The order the realizer creates and wires the nodes in. Each one is the order of the team routine it reproduces, so the trace of the SDK\n * matches the trace of the team code node for node:\n * - `layered` (default): the art scene's `sNoise`, `sTone` and `voice`; every layer has its own output stage (distance gain, pan, reverb send);\n * - `burst`: the game's `sfx` (jogo L62089);\n * - `scream`: the game's `grito` (L62128);\n * - `groan`: the game's `voz` (L62143);\n * - `boom`: the game's `boom` (L68278);\n * - `static`: the game's `chiado` (L69998);\n * - `chord`: the game's `buzina` (L70272);\n * - `calls`: the game's `bichoSom` (L66625);\n * - `siren`: the art scene's `siren` (L1369);\n * - `weapon`: the weapon audio of the kit (`weapons-three`): per layer a filtered noise burst or an oscillator, then its own gain, chained to the bus.\n * Every order but `layered` puts the level into the envelope and connects straight to the bus.\n */\nexport const BUILD_ORDERS = ['layered', 'burst', 'scream', 'groan', 'boom', 'static', 'chord', 'calls', 'siren', 'weapon'] as const\nexport type BuildOrder = (typeof BUILD_ORDERS)[number]\n\nexport type SoundRecipe = {\n readonly id: string\n readonly bus: BusId\n readonly order?: BuildOrder\n /** Named numbers drawn first, in the order written; layers read them with `{ param }`. */\n readonly vars?: Readonly<Record<string, Range>>\n readonly duration?: Range\n readonly tail?: number\n readonly layers: readonly Layer[]\n readonly falloff?: Falloff\n /** Stereo pan from the listener (the art scene's `panFor`). Only the `layered` order has an output stage to pan. */\n readonly pan?: boolean\n /** Voice group for the limiter (`undead_voice`...). */\n readonly group?: string\n /** Limiter priority; default from the bus (weapons > voices > world > ambience). */\n readonly priority?: number\n /**\n * The team routine creates nodes before it checks the level (the game's `bichoSom` creates its gain and filter first): a silent sound is kept as\n * a plan with `silent: true` so the realizer repeats those nodes. Off by default.\n */\n readonly keepSilent?: boolean\n /** Where the numbers come from: `\"jogo:L62128\"`, `\"cena:L1342\"`, or `\"sdk\"` for values the kit chose. */\n readonly sources: readonly string[]\n}\n\n/**\n * The order the loop realizer creates the nodes in:\n * - `game_loop`: the game's `loopSet` (L62109): gain and filter first, nodes created the first time the volume passes `startAbove`, gain from 0;\n * - `chain_loop`: the art scene's loops in `initAudio` (L1314–1316): source, filters, gain at its starting volume, created when the loop opens.\n */\nexport type LoopOrder = 'game_loop' | 'chain_loop'\n\nexport type LoopRecipe = {\n readonly id: string\n readonly bus: BusId\n readonly order?: LoopOrder\n /** An oscillator loop may leave the frequency out: it starts at the default and `set({ frequency })` glides it (the game's motor). */\n readonly source: (NoiseSource & { readonly loop: true }) | { readonly kind: 'osc'; readonly wave: OscillatorType; readonly frequency?: Automation; readonly loop: true }\n readonly filters?: FilterBank\n readonly modulators?: readonly Modulator[]\n /** `setTargetAtTime` time constants (jogo L62125–62126: 0.15 for the volume, 0.08 for the frequency). */\n readonly smoothing?: { readonly volume: number; readonly frequency: number }\n /** Nodes are created the first time the volume reaches this (jogo L62112: 0.002). */\n readonly startAbove?: number\n /** Volume the gain starts at when the loop opens (`chain_loop`). */\n readonly volume?: number\n readonly sources: readonly string[]\n}\n\nexport type SoundBank = {\n readonly id: string\n readonly sounds?: readonly SoundRecipe[]\n readonly loops?: readonly LoopRecipe[]\n}\n","import type { Falloff } from './recipe'\n\n/** The distance curves of the team routines, by name. Each recipe carries its own; these are the values to copy. */\nexport const DISTANCE_CURVES = {\n /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */\n game_sfx: { model: 'inverse_square', k: 0.004, cutoffK: 0.01, minGain: 0.01 },\n /** The game's `grito`: `1 / (1 + d² × 0.006)`, silent below 0.02 (jogo L62129–62130). */\n game_scream: { model: 'inverse_square', k: 0.006, minGain: 0.02 },\n /** The game's `voz`: `1 / (1 + d² × 0.01)`, silent below 0.02 (jogo L62146–62147). */\n game_voice: { model: 'inverse_square', k: 0.01, minGain: 0.02 },\n /** The game's `boom`: `1.4 / (1 + d² × 0.002)`, never silent (jogo L68280). */\n game_boom: { model: 'inverse_square', k: 0.002, scale: 1.4 },\n /** The art scene's `outAt`: `vol / (1 + d² × 0.008)` (cena L1320). */\n art_scene: { model: 'inverse_square', k: 0.008 },\n /** The game's animal calls: `max(0, 1 − d / 70) × 0.25`, silent at or below 0.005 (jogo L66627–66628). */\n animals: { model: 'linear', range: 70, scale: 0.25, minGain: 0.005 },\n} as const satisfies Readonly<Record<string, Falloff>>\n\nexport type DistanceCurveName = keyof typeof DISTANCE_CURVES\n\n/** The level at `distance` for a sound at `volume`, before the recipe's envelope. */\nexport function falloffGain(falloff: Falloff | undefined, volume: number, distance: number): number {\n if (!falloff || falloff.model === 'none') return volume\n if (falloff.model === 'linear') {\n const level = Math.max(0, 1 - distance / falloff.range) * volume\n return falloff.scale === undefined ? level : level * falloff.scale\n }\n const level = volume / (1 + distance * distance * falloff.k)\n return falloff.scale === undefined ? level : falloff.scale * level\n}\n\n/** Whether a level is too quiet to play: below `minGain` (the game's `att < 0.01`), or at or below it for `linear` (the animal calls' `v <= 0.005`). */\nexport function isSilent(falloff: Falloff | undefined, gain: number): boolean {\n if (!falloff || falloff.model === 'none' || falloff.minGain === undefined) return false\n return falloff.model === 'linear' ? gain <= falloff.minGain : gain < falloff.minGain\n}\n\n/** What every lowpass cutoff is divided by at `distance` (1 when the curve has no muffling). */\nexport function cutoffDivisor(falloff: Falloff | undefined, distance: number): number {\n if (!falloff || falloff.model !== 'inverse_square' || falloff.cutoffK === undefined) return 1\n return 1 + distance * falloff.cutoffK\n}\n","import type { Vec3 } from '@adatechnology/game-contracts'\n\n/** Where the ears are. `yaw` turns around y like a three.js camera: the right ear points at `(cos yaw, 0, −sin yaw)`. */\nexport type Listener = { readonly position: Vec3; readonly yaw: number }\n\nexport type DistanceMode = 'xz' | '3d'\n\nexport const DEFAULT_LISTENER: Listener = { position: [0, 0, 0], yaw: 0 }\n\n/** The art scene clamps the pan to ±0.85 and never divides by less than 2 m (cena L1319). */\nexport const PAN_LIMIT = 0.85\nexport const PAN_MIN_DISTANCE = 2\n\n/** Distance from the listener: on the ground (`xz`, the teams' choice) or in 3D. */\nexport function listenerDistance(listener: Listener, position: Vec3, mode: DistanceMode = 'xz'): number {\n const dx = position[0] - listener.position[0]\n const dz = position[2] - listener.position[2]\n if (mode === 'xz') return Math.hypot(dx, dz)\n return Math.hypot(dx, position[1] - listener.position[1], dz)\n}\n\n/** The art scene's `panFor`: the side the sound is on, −0.85 (left) to 0.85 (right). No HRTF, for cost. */\nexport function panFor(listener: Listener, position: Vec3): number {\n const rightX = Math.cos(listener.yaw)\n const rightZ = -Math.sin(listener.yaw)\n const dx = position[0] - listener.position[0]\n const dz = position[2] - listener.position[2]\n const distance = Math.hypot(dx, dz) || 1\n const pan = (dx * rightX + dz * rightZ) / Math.max(distance, PAN_MIN_DISTANCE)\n return pan < -PAN_LIMIT ? -PAN_LIMIT : pan > PAN_LIMIT ? PAN_LIMIT : pan\n}\n","{\n \"undead_voice\": {\n \"maxConcurrent\": 5,\n \"cooldown\": 0.25,\n \"maxDistance\": 40,\n \"sources\": [\n \"cena:L1342 `AU.groans > 4` refuses, so five play at once\",\n \"jogo:L66986 one groan every 0.25 s (`groanBudget`), only nearer than 40 m\",\n \"jogo:L70472 the budget counts down by the frame time\"\n ]\n }\n}\n","import voiceGroupsJson from './data/voice-groups.json'\nimport type { BusId } from './recipe'\n\n/**\n * Rules for a group of voices (`undead_voice`...). A request passes when every rule passes:\n * - `maxConcurrent`: at most this many of the group play at once (`steal` replaces the group's oldest instead of refusing);\n * - `cooldown`: seconds after an accepted voice before the next one (the game's `groanBudget`, jogo L66986 and L70472);\n * - `maxDistance`: only voices nearer than this (the game's `d < 40`).\n */\nexport type VoiceGroup = {\n readonly maxConcurrent?: number\n readonly cooldown?: number\n readonly maxDistance?: number\n readonly steal?: boolean\n readonly sources?: readonly string[]\n}\n\n/** `undead_voice`: the art scene's five at once with the game's 0.25 s cooldown and 40 m. */\nexport const VOICE_GROUPS: Readonly<Record<string, VoiceGroup>> = voiceGroupsJson\n\n/** Q7: weapons > voices > world > ambience; the interface above all, weather with the ambience. */\nexport const BUS_PRIORITY: Readonly<Record<BusId, number>> = { ui: 5, weapons: 4, voices: 3, world: 2, weather: 1, ambience: 1 }\nexport const DEFAULT_MAX_VOICES = 32\n\nexport function busPriority(bus: BusId): number {\n return BUS_PRIORITY[bus]\n}\n\nexport type VoiceRequest = {\n readonly group?: string\n readonly priority: number\n /** Planned level; the global cap drops the lowest `priority × gain`. */\n readonly gain: number\n readonly distance?: number\n /** Seconds the voice lasts. */\n readonly length: number\n}\n\nexport type VoiceRefusal = 'cooldown' | 'distance' | 'group_full' | 'global_full'\n\nexport type VoiceDecision =\n | { readonly accepted: true; readonly id: number; readonly evicted: readonly number[] }\n | { readonly accepted: false; readonly reason: VoiceRefusal }\n\nexport type VoiceStats = { readonly active: number; readonly peak: number; readonly dropped: number; readonly maxVoices: number }\n\nexport type VoiceManager = {\n request(request: VoiceRequest): VoiceDecision\n /** Ends a voice early (stopped by the host). */\n release(id: number): void\n /** Sets how long an accepted voice lasts (the engine admits a voice before its random length is drawn). */\n setLength(id: number, seconds: number): void\n /** Moves time on: cooldowns count down and finished voices leave. Returns the ids that ended. */\n advance(dt: number): number[]\n setGroup(id: string, group: VoiceGroup | undefined): void\n stats(): VoiceStats\n}\n\nexport type VoiceManagerOptions = {\n /** Default `VOICE_GROUPS`. */\n readonly groups?: Readonly<Record<string, VoiceGroup>>\n readonly maxVoices?: number\n}\n\ntype ActiveVoice = { readonly id: number; readonly group?: string; readonly score: number; remaining: number }\n\n/** The voice limiter: per-group rules, a global cap and priority dropping. Pure and time-driven (`advance`), so goldens script it frame by frame. */\nexport function createVoiceManager(options: VoiceManagerOptions = {}): VoiceManager {\n const groups = new Map(Object.entries(options.groups ?? VOICE_GROUPS))\n const maxVoices = options.maxVoices ?? DEFAULT_MAX_VOICES\n const budgets = new Map<string, number>()\n let voices: ActiveVoice[] = []\n let nextId = 1\n let peak = 0\n let dropped = 0\n\n const refuse = (reason: VoiceRefusal): VoiceDecision => {\n dropped += 1\n return { accepted: false, reason }\n }\n\n const groupCheck = (request: VoiceRequest, evicted: number[]): VoiceRefusal | undefined => {\n const group = request.group === undefined ? undefined : groups.get(request.group)\n if (!group) return undefined\n if (group.maxDistance !== undefined && !((request.distance ?? 0) < group.maxDistance)) return 'distance'\n if (group.cooldown !== undefined && (budgets.get(request.group as string) ?? 0) > 0) return 'cooldown'\n const members = voices.filter((voice) => voice.group === request.group)\n if (group.maxConcurrent === undefined || members.length < group.maxConcurrent) return undefined\n if (!group.steal) return 'group_full'\n evicted.push((members[0] as ActiveVoice).id)\n return undefined\n }\n\n return {\n request(request) {\n const evicted: number[] = []\n const refusal = groupCheck(request, evicted)\n if (refusal) return refuse(refusal)\n const score = request.priority * request.gain\n const remaining = voices.filter((voice) => !evicted.includes(voice.id))\n if (remaining.length >= maxVoices) {\n const lowest = remaining.reduce((low, voice) => (voice.score < low.score ? voice : low))\n if (score <= lowest.score) return refuse('global_full')\n evicted.push(lowest.id)\n dropped += 1\n }\n const group = request.group === undefined ? undefined : groups.get(request.group)\n if (group?.cooldown !== undefined) budgets.set(request.group as string, group.cooldown)\n const id = nextId++\n voices = [...voices.filter((voice) => !evicted.includes(voice.id)), { id, ...(request.group === undefined ? {} : { group: request.group }), score, remaining: request.length }]\n peak = Math.max(peak, voices.length)\n return { accepted: true, id, evicted }\n },\n release(id) {\n voices = voices.filter((voice) => voice.id !== id)\n },\n setLength(id, seconds) {\n const voice = voices.find((entry) => entry.id === id)\n if (voice) voice.remaining = seconds\n },\n advance(dt) {\n for (const [group, budget] of budgets) budgets.set(group, budget - dt)\n const ended: number[] = []\n for (const voice of voices) {\n voice.remaining -= dt\n if (voice.remaining <= 0) ended.push(voice.id)\n }\n if (ended.length > 0) voices = voices.filter((voice) => voice.remaining > 0)\n return ended\n },\n setGroup(id, group) {\n if (group) groups.set(id, group)\n else groups.delete(id)\n },\n stats: () => ({ active: voices.length, peak, dropped, maxVoices }),\n }\n}\n","import type { Vec3 } from '@adatechnology/game-contracts'\n\nimport { cutoffDivisor, falloffGain, isSilent } from './falloff'\nimport { DEFAULT_LISTENER, listenerDistance, panFor } from './listener'\nimport type { DistanceMode, Listener } from './listener'\nimport type { Automation, BuildOrder, BusId, Curve, Envelope, FilterSpec, Layer, LayerSource, Modulator, Range, SoundRecipe, Time } from './recipe'\nimport { createRng } from './rng'\nimport { busPriority } from './voices'\n\n/** How an AudioParam moves, with every number resolved and every time in seconds from the layer start. */\nexport type PlannedAutomation = {\n /** Assigned to `.value`. */\n readonly value?: number\n /** `setValueAtTime(from)` at the layer start. */\n readonly from?: number\n /** Ramp target, reached `at` seconds after the layer start. */\n readonly to?: number\n readonly curve?: Curve\n readonly at?: number\n}\n\nexport type PlannedFilter = {\n readonly type: BiquadFilterType\n readonly frequency: PlannedAutomation\n readonly q?: number\n readonly gain?: number\n readonly level?: number\n}\n\nexport type PlannedModulator = {\n readonly target: 'gain' | 'frequency'\n readonly wave?: OscillatorType\n readonly rate: number\n readonly depth: number\n readonly base?: number\n}\n\nexport type PlannedSource =\n | { readonly kind: 'noise'; readonly offset: number; readonly loop: boolean }\n | { readonly kind: 'osc'; readonly wave: OscillatorType; readonly frequency: PlannedAutomation }\n | {\n readonly kind: 'formant_voice'\n readonly wave: OscillatorType\n readonly frequency: PlannedAutomation\n readonly vibrato: { readonly rate: number; readonly depth: number }\n readonly formants: readonly { readonly frequency: number; readonly q: number }[]\n readonly breath: { readonly frequency: number; readonly q: number; readonly gain: number; readonly offset: number }\n readonly mix: number\n }\n\nexport type PlannedEnvelopePoint = { readonly time: number; readonly value: number; readonly curve: 'set' | Curve }\n\nexport type PlannedLayer = {\n readonly at: number\n readonly duration: number\n readonly tail: number\n /** Layer gain × the sound's level: in the envelope for every order but `layered`, in the output stage for `layered`. */\n readonly level: number\n readonly wet?: number\n readonly source: PlannedSource\n readonly filters?: { readonly mode: 'serial' | 'parallel'; readonly bank: readonly PlannedFilter[] }\n readonly modulators: readonly PlannedModulator[]\n readonly envelope: readonly PlannedEnvelopePoint[]\n}\n\n/** A sound with every number decided: what the realizer builds, what goldens compare and what the bench sheet prints. */\nexport type SoundPlan = {\n readonly id: string\n readonly bus: BusId\n readonly order: BuildOrder\n readonly group?: string\n readonly priority: number\n /** The level from volume and distance, before layer gains. */\n readonly level: number\n readonly distance: number\n readonly pan?: number\n /** Too quiet to hear, kept only for a recipe with `keepSilent`. */\n readonly silent: boolean\n /** Seconds until the last source stops. */\n readonly length: number\n readonly layers: readonly PlannedLayer[]\n}\n\nexport type PlanInput = {\n /** Where the numbers are drawn from. Default: a fresh stream with seed 1. */\n readonly random?: () => number\n readonly volume?: number\n /** Distance in meters; overrides `position`. */\n readonly distance?: number\n readonly position?: Vec3\n readonly listener?: Listener\n readonly distanceMode?: DistanceMode\n readonly params?: Readonly<Record<string, number>>\n /** 0..1 from the host's occlusion callback: gain × (1 − 0.5 × o), lowpass cutoffs × (1 − 0.6 × o). */\n readonly occlusion?: number\n /** Length of the noise buffer, for `offset: 'fit'`. Default 1.5. */\n readonly noiseSeconds?: number\n}\n\nconst DEFAULT_TAIL = 0.05\nconst DEFAULT_NOISE_SECONDS = 1.5\nconst OCCLUSION_GAIN = 0.5\nconst OCCLUSION_CUTOFF = 0.6\n\ntype Scope = { readonly random: () => number; readonly params: Record<string, number> }\n\nfunction isParamRef(range: Range): range is Extract<Range, { readonly param: string }> {\n return typeof range === 'object' && !Array.isArray(range)\n}\n\nfunction draw(range: Range, scope: Scope): number {\n if (typeof range === 'number') return range\n if (isParamRef(range)) {\n const value = scope.params[range.param]\n if (value === undefined) throw new Error(`game-audio: the recipe reads the param \"${range.param}\", which was not given`)\n return value * (range.scale ?? 1) + (range.add ?? 0)\n }\n const [min, max] = range as readonly [number, number]\n return min + scope.random() * (max - min)\n}\n\n// a time is drawn now when it is a range; 'duration' and '0.6d' wait for the layer duration\ntype PendingTime = number | 'duration' | `${number}d`\n\nfunction drawTime(time: Time, scope: Scope): PendingTime {\n if (time === 'duration' || (typeof time === 'string' && time.endsWith('d'))) return time as PendingTime\n return draw(time as Range, scope)\n}\n\nfunction timeOf(time: PendingTime, duration: number): number {\n if (typeof time === 'number') return time\n if (time === 'duration') return duration\n return Number(time.slice(0, -1)) * duration\n}\n\ntype PendingAutomation = { value?: number; from?: number; to?: number; curve?: Curve; over?: PendingTime }\n\nfunction drawAutomation(automation: Automation, scope: Scope): PendingAutomation {\n const pending: PendingAutomation = {}\n for (const key of Object.keys(automation)) {\n if (key === 'set' && 'set' in automation) pending.value = draw(automation.set, scope)\n else if (key === 'from' && 'from' in automation) pending.from = draw(automation.from, scope)\n else if (key === 'to' && automation.to !== undefined) pending.to = automation.to === 'from' ? (pending.from ?? pending.value ?? 0) : draw(automation.to, scope)\n else if (key === 'curve') pending.curve = automation.curve\n else if (key === 'over' && automation.over !== undefined) pending.over = drawTime(automation.over, scope)\n }\n return pending\n}\n\nfunction finishAutomation(pending: PendingAutomation, duration: number): PlannedAutomation {\n if (pending.from !== undefined && pending.to === undefined) return { from: pending.from }\n if (pending.from !== undefined) return { from: pending.from, to: pending.to, curve: pending.curve ?? 'linear', at: timeOf(pending.over ?? 'duration', duration) }\n if (pending.to === undefined || pending.over === undefined) return { value: pending.value }\n return { value: pending.value, to: pending.to, curve: pending.curve ?? 'linear', at: timeOf(pending.over, duration) }\n}\n\nconst startFrequency = (automation: PendingAutomation): number => automation.from ?? automation.value ?? 0\n\ntype PendingFilter = { spec: FilterSpec; frequency: PendingAutomation; q?: number; gain?: number; level?: number }\n\nfunction drawFilter(spec: FilterSpec, scope: Scope): PendingFilter {\n const pending: PendingFilter = { spec, frequency: {} }\n for (const key of Object.keys(spec)) {\n if (key === 'frequency') pending.frequency = drawAutomation(spec.frequency, scope)\n else if (key === 'q' && spec.q !== undefined) pending.q = draw(spec.q, scope)\n else if (key === 'gain' && spec.gain !== undefined) pending.gain = draw(spec.gain, scope)\n else if (key === 'level' && spec.level !== undefined) pending.level = draw(spec.level, scope)\n }\n return pending\n}\n\ntype PendingModulator = { modulator: Modulator; rate: number; depth?: number }\n\nfunction drawModulator(modulator: Modulator, scope: Scope): PendingModulator {\n const pending: PendingModulator = { modulator, rate: 0 }\n for (const key of Object.keys(modulator)) {\n if (key === 'rate') pending.rate = draw(modulator.rate, scope)\n else if (key === 'depth' && !(typeof modulator.depth === 'object' && 'scaleOf' in modulator.depth)) pending.depth = draw(modulator.depth as Range, scope)\n }\n return pending\n}\n\ntype PendingVoice = { frequency: PendingAutomation; vibratoRate: number; formants: { frequency: number; q: number }[]; breath: { frequency: number; q: number; gain: number } }\n\nfunction drawVoice(source: Extract<LayerSource, { kind: 'formant_voice' }>, scope: Scope): PendingVoice {\n const pending: PendingVoice = { frequency: {}, vibratoRate: 0, formants: [], breath: { frequency: 0, q: source.breath.q, gain: 0 } }\n for (const key of Object.keys(source)) {\n if (key === 'frequency') pending.frequency = drawAutomation(source.frequency, scope)\n else if (key === 'vibrato') pending.vibratoRate = draw(source.vibrato.rate, scope)\n else if (key === 'formants') pending.formants = source.formants.map((formant) => ({ frequency: draw(formant.frequency, scope), q: formant.q }))\n else if (key === 'breath') pending.breath = { frequency: draw(source.breath.frequency, scope), q: source.breath.q, gain: draw(source.breath.gain, scope) }\n }\n return pending\n}\n\ntype LayerDefaults = { readonly duration?: number; readonly tail?: number; readonly noiseSeconds: number }\n\ntype PendingLayer = {\n at: number\n duration?: number\n gain: number\n automation?: PendingAutomation\n voice?: PendingVoice\n filters: PendingFilter[]\n modulators: PendingModulator[]\n}\n\nfunction drawLayer(layer: Layer, scope: Scope): PendingLayer {\n const pending: PendingLayer = { at: 0, gain: 1, filters: [], modulators: [] }\n for (const key of Object.keys(layer)) {\n if (key === 'at' && layer.at !== undefined) pending.at = draw(layer.at, scope)\n else if (key === 'duration' && layer.duration !== undefined) pending.duration = draw(layer.duration, scope)\n else if (key === 'gain' && layer.gain !== undefined) pending.gain = draw(layer.gain, scope)\n else if (key === 'source' && layer.source.kind === 'osc') pending.automation = drawAutomation(layer.source.frequency, scope)\n else if (key === 'source' && layer.source.kind === 'formant_voice') pending.voice = drawVoice(layer.source, scope)\n else if (key === 'filters' && layer.filters) pending.filters = layer.filters.bank.map((spec) => drawFilter(spec, scope))\n else if (key === 'modulators' && layer.modulators) pending.modulators = layer.modulators.map((modulator) => drawModulator(modulator, scope))\n }\n return pending\n}\n\n// the team code draws the noise start point when the source starts, after every other number of the layer\nfunction drawOffset(source: LayerSource, duration: number, scope: Scope, noiseSeconds: number): number {\n if (source.kind === 'formant_voice') return scope.random()\n if (source.kind !== 'noise' || source.offset === undefined) return 0\n if (typeof source.offset === 'number') return source.offset\n if (source.offset === 'fit') return scope.random() * Math.max(0, noiseSeconds - duration)\n return scope.random() * (source.offsetScale ?? 1)\n}\n\nfunction finishSource(layer: Layer, pending: PendingLayer, duration: number, offset: number): PlannedSource {\n const source = layer.source\n if (source.kind === 'noise') return { kind: 'noise', offset, loop: source.loop === true }\n if (source.kind === 'osc') return { kind: 'osc', wave: source.wave, frequency: finishAutomation(pending.automation ?? {}, duration) }\n const voice = pending.voice as PendingVoice\n return {\n kind: 'formant_voice',\n wave: source.wave,\n frequency: finishAutomation(voice.frequency, duration),\n vibrato: { rate: voice.vibratoRate, depth: startFrequency(voice.frequency) * source.vibrato.depth },\n formants: voice.formants,\n breath: { ...voice.breath, offset },\n mix: source.mix,\n }\n}\n\nfunction finishModulator(pending: PendingModulator, sourceFrequency: number): PlannedModulator {\n const { modulator } = pending\n const depth = typeof modulator.depth === 'object' && 'scaleOf' in modulator.depth ? sourceFrequency * modulator.depth.factor : (pending.depth ?? 0)\n return { target: modulator.target, ...(modulator.wave ? { wave: modulator.wave } : {}), rate: pending.rate, depth, ...(modulator.base === undefined ? {} : { base: modulator.base }) }\n}\n\nfunction finishEnvelope(envelope: Envelope, duration: number, level: number): PlannedEnvelopePoint[] {\n return envelope.map(([time, value, curve, fixed]) => ({ time: timeOf(time, duration), value: fixed === 'fixed' ? value : value * level, curve }))\n}\n\nfunction planLayer(layer: Layer, scope: Scope, defaults: LayerDefaults, levelInEnvelope: boolean, soundLevel: number): PlannedLayer[] {\n const pending = drawLayer(layer, scope)\n const duration = pending.duration ?? defaults.duration\n if (duration === undefined) throw new Error('game-audio: a layer needs a duration (its own or the recipe’s)')\n const offset = drawOffset(layer.source, duration, scope, defaults.noiseSeconds)\n const level = pending.gain * soundLevel\n const sourceFrequency = layer.source.kind === 'noise' ? 0 : startFrequency(pending.automation ?? pending.voice?.frequency ?? {})\n const planned: PlannedLayer = {\n at: pending.at,\n duration,\n tail: layer.tail ?? defaults.tail ?? DEFAULT_TAIL,\n level,\n ...(layer.wet === undefined ? {} : { wet: layer.wet }),\n source: finishSource(layer, pending, duration, offset),\n ...(layer.filters\n ? {\n filters: {\n mode: layer.filters.mode,\n bank: pending.filters.map(({ spec, frequency, q, gain, level: filterLevel }) => ({\n type: spec.type,\n frequency: finishAutomation(frequency, duration),\n ...(q === undefined ? {} : { q }),\n ...(gain === undefined ? {} : { gain }),\n ...(filterLevel === undefined ? {} : { level: filterLevel }),\n })),\n },\n }\n : {}),\n modulators: pending.modulators.map((modulator) => finishModulator(modulator, sourceFrequency)),\n envelope: finishEnvelope(layer.envelope, duration, levelInEnvelope ? level : 1),\n }\n if (!layer.repeat) return [planned]\n return Array.from({ length: layer.repeat.count }, (_, index) => ({ ...planned, at: planned.at + index * (layer.repeat?.spacing ?? 0) }))\n}\n\nfunction scaleAutomation(automation: PlannedAutomation, divisor: number): PlannedAutomation {\n const scale = (value: number | undefined): number | undefined => (value === undefined ? undefined : value / divisor)\n const scaled: Record<string, unknown> = { ...automation }\n for (const key of ['value', 'from', 'to'] as const) if (automation[key] !== undefined) scaled[key] = scale(automation[key])\n return scaled as PlannedAutomation\n}\n\n// distance muffling and occlusion lower every lowpass cutoff of the sound\nfunction muffle(layer: PlannedLayer, divisor: number): PlannedLayer {\n if (divisor === 1 || !layer.filters) return layer\n const bank = layer.filters.bank.map((filter) => (filter.type === 'lowpass' ? { ...filter, frequency: scaleAutomation(filter.frequency, divisor) } : filter))\n return { ...layer, filters: { ...layer.filters, bank } }\n}\n\nconst layerEnd = (layer: PlannedLayer): number => layer.at + layer.duration + layer.tail\n\n/**\n * Turns a recipe into a plan: pure, Node-safe, the same plan for the same `random`. Numbers are drawn in this order: `vars` (in the order written),\n * `duration`, then each layer in the order its fields are written, the noise start point last in its layer. A sound below its falloff's `minGain`\n * gives `null` (or a silent plan with `keepSilent`) before any number is drawn.\n */\nexport function planSound(recipe: SoundRecipe, input: PlanInput = {}): SoundPlan | null {\n const listener = input.listener ?? DEFAULT_LISTENER\n const distance = input.distance ?? (input.position ? listenerDistance(listener, input.position, input.distanceMode) : 0)\n const occlusion = Math.min(1, Math.max(0, input.occlusion ?? 0))\n const order = recipe.order ?? 'layered'\n const falloffLevel = falloffGain(recipe.falloff, input.volume ?? 1, distance)\n const level = occlusion > 0 ? falloffLevel * (1 - OCCLUSION_GAIN * occlusion) : falloffLevel\n const silent = isSilent(recipe.falloff, falloffLevel)\n const base = {\n id: recipe.id,\n bus: recipe.bus,\n order,\n ...(recipe.group === undefined ? {} : { group: recipe.group }),\n priority: recipe.priority ?? busPriority(recipe.bus),\n level,\n distance,\n ...(recipe.pan ? { pan: input.position ? panFor(listener, input.position) : 0 } : {}),\n }\n if (silent) return recipe.keepSilent ? { ...base, silent: true, length: 0, layers: [] } : null\n\n const scope: Scope = { random: input.random ?? createRng(1), params: { ...input.params } }\n for (const [name, range] of Object.entries(recipe.vars ?? {})) if (scope.params[name] === undefined) scope.params[name] = draw(range, scope)\n const defaults: LayerDefaults = {\n ...(recipe.duration === undefined ? {} : { duration: draw(recipe.duration, scope) }),\n ...(recipe.tail === undefined ? {} : { tail: recipe.tail }),\n noiseSeconds: input.noiseSeconds ?? DEFAULT_NOISE_SECONDS,\n }\n const divisor = cutoffDivisor(recipe.falloff, distance) / (1 - OCCLUSION_CUTOFF * occlusion)\n const layers = recipe.layers.flatMap((layer) => planLayer(layer, scope, defaults, order !== 'layered', level)).map((layer) => muffle(layer, divisor))\n return { ...base, silent: false, length: Math.max(0, ...layers.map(layerEnd)), layers }\n}\n","import type { LoopRecipe } from './recipe'\n\n/** jogo L62112: a loop's nodes exist only once its volume reaches 0.002. */\nexport const LOOP_START_ABOVE = 0.002\n/** jogo L62125–62126: `setTargetAtTime` time constants. */\nexport const LOOP_SMOOTHING = { volume: 0.15, frequency: 0.08 } as const\n\n/** What the realizer does for one `set`: create the nodes first (once), then glide the volume and, for an oscillator, the frequency. */\nexport type LoopAction = { readonly create: boolean; readonly volume: number; readonly frequency?: number }\n\nexport type LoopState = {\n /** `null` while the loop has no nodes and the volume is still below the start threshold. */\n set(state: { readonly volume?: number; readonly frequency?: number }): LoopAction | null\n /** Whether the nodes exist. */\n readonly created: boolean\n /** The nodes were torn down (`stop`): the next audible `set` creates them again. */\n reset(): void\n}\n\n/**\n * The game's `loopSet` as state: nodes on the first volume at or above `startAbove`, then every `set` glides. The volume set last is kept, so a\n * `set` of only the frequency keeps the level. `chain_loop` recipes open with their nodes already made (`open` is true).\n */\nexport function createLoopState(recipe: LoopRecipe, open = false): LoopState {\n const startAbove = recipe.startAbove ?? LOOP_START_ABOVE\n let created = open\n let volume = recipe.volume ?? 0\n return {\n set(state) {\n volume = state.volume ?? volume\n const create = !created\n if (create && !(volume >= startAbove)) return null\n created = true\n const frequency = recipe.source.kind === 'osc' && state.frequency ? state.frequency : undefined\n return { create, volume, ...(frequency === undefined ? {} : { frequency }) }\n },\n get created() {\n return created\n },\n reset() {\n created = false\n },\n }\n}\n","[\n {\n \"id\": \"game\",\n \"master\": 0.5,\n \"noise\": { \"seconds\": 1.5, \"create\": \"lazy\" },\n \"sources\": [\"jogo:L62077-62081 master 0.5 to the output\", \"jogo:L62082-62088 noise 1.5 s made by the first sound that needs it\"]\n },\n {\n \"id\": \"art_scene\",\n \"master\": 0.85,\n \"compressor\": { \"threshold\": -12, \"ratio\": 5 },\n \"reverb\": { \"seconds\": 2.2, \"decay\": 3.4, \"wet\": 0.4 },\n \"noise\": { \"seconds\": 2, \"create\": \"eager\" },\n \"sources\": [\n \"cena:L1308 compressor -12 dB, ratio 5\",\n \"cena:L1309 master 0.85\",\n \"cena:L1310 convolver, impulse 2.2 s of (1 - i/len)^3.4\",\n \"cena:L1311 wet 0.4\",\n \"cena:L1312 noise 2 s made with the chain\"\n ]\n }\n]\n","import mixProfilesJson from './data/mix-profiles.json'\nimport type { BusId } from './recipe'\n\nexport type BusSetting = { readonly gain?: number; readonly muted?: boolean }\n\n/**\n * A mixing chain. `master` → (compressor) → output; with `reverb`, a wet bus → convolver (an impulse from the engine's seed) → master. `noise` is\n * the shared noise buffer: its length, and whether it is made with the chain (`eager`, the art scene) or by the first sound that needs it (`lazy`,\n * the game). Buses sit under the master and have no node until one is changed.\n */\nexport type MixProfile = {\n readonly id: string\n readonly master: number\n readonly compressor?: { readonly threshold: number; readonly ratio: number; readonly knee?: number; readonly attack?: number; readonly release?: number }\n readonly reverb?: { readonly seconds: number; readonly decay: number; readonly wet: number }\n readonly noise: { readonly seconds: number; readonly create: 'lazy' | 'eager' }\n readonly buses?: Readonly<Partial<Record<BusId, BusSetting>>>\n readonly sources: readonly string[]\n}\n\n/** The two team chains: `game` (master 0.5, jogo L62080) and `art_scene` (compressor and reverb, cena L1305–1313). */\nexport const MIX_PROFILES: Readonly<Record<string, MixProfile>> = Object.fromEntries(\n (mixProfilesJson as unknown as readonly MixProfile[]).map((profile) => [profile.id, profile]),\n)\n/** Q1: the kit plays through the art scene chain (compressor and reverb); `mix: 'game'` reverts. */\nexport const DEFAULT_MIX = 'art_scene'\n\n/** A profile by id from `profiles`, or the profile itself. */\nexport function resolveMix(mix: string | MixProfile, profiles: Readonly<Record<string, MixProfile>> = MIX_PROFILES): MixProfile {\n if (typeof mix !== 'string') return mix\n const profile = profiles[mix]\n if (!profile) throw new Error(`game-audio: unknown mix profile \"${mix}\" (known: ${Object.keys(profiles).join(', ')})`)\n return profile\n}\n\n/** The gain a bus plays at: 0 when muted, its gain (default 1) otherwise. */\nexport function busGain(setting: BusSetting | undefined): number {\n if (!setting) return 1\n return setting.muted ? 0 : (setting.gain ?? 1)\n}\n","import type { LoopRecipe, SoundBank, SoundRecipe } from './recipe'\n\nexport type DeepPartial<TValue> = TValue extends readonly unknown[] ? TValue : TValue extends object ? { readonly [Key in keyof TValue]?: DeepPartial<TValue[Key]> } : TValue\n\n/** Every sound and loop the engine knows, by id. Banks register in order; a later bank replaces an earlier recipe with the same id. */\nexport type SoundRegistry = {\n register(bank: SoundBank): void\n /** Merged into the recipe by id (objects merge, arrays such as `layers` are replaced); an unknown id is ignored. */\n patch(id: string, patch: DeepPartial<SoundRecipe> | DeepPartial<LoopRecipe>): void\n remove(id: string): void\n sound(id: string): SoundRecipe | undefined\n loop(id: string): LoopRecipe | undefined\n soundIds(): string[]\n loopIds(): string[]\n /** Which bank each id came from. */\n bankOf(id: string): string | undefined\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n// keys keep their place, so a patched range is still drawn where it was\nfunction mergeRecords(base: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...base }\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined) continue\n const current = merged[key]\n merged[key] = isPlainObject(current) && isPlainObject(value) ? mergeRecords(current, value) : value\n }\n return merged\n}\n\nexport function createSoundRegistry(banks: readonly SoundBank[] = []): SoundRegistry {\n const sounds = new Map<string, SoundRecipe>()\n const loops = new Map<string, LoopRecipe>()\n const owners = new Map<string, string>()\n const registry: SoundRegistry = {\n register(bank) {\n for (const sound of bank.sounds ?? []) {\n sounds.set(sound.id, sound)\n owners.set(sound.id, bank.id)\n }\n for (const loop of bank.loops ?? []) {\n loops.set(loop.id, loop)\n owners.set(loop.id, bank.id)\n }\n },\n patch(id, patch) {\n const sound = sounds.get(id)\n if (sound) sounds.set(id, mergeRecords(sound, patch as Record<string, unknown>) as SoundRecipe)\n const loop = loops.get(id)\n if (loop) loops.set(id, mergeRecords(loop, patch as Record<string, unknown>) as LoopRecipe)\n },\n remove(id) {\n sounds.delete(id)\n loops.delete(id)\n owners.delete(id)\n },\n sound: (id) => sounds.get(id),\n loop: (id) => loops.get(id),\n soundIds: () => [...sounds.keys()],\n loopIds: () => [...loops.keys()],\n bankOf: (id) => owners.get(id),\n }\n for (const bank of banks) registry.register(bank)\n return registry\n}\n","{\n \"sources\": [\"jogo:L62089-62107\"],\n \"note\": \"The game's sfx table P: [lowpass cutoff Hz, peak gain, seconds]. Played as a noise burst with the game's distance curve (falloff game_sfx) and a 0.05 s tail. Team names in `team`.\",\n \"classes\": [\n { \"id\": \"pistol\", \"team\": \"pistola\", \"bus\": \"weapons\", \"cutoff\": 1800, \"gain\": 0.9, \"seconds\": 0.18 },\n { \"id\": \"shotgun\", \"team\": \"escopeta\", \"bus\": \"weapons\", \"cutoff\": 900, \"gain\": 1.2, \"seconds\": 0.35 },\n { \"id\": \"rifle\", \"team\": \"fuzil\", \"bus\": \"weapons\", \"cutoff\": 1400, \"gain\": 1.1, \"seconds\": 0.28 },\n { \"id\": \"long_rifle\", \"team\": \"rifle\", \"bus\": \"weapons\", \"cutoff\": 1100, \"gain\": 1.3, \"seconds\": 0.45 },\n { \"id\": \"impact\", \"team\": \"impacto\", \"bus\": \"world\", \"cutoff\": 2500, \"gain\": 0.25, \"seconds\": 0.05 },\n { \"id\": \"flesh\", \"team\": \"carne\", \"bus\": \"world\", \"cutoff\": 600, \"gain\": 0.35, \"seconds\": 0.08 },\n { \"id\": \"step\", \"team\": \"passo\", \"bus\": \"world\", \"cutoff\": 400, \"gain\": 0.08, \"seconds\": 0.05 },\n { \"id\": \"glass\", \"team\": \"vidro\", \"bus\": \"world\", \"cutoff\": 5000, \"gain\": 0.5, \"seconds\": 0.3 },\n { \"id\": \"wood\", \"team\": \"madeira\", \"bus\": \"world\", \"cutoff\": 700, \"gain\": 0.5, \"seconds\": 0.2 },\n { \"id\": \"reload\", \"team\": \"recarga\", \"bus\": \"weapons\", \"cutoff\": 3000, \"gain\": 0.15, \"seconds\": 0.06 },\n { \"id\": \"empty\", \"team\": \"vazio\", \"bus\": \"weapons\", \"cutoff\": 4000, \"gain\": 0.12, \"seconds\": 0.03 },\n { \"id\": \"explosion\", \"team\": \"explosao\", \"bus\": \"world\", \"cutoff\": 200, \"gain\": 1.6, \"seconds\": 1.4 }\n ],\n \"fallback\": { \"id\": \"unknown\", \"team\": \"(any other name)\", \"bus\": \"world\", \"cutoff\": 1000, \"gain\": 0.5, \"seconds\": 0.1 }\n}\n","import { DISTANCE_CURVES } from '../falloff'\nimport type { BusId, SoundBank, SoundRecipe } from '../recipe'\nimport gameSfxJson from '../data/game-sfx.json'\n\n/** One row of the game's `sfx` table (jogo L62096–62100): lowpass cutoff, peak gain and seconds of a noise burst. */\nexport type NoiseClass = {\n readonly id: string\n /** The name in the team code (`pistola`, `vidro`...). */\n readonly team: string\n readonly bus: BusId\n readonly cutoff: number\n readonly gain: number\n readonly seconds: number\n}\n\ntype GameSfxTable = { readonly sources: readonly string[]; readonly classes: readonly NoiseClass[]; readonly fallback: NoiseClass }\n\nconst GAME_SFX = gameSfxJson as unknown as GameSfxTable\n\n/** The 12 game classes, in the table's order. */\nexport const GAME_SFX_CLASSES: readonly NoiseClass[] = GAME_SFX.classes\n/** What the game plays for a name it does not know: `[1000, 0.5, 0.1]`. */\nexport const GAME_SFX_FALLBACK: NoiseClass = GAME_SFX.fallback\n\n/**\n * A class as a recipe: noise from a random point, the class lowpass (muffled by distance), the peak set at once and an exponential fall to 0.001 over\n * the class seconds, stopped 0.05 s later; the game's distance curve, silent below 0.01. Built in the game's `sfx` order (`burst`).\n */\nexport function noiseClassRecipe(row: NoiseClass, id = `sfx_${row.id}`): SoundRecipe {\n return {\n id,\n bus: row.bus,\n order: 'burst',\n duration: row.seconds,\n tail: 0.05,\n layers: [\n {\n source: { kind: 'noise', offset: 'random' },\n filters: { mode: 'serial', bank: [{ type: 'lowpass', frequency: { set: row.cutoff } }] },\n envelope: [\n [0, row.gain, 'set'],\n ['1d', 0.001, 'exp', 'fixed'],\n ],\n },\n ],\n falloff: DISTANCE_CURVES.game_sfx,\n sources: [...GAME_SFX.sources.map((source) => `${source} ${row.team}`)],\n }\n}\n\n/** The game's classes as a bank (`sfx_pistol`... and `sfx_unknown`). Subject banks pick their own classes from `GAME_SFX_CLASSES`. */\nexport const GAME_SFX_BANK: SoundBank = { id: 'game_sfx', sounds: [...GAME_SFX_CLASSES, GAME_SFX_FALLBACK].map((row) => noiseClassRecipe(row)) }\n","// The art scene's synthesis primitives (cena L1322-L1328) as recipe layers, shared by every subject bank that ports a `sfx.*` of the page: the\n// materials in `destruction-three`, the voices and wounds in `characters-three`. Each helper writes the layer the team routine builds, with its\n// fields in the order the routine draws its random numbers, so a recipe built from them leaves the trace of the team code.\nimport { DISTANCE_CURVES } from '../falloff'\nimport type { BusId, Envelope, Layer, ParamRef, Range, SoundRecipe } from '../recipe'\n\nconst ATTACK = 0.002\n\n/** A length in seconds: a number, a `[min, max]` range or a named number. */\nexport type SceneLength = number | readonly [number, number] | ParamRef\n\n// the layer is `attack + d` long (the art scene's gain rises over `a` and falls over `d`)\nfunction lengthOf(d: SceneLength, attack: number): Range {\n if (typeof d === 'number') return attack + d\n if (Array.isArray(d)) return [attack + d[0], attack + d[1]] as const\n return { ...(d as ParamRef), add: ((d as ParamRef).add ?? 0) + attack }\n}\n\nconst envelope = (attack: number): Envelope => [\n [0, 0.0001, 'set', 'fixed'],\n [attack, 1, 'exp'],\n ['1d', 0.0001, 'exp', 'fixed'],\n]\n\nexport type SceneNoise = {\n readonly type?: BiquadFilterType\n readonly f: Range\n /** The filter slides here over `d` (needs a fixed `d`). */\n readonly f2?: number\n readonly q?: number\n readonly a?: number\n readonly d: SceneLength\n readonly vol: Range\n readonly wet?: number\n readonly delay?: Range\n}\n\n/** `sNoise` (cena L1323): noise from a random point, a filter (default lowpass, Q 0.7), the gain rising over `a` and falling over `d`. */\nexport function sceneNoise(o: SceneNoise): Layer {\n const a = o.a ?? ATTACK\n const over = typeof o.d === 'number' ? o.d : 0\n return {\n filters: {\n mode: 'serial',\n bank: [{ type: o.type ?? 'lowpass', frequency: o.f2 === undefined ? { set: o.f } : { set: o.f, to: o.f2, curve: 'exp', over }, q: o.q ?? 0.7 }],\n },\n duration: lengthOf(o.d, a),\n gain: o.vol,\n ...(o.delay === undefined ? {} : { at: o.delay }),\n ...(o.wet === undefined ? {} : { wet: o.wet }),\n source: { kind: 'noise', offset: 'random', offsetScale: 1.5, loop: true },\n envelope: envelope(a),\n }\n}\n\nexport type SceneTone = { readonly wave?: OscillatorType; readonly f: Range; readonly f2?: number; readonly d: SceneLength; readonly vol: Range; readonly wet?: number; readonly delay?: Range }\n\n/** `sTone` (cena L1324): an oscillator sliding from `f` to `f2` (back to `f` when none), the gain rising over 0.002 s and falling over `d`. */\nexport function sceneTone(o: SceneTone): Layer {\n return {\n source: { kind: 'osc', wave: o.wave ?? 'sine', frequency: { from: o.f, to: o.f2 ?? 'from', curve: 'exp', over: 'duration' } },\n duration: lengthOf(o.d, ATTACK),\n gain: o.vol,\n ...(o.delay === undefined ? {} : { at: o.delay }),\n ...(o.wet === undefined ? {} : { wet: o.wet }),\n envelope: envelope(ATTACK),\n }\n}\n\nexport type SceneVoice = {\n /** Start and end frequency of the sweep (the page ramps linearly from `f0` to `f1` over the length). */\n readonly f0: Range\n readonly f1: Range\n /** Length in seconds. */\n readonly d: SceneLength\n /** Centre of the first and second formant (Q 4 and 6) and of the breath band (Q 1.1). */\n readonly fA: Range\n readonly fB: Range\n /** Level of the breath noise. */\n readonly noise: Range\n readonly vol: Range\n readonly wave?: OscillatorType\n}\n\n/**\n * `voice` (cena L1325): an oscillator swept from `f0` to `f1` with a vibrato of 4 to 10 Hz (depth 0.08 of `f0`) through two formant bands plus a band of\n * breath noise, summed and boosted 3.2 times, the gain rising over 0.1 s, held to 0.6 of the length and falling; reverb send 0.45. It is the one\n * layer the page builds with `voice(...)`, so a scream is two of them.\n */\nexport function sceneVoice(o: SceneVoice): Layer {\n return {\n duration: lengthOf(o.d, 0),\n gain: o.vol,\n wet: 0.45,\n source: {\n kind: 'formant_voice',\n wave: o.wave ?? 'sawtooth',\n frequency: { from: o.f0, to: o.f1, curve: 'linear', over: 'duration' },\n vibrato: { rate: [4, 10], depth: 0.08 },\n formants: [\n { frequency: o.fA, q: 4 },\n { frequency: o.fB, q: 6 },\n ],\n breath: { frequency: o.fB, q: 1.1, gain: o.noise },\n mix: 3.2,\n },\n envelope: [\n [0, 0.0001, 'set', 'fixed'],\n [0.1, 1, 'exp'],\n ['0.6d', 1, 'set'],\n ['1d', 0.0001, 'exp', 'fixed'],\n ],\n }\n}\n\n/** The layer played `count` times (a team `for` loop whose body draws numbers): each pass is its own layer, so the draws stay in the loop's order. */\nexport const sceneMany = (count: number, make: () => Layer): Layer[] => Array.from({ length: count }, make)\n\nexport type SceneRecipeOptions = {\n /** Named numbers drawn first (`PlayOptions.params` can override them). */\n readonly vars?: SoundRecipe['vars']\n readonly bus?: BusId\n readonly group?: string\n}\n\n/** A recipe in the art scene's mix: positional with the art scene's curve (`vol / (1 + d² × 0.008)`) and pan; `line` is the team code it comes from (`cena:<line>`). */\nexport function sceneRecipe(id: string, layers: readonly Layer[], line: string, options: SceneRecipeOptions = {}): SoundRecipe {\n return {\n id,\n bus: options.bus ?? 'world',\n pan: true,\n layers,\n falloff: DISTANCE_CURVES.art_scene,\n ...(options.vars ? { vars: options.vars } : {}),\n ...(options.group ? { group: options.group } : {}),\n sources: [`cena:${line}`],\n }\n}\n","{\n \"id\": \"crowd_rumble\",\n \"bus\": \"ambience\",\n \"order\": \"game_loop\",\n \"source\": { \"kind\": \"noise\", \"loop\": true },\n \"filters\": { \"mode\": \"serial\", \"bank\": [{ \"type\": \"lowpass\", \"frequency\": { \"set\": 160 } }] },\n \"smoothing\": { \"volume\": 0.15, \"frequency\": 0.08 },\n \"startAbove\": 0.002,\n \"sources\": [\"jogo:L62116 horda: noise through a 160 Hz lowpass\", \"jogo:L70424 level min(0.16, near × 0.0016)\", \"temporary home until the crowd package (Q12)\"]\n}\n","{\n \"id\": \"engine_saw\",\n \"bus\": \"world\",\n \"order\": \"game_loop\",\n \"source\": { \"kind\": \"osc\", \"wave\": \"sawtooth\", \"loop\": true },\n \"filters\": { \"mode\": \"serial\", \"bank\": [{ \"type\": \"lowpass\", \"frequency\": { \"set\": 400 } }] },\n \"smoothing\": { \"volume\": 0.15, \"frequency\": 0.08 },\n \"startAbove\": 0.002,\n \"sources\": [\n \"jogo:L62115 motor: sawtooth through a 400 Hz lowpass\",\n \"jogo:L70421 level 0.035 + speed × 0.003, frequency 38 + speed × 7 (the host drives `set`)\",\n \"temporary home until the vehicles package (Q12)\"\n ]\n}\n","{\n \"id\": \"horn\",\n \"bus\": \"world\",\n \"order\": \"chord\",\n \"duration\": 0.6,\n \"tail\": 0.02,\n \"layers\": [\n { \"source\": { \"kind\": \"osc\", \"wave\": \"square\", \"frequency\": { \"set\": 415 } }, \"envelope\": [[0, 0.08, \"set\"], [0.6, 0.0001, \"set\", \"fixed\"]] },\n { \"source\": { \"kind\": \"osc\", \"wave\": \"square\", \"frequency\": { \"set\": 523 } }, \"envelope\": [[0, 0.08, \"set\"], [0.6, 0.0001, \"set\", \"fixed\"]] }\n ],\n \"falloff\": { \"model\": \"none\" },\n \"sources\": [\"jogo:L70272-70276 buzina: squares at 415 and 523 Hz, 0.08 for 0.6 s\", \"temporary home until the vehicles package (Q12)\"]\n}\n","{\n \"id\": \"radio_static\",\n \"bus\": \"world\",\n \"order\": \"static\",\n \"duration\": 0.6,\n \"layers\": [\n {\n \"source\": { \"kind\": \"noise\", \"offset\": \"random\" },\n \"filters\": { \"mode\": \"serial\", \"bank\": [{ \"type\": \"bandpass\", \"frequency\": { \"set\": 1800 }, \"q\": 0.8 }] },\n \"envelope\": [[0, 0.06, \"set\"], [\"1d\", 0.001, \"exp\", \"fixed\"]]\n }\n ],\n \"falloff\": { \"model\": \"none\" },\n \"sources\": [\"jogo:L69998-70004 chiado: noise through a 1800 Hz bandpass (Q 0.8), 0.06 falling over 0.6 s\", \"temporary home until the props or radio package (Q12)\"]\n}\n","{\n \"id\": \"rotor_blades\",\n \"bus\": \"world\",\n \"order\": \"game_loop\",\n \"source\": { \"kind\": \"noise\", \"loop\": true },\n \"filters\": { \"mode\": \"serial\", \"bank\": [{ \"type\": \"lowpass\", \"frequency\": { \"set\": 300 } }] },\n \"modulators\": [{ \"target\": \"gain\", \"rate\": 11, \"depth\": 0.5, \"base\": 0.5 }],\n \"smoothing\": { \"volume\": 0.15, \"frequency\": 0.08 },\n \"startAbove\": 0.002,\n \"sources\": [\n \"jogo:L62116-62120 rotor: noise through a 300 Hz lowpass, blade beat as an 11 Hz LFO of 0.5 on a 0.5 gain stage\",\n \"jogo:L70426 level min(0.5, 30 / (distance + 10))\",\n \"temporary home until the vehicles package (Q12)\"\n ]\n}\n","{\n \"id\": \"siren\",\n \"bus\": \"world\",\n \"order\": \"siren\",\n \"duration\": 9,\n \"tail\": 0.2,\n \"layers\": [\n {\n \"wet\": 1.5,\n \"source\": { \"kind\": \"osc\", \"wave\": \"sine\", \"frequency\": { \"set\": 850 } },\n \"modulators\": [{ \"target\": \"frequency\", \"rate\": 0.18, \"depth\": 280 }],\n \"filters\": { \"mode\": \"serial\", \"bank\": [{ \"type\": \"lowpass\", \"frequency\": { \"set\": 1400 } }] },\n \"envelope\": [[0, 0.0001, \"set\", \"fixed\"], [2, 0.035, \"exp\"], [6, 0.035, \"set\"], [9, 0.0001, \"exp\", \"fixed\"]]\n }\n ],\n \"falloff\": { \"model\": \"none\" },\n \"sources\": [\"cena:L1369 siren: 850 Hz sine swept ±280 Hz at 0.18 Hz, 1400 Hz lowpass, 0.035 for 9 s, reverb send 1.5\", \"temporary home until the vehicles package (Q12)\"]\n}\n","// Sounds whose subject has no package yet (Q12): they move to vehicles, crowd and props in Phases 7 and 8 and stay here as re-exports.\nimport type { LoopRecipe, SoundBank, SoundRecipe } from '../recipe'\nimport crowdRumble from './crowd_rumble.json'\nimport engineSaw from './engine_saw.json'\nimport horn from './horn.json'\nimport radioStatic from './radio_static.json'\nimport rotorBlades from './rotor_blades.json'\nimport siren from './siren.json'\n\nexport const LIBRARY_SOUNDS: readonly SoundRecipe[] = [horn, radioStatic, siren] as unknown as readonly SoundRecipe[]\nexport const LIBRARY_LOOPS: readonly LoopRecipe[] = [engineSaw, rotorBlades, crowdRumble] as unknown as readonly LoopRecipe[]\nexport const LIBRARY_BANK: SoundBank = { id: 'library', sounds: LIBRARY_SOUNDS, loops: LIBRARY_LOOPS }\n","/** `length` samples of white noise, `random × 2 − 1`, drawn in order (jogo L62086, cena L1312). */\nexport function fillNoise(random: () => number, length: number): Float32Array {\n const samples = new Float32Array(length)\n for (let index = 0; index < length; index++) samples[index] = random() * 2 - 1\n return samples\n}\n\n/**\n * A reverb impulse: per channel, noise times `(1 − i / length)^decay` (cena L1310). `length` is `sampleRate × seconds` as the art scene computes it,\n * which is not always whole (48000 × 2.2 = 105600.00000000001): the loop runs while `i < length`, so one more number is drawn than the buffer holds,\n * exactly like the team code. The power is `exp(decay × log(base))`, which every engine computes the same.\n */\nexport function fillImpulse(random: () => number, options: { readonly channels: number; readonly length: number; readonly decay: number }): Float32Array[] {\n const size = Math.floor(options.length)\n return Array.from({ length: options.channels }, () => {\n const samples = new Float32Array(size)\n for (let index = 0; index < options.length; index++) {\n const value = (random() * 2 - 1) * Math.exp(options.decay * Math.log(1 - index / options.length))\n if (index < size) samples[index] = value\n }\n return samples\n })\n}\n\n/** An AudioBuffer holding the samples (one channel per array). */\nexport function bufferOf(context: AudioContext, channels: readonly Float32Array[], requestedLength: number): AudioBuffer {\n const buffer = context.createBuffer(channels.length, requestedLength, context.sampleRate)\n channels.forEach((samples, channel) => buffer.getChannelData(channel).set(samples))\n return buffer\n}\n","import type { MixProfile } from '../mix'\nimport { bufferOf, fillImpulse } from './noise'\n\nexport type MixChain = { readonly master: GainNode; readonly wet?: GainNode; disconnect(): void }\n\n/**\n * The mixing chain in the team order (cena L1308–1311; jogo L62080): compressor to the output, master into it (or straight to the output), then the\n * convolver with an impulse drawn from `random` and the wet bus into it, back into the master.\n */\nexport function buildChain(context: AudioContext, profile: MixProfile, random: () => number): MixChain {\n let compressor: DynamicsCompressorNode | undefined\n if (profile.compressor) {\n compressor = context.createDynamicsCompressor()\n compressor.threshold.value = profile.compressor.threshold\n compressor.ratio.value = profile.compressor.ratio\n if (profile.compressor.knee !== undefined) compressor.knee.value = profile.compressor.knee\n if (profile.compressor.attack !== undefined) compressor.attack.value = profile.compressor.attack\n if (profile.compressor.release !== undefined) compressor.release.value = profile.compressor.release\n compressor.connect(context.destination)\n }\n const master = context.createGain()\n master.gain.value = profile.master\n master.connect(compressor ?? context.destination)\n if (!profile.reverb) {\n return {\n master,\n disconnect: () => {\n master.disconnect()\n compressor?.disconnect()\n },\n }\n }\n const convolver = context.createConvolver()\n const length = context.sampleRate * profile.reverb.seconds\n convolver.buffer = bufferOf(context, fillImpulse(random, { channels: 2, length, decay: profile.reverb.decay }), length)\n const wet = context.createGain()\n wet.gain.value = profile.reverb.wet\n wet.connect(convolver)\n convolver.connect(master)\n return {\n master,\n wet,\n disconnect: () => {\n wet.disconnect()\n convolver.disconnect()\n master.disconnect()\n compressor?.disconnect()\n },\n }\n}\n","type AudioContextConstructor = new () => AudioContext\n\n/** A new context from the page (`AudioContext` or the old `webkitAudioContext`), or `null` in Node and workers. */\nexport function globalContext(): AudioContext | null {\n const scope = globalThis as { AudioContext?: AudioContextConstructor; webkitAudioContext?: AudioContextConstructor }\n const Constructor = scope.AudioContext ?? scope.webkitAudioContext\n return Constructor ? new Constructor() : null\n}\n\n/** Browsers start a context suspended until a click or key; call from one. */\nexport function unlockContext(context: AudioContext): void {\n if (context.state === 'suspended') void context.resume()\n}\n","import type { PlannedAutomation, PlannedEnvelopePoint, PlannedFilter } from '../plan'\n\n/** Where a plan is built: the context, the bus it plays into, the reverb send (if the mix has one), the shared noise and the start time. */\nexport type RealizeTarget = {\n readonly context: AudioContext\n readonly output: AudioNode\n readonly wet?: AudioNode\n /** The noise buffer; made on the first call when the mix makes it on demand. */\n readonly noise: () => AudioBuffer\n readonly time: number\n}\n\n/** What `realize` built: every scheduled source (so the voice can be cut short) and `stop`. */\nexport type RealizedVoice = { readonly sources: readonly AudioScheduledSourceNode[]; stop(): void }\n\n/** `.value = value`, or `setValueAtTime(from)` at `time`. */\nexport function assign(param: AudioParam, automation: PlannedAutomation, time: number): void {\n if (automation.value !== undefined) param.value = automation.value\n if (automation.from !== undefined) param.setValueAtTime(automation.from, time)\n}\n\nexport function ramp(param: AudioParam, curve: 'set' | 'linear' | 'exp', value: number, at: number): void {\n if (curve === 'set') param.setValueAtTime(value, at)\n else if (curve === 'exp') param.exponentialRampToValueAtTime(value, at)\n else param.linearRampToValueAtTime(value, at)\n}\n\n/** The ramp of an automation, if it has one. */\nexport function rampTo(param: AudioParam, automation: PlannedAutomation, time: number): void {\n if (automation.to !== undefined && automation.at !== undefined) ramp(param, automation.curve ?? 'linear', automation.to, time + automation.at)\n}\n\nexport function applyEnvelope(param: AudioParam, points: readonly PlannedEnvelopePoint[], time: number): void {\n for (const point of points) ramp(param, point.curve, point.value, time + point.time)\n}\n\n/** A noise source on the shared buffer (`loop` set only when asked, as the art scene's `nsrc` does). */\nexport function noiseSource(target: RealizeTarget, loop: boolean): AudioBufferSourceNode {\n const source = target.context.createBufferSource()\n source.buffer = target.noise()\n if (loop) source.loop = true\n return source\n}\n\n/** The art scene's `bq`: type, frequency, then Q and gain when given. The frequency ramp is left to the caller. */\nexport function filterNode(context: AudioContext, filter: PlannedFilter, time: number): BiquadFilterNode {\n const node = context.createBiquadFilter()\n node.type = filter.type\n assign(node.frequency, filter.frequency, time)\n if (filter.q !== undefined) node.Q.value = filter.q\n if (filter.gain !== undefined) node.gain.value = filter.gain\n return node\n}\n\nexport function gainNode(context: AudioContext, value: number): GainNode {\n const node = context.createGain()\n node.gain.value = value\n return node\n}\n\nexport function voiceOf(sources: readonly AudioScheduledSourceNode[]): RealizedVoice {\n return {\n sources,\n stop() {\n for (const source of sources) {\n try {\n source.stop()\n } catch {\n // already stopped\n }\n }\n },\n }\n}\n","// Loop nodes in the two team orders: the game's `loopSet` (jogo L62109–62127) and the art scene's loops in `initAudio` (cena L1314–1316).\nimport type { Automation, LoopRecipe, Range } from '../recipe'\nimport { filterNode, gainNode, noiseSource } from './nodes'\nimport type { RealizeTarget } from './nodes'\n\nexport type RealizedLoop = {\n readonly gain: GainNode\n readonly source: AudioScheduledSourceNode\n readonly frequency?: AudioParam\n stop(): void\n}\n\n// loops take fixed numbers: a range or a param in a loop recipe reads as 0\nconst fixed = (range: Range | undefined): number => (typeof range === 'number' ? range : 0)\nconst fixedFrequency = (automation: Automation): number => fixed('set' in automation ? automation.set : automation.from)\n\nfunction sourceOf(recipe: LoopRecipe, target: RealizeTarget): { source: AudioScheduledSourceNode; frequency?: AudioParam } {\n if (recipe.source.kind === 'noise') return { source: noiseSource(target, true) }\n const oscillator = target.context.createOscillator()\n oscillator.type = recipe.source.wave\n if (recipe.source.frequency) oscillator.frequency.value = fixedFrequency(recipe.source.frequency)\n return { source: oscillator, frequency: oscillator.frequency }\n}\n\nfunction stopAll(sources: readonly AudioScheduledSourceNode[]): () => void {\n return () => {\n for (const source of sources) {\n try {\n source.stop()\n } catch {\n // already stopped\n }\n }\n }\n}\n\n/** `loopSet` on first use: gain from 0 and filter wired first, the source and filter set, an optional blade-beat stage, then the source starts. */\nexport function realizeGameLoop(recipe: LoopRecipe, target: RealizeTarget): RealizedLoop {\n const { context } = target\n const gain = context.createGain()\n const filters = (recipe.filters?.bank ?? []).map(() => context.createBiquadFilter())\n gain.gain.value = 0\n const last = filters.reduce<AudioNode | undefined>((previous, filter) => (previous ? previous.connect(filter) : filter), undefined)\n last?.connect(gain)\n gain.connect(target.output)\n const { source, frequency } = sourceOf(recipe, target)\n recipe.filters?.bank.forEach((spec, index) => {\n const filter = filters[index] as BiquadFilterNode\n filter.type = spec.type\n filter.frequency.value = fixedFrequency(spec.frequency)\n if (spec.q !== undefined) filter.Q.value = fixed(spec.q)\n })\n const sources: AudioScheduledSourceNode[] = [source]\n for (const modulator of recipe.modulators ?? []) {\n const lfo = context.createOscillator()\n const depth = context.createGain()\n const stage = context.createGain()\n if (modulator.wave) lfo.type = modulator.wave\n lfo.frequency.value = fixed(modulator.rate)\n depth.gain.value = fixed(modulator.depth as Range)\n stage.gain.value = modulator.base ?? 0\n lfo.connect(depth)\n depth.connect(stage.gain)\n last?.disconnect()\n last?.connect(stage)\n stage.connect(gain)\n lfo.start()\n sources.push(lfo)\n }\n source.connect(filters[0] ?? gain)\n source.start()\n return { gain, source, ...(frequency ? { frequency } : {}), stop: stopAll(sources) }\n}\n\n/** The art scene's loops: source, filters, gain at its starting volume, a gain LFO, wired, started. */\nexport function realizeChainLoop(recipe: LoopRecipe, target: RealizeTarget, volume: number): RealizedLoop {\n const { context } = target\n const { source, frequency } = sourceOf(recipe, target)\n const filters = (recipe.filters?.bank ?? []).map((spec) =>\n filterNode(context, { type: spec.type, frequency: { value: fixedFrequency(spec.frequency) }, ...(spec.q === undefined ? {} : { q: fixed(spec.q) }) }, target.time),\n )\n const gain = gainNode(context, volume)\n const lfos = (recipe.modulators ?? []).map((modulator) => {\n const lfo = context.createOscillator()\n if (modulator.wave) lfo.type = modulator.wave\n lfo.frequency.value = fixed(modulator.rate)\n const depth = gainNode(context, fixed(modulator.depth as Range))\n lfo.connect(depth)\n depth.connect(gain.gain)\n return lfo\n })\n let tail: AudioNode = source\n for (const filter of filters) tail = tail.connect(filter)\n tail.connect(gain)\n gain.connect(target.output)\n source.start()\n for (const lfo of lfos) lfo.start()\n return { gain, source, ...(frequency ? { frequency } : {}), stop: stopAll([source, ...lfos]) }\n}\n","import { LOOP_SMOOTHING, createLoopState } from '../loops'\nimport type { BusId, LoopRecipe } from '../recipe'\nimport type { RealizeTarget } from './nodes'\nimport { realizeChainLoop, realizeGameLoop } from './realize-loop'\nimport type { RealizedLoop } from './realize-loop'\n\n/** A continuous sound: `set` glides the volume and, for an oscillator, the frequency; `stop` tears the nodes down. */\nexport type AudioLoopHandle = {\n set(state: { readonly volume?: number; readonly frequency?: number }): void\n stop(): void\n}\n\nexport type LoopEntry = { rewire(): void; stop(): void; readonly created: boolean }\n\n/** What a loop needs from its engine. */\nexport type LoopHost = {\n ready(): AudioContext | null\n output(bus: BusId): AudioNode\n prepareNoise(): unknown\n noise(): AudioBuffer\n register(loop: LoopEntry): void\n unregister(loop: LoopEntry): void\n}\n\nconst INERT: AudioLoopHandle = { set() {}, stop() {} }\n\n/** A `game_loop` makes its nodes on the first audible `set`; a `chain_loop` makes them when it opens, at `volume` (default the recipe's). */\nexport function createLoopHandle(recipe: LoopRecipe | undefined, host: LoopHost, volume?: number): AudioLoopHandle {\n if (!recipe) return INERT\n const loopRecipe = recipe\n const isChain = loopRecipe.order === 'chain_loop'\n const opening = volume ?? loopRecipe.volume ?? 0\n const smoothing = loopRecipe.smoothing ?? LOOP_SMOOTHING\n let state = createLoopState(loopRecipe)\n let nodes: RealizedLoop | undefined\n\n const entry: LoopEntry = {\n get created() {\n return nodes !== undefined\n },\n rewire() {\n nodes?.gain.disconnect()\n nodes?.gain.connect(host.output(loopRecipe.bus))\n },\n stop() {\n nodes?.stop()\n nodes?.gain.disconnect()\n nodes = undefined\n state = createLoopState(loopRecipe)\n host.unregister(entry)\n },\n }\n\n const create = (context: AudioContext, startVolume: number): RealizedLoop => {\n if (loopRecipe.source.kind === 'noise') host.prepareNoise()\n const target: RealizeTarget = { context, output: host.output(loopRecipe.bus), noise: host.noise, time: context.currentTime }\n const made = isChain ? realizeChainLoop(loopRecipe, target, startVolume) : realizeGameLoop(loopRecipe, target)\n host.register(entry)\n return made\n }\n\n const open = (): void => {\n const context = host.ready()\n if (!context) return\n nodes = create(context, opening)\n state = createLoopState({ ...loopRecipe, volume: opening }, true)\n }\n if (isChain) open()\n\n return {\n set(next) {\n if (isChain && !nodes) open()\n const context = host.ready()\n if (!context) return\n const action = state.set(next)\n if (!action) return\n nodes ??= create(context, 0)\n nodes.gain.gain.setTargetAtTime(action.volume, context.currentTime, smoothing.volume)\n if (action.frequency !== undefined && nodes.frequency) nodes.frequency.setTargetAtTime(action.frequency, context.currentTime, smoothing.frequency)\n },\n stop: entry.stop,\n }\n}\n","import { bufferOf, fillNoise } from './noise'\n\n/** One noise buffer and the stream it is filled from. The samples are drawn before the sound that first needs them draws its own numbers. */\nexport type NoiseStore = { readonly random: () => number; samples?: Float32Array; buffer?: AudioBuffer; reset(): void }\n\nexport function createNoiseStore(random: () => number): NoiseStore {\n const store: NoiseStore = {\n random,\n reset() {\n store.samples = undefined\n store.buffer = undefined\n },\n }\n return store\n}\n\n/** Draws the samples (once): `sampleRate × seconds` of them, as the team code sizes its buffer. */\nexport function prepareNoise(store: NoiseStore, context: AudioContext, seconds: number): Float32Array {\n store.samples ??= fillNoise(store.random, Math.floor(context.sampleRate * seconds))\n return store.samples\n}\n\n/** The AudioBuffer (made on the first call, which is where the team code calls its `noise()`). */\nexport function noiseBuffer(store: NoiseStore, context: AudioContext, seconds: number): AudioBuffer {\n const samples = prepareNoise(store, context, seconds)\n store.buffer ??= bufferOf(context, [samples], context.sampleRate * seconds)\n return store.buffer\n}\n","/** A sound waiting to be built: `run` builds it, `drop` gives its voice back when it waited too long to still make sense. */\nexport type StartJob = { readonly queuedAt: number; run(): void; drop(): void }\n\nexport type StartQueueOptions = {\n /** Most milliseconds of main-thread work one task spends building voices (at least one voice is always built). Default 2. */\n readonly budgetMs?: number\n /** A voice still waiting after this many milliseconds is dropped instead of starting late. Default 250. */\n readonly maxLatencyMs?: number\n}\n\nexport type StartQueue = { push(job: Omit<StartJob, 'queuedAt'>): void; clear(): void; readonly size: number }\n\nconst nowMs = (): number => (typeof performance === 'undefined' ? Date.now() : performance.now())\n\nfunction scheduleTask(task: () => void): void {\n if (typeof MessageChannel === 'undefined') {\n setTimeout(task, 0)\n return\n }\n const channel = new MessageChannel()\n channel.port1.onmessage = () => {\n channel.port1.close()\n task()\n }\n channel.port2.postMessage(0)\n}\n\n/** Builds voices in small timed slices between frames, so a burst (an explosion's debris, glass and metal) never lands inside one frame. */\nexport function createStartQueue(options: StartQueueOptions = {}): StartQueue {\n const budgetMs = options.budgetMs ?? 2\n const maxLatencyMs = options.maxLatencyMs ?? 250\n let jobs: StartJob[] = []\n let scheduled = false\n\n const drain = (): void => {\n scheduled = false\n const began = nowMs()\n while (jobs.length > 0) {\n const job = jobs.shift() as StartJob\n if (nowMs() - job.queuedAt > maxLatencyMs) job.drop()\n else job.run()\n if (nowMs() - began >= budgetMs) break\n }\n if (jobs.length > 0) schedule()\n }\n\n const schedule = (): void => {\n if (scheduled) return\n scheduled = true\n scheduleTask(drain)\n }\n\n return {\n push(job) {\n jobs.push({ ...job, queuedAt: nowMs() })\n schedule()\n },\n clear() {\n for (const job of jobs) job.drop()\n jobs = []\n },\n get size() {\n return jobs.length\n },\n }\n}\n","// The game's routines (jogo.js), one builder each: the nodes are created, set and wired in the routine's own order, so the trace of a plan\n// matches the trace of the team function. The level is already in the envelope; every builder connects straight to the bus.\nimport type { PlannedFilter, PlannedLayer, SoundPlan } from '../plan'\nimport { applyEnvelope, assign, noiseSource, rampTo, voiceOf } from './nodes'\nimport type { RealizedVoice, RealizeTarget } from './nodes'\n\nconst end = (layer: PlannedLayer, start: number): number => start + layer.duration + layer.tail\nconst firstFilter = (layer: PlannedLayer): PlannedFilter | undefined => layer.filters?.bank[0]\nconst offsetOf = (layer: PlannedLayer): number => (layer.source.kind === 'noise' ? layer.source.offset : 0)\n\nfunction setFilter(node: BiquadFilterNode, filter: PlannedFilter | undefined, time: number): void {\n if (!filter) return\n node.type = filter.type\n assign(node.frequency, filter.frequency, time)\n if (filter.q !== undefined) node.Q.value = filter.q\n}\n\n/** `sfx` (L62089): gain to the bus first, then the noise, its lowpass, the envelope, and a random start in the noise. */\nexport function realizeBurst(plan: SoundPlan, target: RealizeTarget): RealizedVoice {\n const { context } = target\n const layer = plan.layers[0] as PlannedLayer\n const time = target.time + layer.at\n const gain = context.createGain()\n gain.connect(target.output)\n const source = noiseSource(target, layer.source.kind === 'noise' && layer.source.loop)\n const filter = context.createBiquadFilter()\n source.connect(filter)\n filter.connect(gain)\n setFilter(filter, firstFilter(layer), time)\n applyEnvelope(gain.gain, layer.envelope, time)\n source.start(time, offsetOf(layer))\n source.stop(end(layer, time))\n return voiceOf([source])\n}\n\n/** `grito` (L62128): noise through a parallel formant bank (each band with its own level), a gain LFO for roughness. */\nexport function realizeScream(plan: SoundPlan, target: RealizeTarget): RealizedVoice {\n const { context } = target\n const layer = plan.layers[0] as PlannedLayer\n const time = target.time + layer.at\n const source = noiseSource(target, layer.source.kind === 'noise' && layer.source.loop)\n const gain = context.createGain()\n gain.connect(target.output)\n for (const band of layer.filters?.bank ?? []) {\n const filter = context.createBiquadFilter()\n filter.type = band.type\n if (band.q !== undefined) filter.Q.value = band.q\n assign(filter.frequency, band.frequency, time)\n rampTo(filter.frequency, band.frequency, time)\n const level = context.createGain()\n level.gain.value = band.level ?? 1\n source.connect(filter)\n filter.connect(level)\n level.connect(gain)\n }\n const sources: AudioScheduledSourceNode[] = [source]\n const lfos = layer.modulators.map((modulator) => {\n const lfo = context.createOscillator()\n const depth = context.createGain()\n if (modulator.wave) lfo.type = modulator.wave\n lfo.frequency.value = modulator.rate\n depth.gain.value = modulator.depth\n lfo.connect(depth)\n depth.connect(gain.gain)\n return lfo\n })\n applyEnvelope(gain.gain, layer.envelope, time)\n source.start(time, offsetOf(layer))\n source.stop(end(layer, time))\n for (const lfo of lfos) {\n lfo.start(time)\n lfo.stop(end(layer, time))\n sources.push(lfo)\n }\n return voiceOf(sources)\n}\n\n/** `voz` (L62143): oscillator → bandpass → gain; the filter frequency is set after the pitch sweep. */\nexport function realizeGroan(plan: SoundPlan, target: RealizeTarget): RealizedVoice {\n const { context } = target\n const layer = plan.layers[0] as PlannedLayer\n const time = target.time + layer.at\n const oscillator = context.createOscillator()\n const gain = context.createGain()\n const filter = context.createBiquadFilter()\n const band = firstFilter(layer)\n if (layer.source.kind === 'osc') oscillator.type = layer.source.wave\n if (band) {\n filter.type = band.type\n if (band.q !== undefined) filter.Q.value = band.q\n }\n if (layer.source.kind === 'osc') {\n assign(oscillator.frequency, layer.source.frequency, time)\n rampTo(oscillator.frequency, layer.source.frequency, time)\n }\n if (band) assign(filter.frequency, band.frequency, time)\n applyEnvelope(gain.gain, layer.envelope, time)\n oscillator.connect(filter)\n filter.connect(gain)\n gain.connect(target.output)\n oscillator.start(time)\n oscillator.stop(end(layer, time))\n return voiceOf([oscillator])\n}\n\n/** `boom` (L68278): one oscillator sweeping down into a gain. */\nexport function realizeBoom(plan: SoundPlan, target: RealizeTarget): RealizedVoice {\n const { context } = target\n const layer = plan.layers[0] as PlannedLayer\n const time = target.time + layer.at\n const oscillator = context.createOscillator()\n const gain = context.createGain()\n if (layer.source.kind === 'osc') {\n oscillator.type = layer.source.wave\n assign(oscillator.frequency, layer.source.frequency, time)\n rampTo(oscillator.frequency, layer.source.frequency, time)\n }\n applyEnvelope(gain.gain, layer.envelope, time)\n oscillator.connect(gain)\n gain.connect(target.output)\n oscillator.start(time)\n oscillator.stop(end(layer, time))\n return voiceOf([oscillator])\n}\n\n/** `chiado` (L69998): gain, noise and bandpass created first, then set, then wired. */\nexport function realizeStatic(plan: SoundPlan, target: RealizeTarget): RealizedVoice {\n const { context } = target\n const layer = plan.layers[0] as PlannedLayer\n const time = target.time + layer.at\n const gain = context.createGain()\n const source = context.createBufferSource()\n const filter = context.createBiquadFilter()\n source.buffer = target.noise()\n setFilter(filter, firstFilter(layer), time)\n applyEnvelope(gain.gain, layer.envelope, time)\n source.connect(filter)\n filter.connect(gain)\n gain.connect(target.output)\n source.start(time, offsetOf(layer))\n source.stop(end(layer, time))\n return voiceOf([source])\n}\n\n/** `buzina` (L70272): one oscillator per layer into one gain with the first layer's envelope. */\nexport function realizeChord(plan: SoundPlan, target: RealizeTarget): RealizedVoice {\n const { context } = target\n const layers = plan.layers\n const oscillators = layers.map(() => context.createOscillator())\n const gain = context.createGain()\n // the team writes `o.type = o2.type = 'square'`, which assigns right to left\n for (let index = layers.length - 1; index >= 0; index--) {\n const source = (layers[index] as PlannedLayer).source\n if (source.kind === 'osc') (oscillators[index] as OscillatorNode).type = source.wave\n }\n layers.forEach((layer, index) => {\n if (layer.source.kind === 'osc') assign((oscillators[index] as OscillatorNode).frequency, layer.source.frequency, target.time + layer.at)\n })\n applyEnvelope(gain.gain, (layers[0] as PlannedLayer).envelope, target.time + (layers[0] as PlannedLayer).at)\n for (const oscillator of oscillators) oscillator.connect(gain)\n gain.connect(target.output)\n layers.forEach((layer, index) => (oscillators[index] as OscillatorNode).start(target.time + layer.at))\n layers.forEach((layer, index) => (oscillators[index] as OscillatorNode).stop(end(layer, target.time + layer.at)))\n return voiceOf(oscillators)\n}\n\n/**\n * `bichoSom` (L66625): gain and filter are created before the level is checked (a silent plan stops there), then each repeat is an oscillator\n * with its own envelope into the shared filter, whose frequency and Q are set again on every repeat.\n */\nexport function realizeCalls(plan: SoundPlan, target: RealizeTarget): RealizedVoice {\n const { context } = target\n const gain = context.createGain()\n const filter = context.createBiquadFilter()\n if (plan.silent) return voiceOf([])\n const band = firstFilter(plan.layers[0] as PlannedLayer)\n if (band) filter.type = band.type\n filter.connect(gain)\n gain.connect(target.output)\n const oscillators = plan.layers.map((layer) => {\n const time = target.time + layer.at\n const oscillator = context.createOscillator()\n if (layer.source.kind === 'osc') {\n oscillator.type = layer.source.wave\n assign(oscillator.frequency, layer.source.frequency, time)\n rampTo(oscillator.frequency, layer.source.frequency, time)\n }\n if (band) {\n assign(filter.frequency, band.frequency, time)\n if (band.q !== undefined) filter.Q.value = band.q\n }\n const envelope = context.createGain()\n applyEnvelope(envelope.gain, layer.envelope, time)\n oscillator.connect(envelope)\n envelope.connect(filter)\n oscillator.start(time)\n oscillator.stop(end(layer, time))\n return oscillator\n })\n return voiceOf(oscillators)\n}\n","// The art scene's primitives (cena L1319–1328, L1369): `sNoise`, `sTone` and `voice`, each layer with its own output stage (`outAt`: distance\n// gain, pan, reverb send), and the siren straight to the bus. Built in the team's order so the traces match node for node.\nimport type { PlannedLayer, SoundPlan } from '../plan'\nimport { applyEnvelope, assign, filterNode, gainNode, noiseSource, rampTo, voiceOf } from './nodes'\nimport type { RealizedVoice, RealizeTarget } from './nodes'\n\nconst end = (layer: PlannedLayer, start: number): number => start + layer.duration + layer.tail\n\n/** `outAt`: the layer level, the stereo panner when the plan pans and the context has one, the bus, then the reverb send. */\nfunction outputStage(plan: SoundPlan, layer: PlannedLayer, target: RealizeTarget): GainNode {\n const { context } = target\n const level = gainNode(context, layer.level)\n let node: AudioNode = level\n if (plan.pan !== undefined && typeof context.createStereoPanner === 'function') {\n const panner = context.createStereoPanner()\n panner.pan.value = plan.pan\n level.connect(panner)\n node = panner\n }\n node.connect(target.output)\n if (layer.wet && target.wet) {\n const send = gainNode(context, layer.wet)\n node.connect(send)\n send.connect(target.wet)\n }\n return level\n}\n\nfunction realizeNoiseLayer(plan: SoundPlan, layer: PlannedLayer, target: RealizeTarget): AudioScheduledSourceNode[] {\n const { context } = target\n const time = target.time + layer.at\n const source = noiseSource(target, layer.source.kind === 'noise' && layer.source.loop)\n const filters = (layer.filters?.bank ?? []).map((filter) => {\n const node = filterNode(context, filter, time)\n rampTo(node.frequency, filter.frequency, time)\n return node\n })\n const envelope = context.createGain()\n applyEnvelope(envelope.gain, layer.envelope, time)\n let tail: AudioNode = source\n for (const filter of filters) tail = tail.connect(filter)\n tail.connect(envelope)\n envelope.connect(outputStage(plan, layer, target))\n source.start(time, layer.source.kind === 'noise' ? layer.source.offset : 0)\n source.stop(end(layer, time))\n return [source]\n}\n\nfunction realizeToneLayer(plan: SoundPlan, layer: PlannedLayer, target: RealizeTarget): AudioScheduledSourceNode[] {\n const { context } = target\n const time = target.time + layer.at\n const oscillator = context.createOscillator()\n if (layer.source.kind === 'osc') {\n oscillator.type = layer.source.wave\n assign(oscillator.frequency, layer.source.frequency, time)\n rampTo(oscillator.frequency, layer.source.frequency, time)\n }\n const envelope = context.createGain()\n applyEnvelope(envelope.gain, layer.envelope, time)\n oscillator.connect(envelope)\n envelope.connect(outputStage(plan, layer, target))\n oscillator.start(time)\n oscillator.stop(end(layer, time))\n return [oscillator]\n}\n\nfunction realizeVoiceLayer(plan: SoundPlan, layer: PlannedLayer, target: RealizeTarget): AudioScheduledSourceNode[] {\n const source = layer.source\n if (source.kind !== 'formant_voice') return []\n const { context } = target\n const time = target.time + layer.at\n const oscillator = context.createOscillator()\n oscillator.type = source.wave\n assign(oscillator.frequency, source.frequency, time)\n rampTo(oscillator.frequency, source.frequency, time)\n const vibrato = context.createOscillator()\n vibrato.frequency.value = source.vibrato.rate\n const vibratoDepth = gainNode(context, source.vibrato.depth)\n vibrato.connect(vibratoDepth)\n vibratoDepth.connect(oscillator.frequency)\n const formants = source.formants.map((formant) => filterNode(context, { type: 'bandpass', frequency: { value: formant.frequency }, q: formant.q }, time))\n const breath = noiseSource(target, true)\n const breathBand = filterNode(context, { type: 'bandpass', frequency: { value: source.breath.frequency }, q: source.breath.q }, time)\n const breathLevel = gainNode(context, source.breath.gain)\n const mix = gainNode(context, source.mix)\n for (const formant of formants) oscillator.connect(formant)\n breath.connect(breathBand)\n breathBand.connect(breathLevel)\n for (const formant of formants) formant.connect(mix)\n breathLevel.connect(mix)\n const envelope = context.createGain()\n applyEnvelope(envelope.gain, layer.envelope, time)\n mix.connect(envelope)\n envelope.connect(outputStage(plan, layer, target))\n const stop = end(layer, time)\n oscillator.start(time)\n vibrato.start(time)\n breath.start(time, source.breath.offset)\n oscillator.stop(stop)\n vibrato.stop(stop)\n breath.stop(stop)\n return [oscillator, vibrato, breath]\n}\n\n/** Every layer in order, each with its own output stage. */\nexport function realizeLayered(plan: SoundPlan, target: RealizeTarget): RealizedVoice {\n const sources = plan.layers.flatMap((layer) => {\n if (layer.source.kind === 'noise') return realizeNoiseLayer(plan, layer, target)\n if (layer.source.kind === 'osc') return realizeToneLayer(plan, layer, target)\n return realizeVoiceLayer(plan, layer, target)\n })\n return voiceOf(sources)\n}\n\n/** The siren: oscillator with a frequency LFO, envelope, lowpass, straight to the bus with a reverb send. */\nexport function realizeSiren(plan: SoundPlan, target: RealizeTarget): RealizedVoice {\n const { context } = target\n const layer = plan.layers[0] as PlannedLayer\n const time = target.time + layer.at\n const oscillator = context.createOscillator()\n if (layer.source.kind === 'osc') {\n oscillator.type = layer.source.wave\n assign(oscillator.frequency, layer.source.frequency, time)\n rampTo(oscillator.frequency, layer.source.frequency, time)\n }\n const depths = layer.modulators.map((modulator) => {\n const lfo = context.createOscillator()\n if (modulator.wave) lfo.type = modulator.wave\n lfo.frequency.value = modulator.rate\n const depth = gainNode(context, modulator.depth)\n lfo.connect(depth)\n if (modulator.target === 'frequency') depth.connect(oscillator.frequency)\n return { lfo, depth, target: modulator.target }\n })\n const lfos = depths.map(({ lfo }) => lfo)\n const envelope = context.createGain()\n applyEnvelope(envelope.gain, layer.envelope, time)\n for (const { depth, target: modulated } of depths) if (modulated === 'gain') depth.connect(envelope.gain)\n const filters = (layer.filters?.bank ?? []).map((filter) => filterNode(context, filter, time))\n let tail: AudioNode = oscillator\n for (const filter of filters) tail = tail.connect(filter)\n tail.connect(envelope)\n envelope.connect(target.output)\n if (layer.wet && target.wet) {\n const send = gainNode(context, layer.wet)\n envelope.connect(send)\n send.connect(target.wet)\n }\n const stop = end(layer, time)\n oscillator.start(time)\n for (const lfo of lfos) lfo.start(time)\n oscillator.stop(stop)\n for (const lfo of lfos) lfo.stop(stop)\n return voiceOf([oscillator, ...lfos])\n}\n","// The weapon audio of the kit (`weapons-three`, first written against raw WebAudio): per layer, either a noise burst (source, filter, envelope gain)\n// or an oscillator (oscillator, envelope gain), created in that order and chained straight to the bus. The level is already in the envelope.\nimport type { PlannedLayer, SoundPlan } from '../plan'\nimport { applyEnvelope, assign, filterNode, noiseSource, rampTo, voiceOf } from './nodes'\nimport type { RealizedVoice, RealizeTarget } from './nodes'\n\nfunction noiseLayer(layer: PlannedLayer, target: RealizeTarget, time: number): AudioScheduledSourceNode {\n const { context } = target\n const source = noiseSource(target, layer.source.kind === 'noise' && layer.source.loop)\n const filters = (layer.filters?.bank ?? []).map((filter) => filterNode(context, filter, time))\n const envelope = context.createGain()\n applyEnvelope(envelope.gain, layer.envelope, time)\n let tail: AudioNode = source\n for (const filter of filters) tail = tail.connect(filter)\n tail.connect(envelope).connect(target.output)\n source.start(time, layer.source.kind === 'noise' ? layer.source.offset : 0)\n source.stop(time + layer.duration + layer.tail)\n return source\n}\n\nfunction toneLayer(layer: PlannedLayer, target: RealizeTarget, time: number): AudioScheduledSourceNode {\n const { context } = target\n const oscillator = context.createOscillator()\n if (layer.source.kind === 'osc') {\n oscillator.type = layer.source.wave\n assign(oscillator.frequency, layer.source.frequency, time)\n rampTo(oscillator.frequency, layer.source.frequency, time)\n }\n const envelope = context.createGain()\n applyEnvelope(envelope.gain, layer.envelope, time)\n oscillator.connect(envelope).connect(target.output)\n oscillator.start(time)\n oscillator.stop(time + layer.duration + layer.tail)\n return oscillator\n}\n\n/** One noise burst or one tone per layer, in the order written. */\nexport function realizeWeapon(plan: SoundPlan, target: RealizeTarget): RealizedVoice {\n const sources = plan.layers.map((layer) => {\n const time = target.time + layer.at\n return layer.source.kind === 'noise' ? noiseLayer(layer, target, time) : toneLayer(layer, target, time)\n })\n return voiceOf(sources)\n}\n","import type { SoundPlan } from '../plan'\nimport type { BuildOrder } from '../recipe'\nimport type { RealizedVoice, RealizeTarget } from './nodes'\nimport { realizeBoom, realizeBurst, realizeCalls, realizeChord, realizeGroan, realizeScream, realizeStatic } from './realize-game'\nimport { realizeLayered, realizeSiren } from './realize-layered'\nimport { realizeWeapon } from './realize-weapon'\n\nconst BUILDERS: Readonly<Record<BuildOrder, (plan: SoundPlan, target: RealizeTarget) => RealizedVoice>> = {\n layered: realizeLayered,\n burst: realizeBurst,\n scream: realizeScream,\n groan: realizeGroan,\n boom: realizeBoom,\n static: realizeStatic,\n chord: realizeChord,\n calls: realizeCalls,\n siren: realizeSiren,\n weapon: realizeWeapon,\n}\n\n/** Builds a plan's nodes on `target`, in the node order of the team routine its `order` names. */\nexport function realize(plan: SoundPlan, target: RealizeTarget): RealizedVoice {\n return BUILDERS[plan.order](plan, target)\n}\n","import type { Vec3 } from '@adatechnology/game-contracts'\n\nimport { createSoundRegistry } from '../bank'\nimport type { SoundRegistry } from '../bank'\nimport { falloffGain, isSilent } from '../falloff'\nimport { DEFAULT_LISTENER, listenerDistance } from '../listener'\nimport type { DistanceMode, Listener } from '../listener'\nimport { DEFAULT_MIX, busGain, resolveMix } from '../mix'\nimport type { BusSetting, MixProfile } from '../mix'\nimport { planSound } from '../plan'\nimport type { PlanInput, SoundPlan } from '../plan'\nimport type { BusId, SoundBank, SoundRecipe } from '../recipe'\nimport { createRng, streamFor } from '../rng'\nimport { busPriority, createVoiceManager } from '../voices'\nimport type { VoiceGroup } from '../voices'\nimport { buildChain } from './chain'\nimport type { MixChain } from './chain'\nimport { globalContext, unlockContext } from './context'\nimport { createLoopHandle } from './engine-loop'\nimport type { AudioLoopHandle, LoopHost } from './engine-loop'\nimport { createNoiseStore, noiseBuffer, prepareNoise } from './engine-noise'\nimport type { NoiseStore } from './engine-noise'\nimport type { RealizedVoice } from './nodes'\nimport { createStartQueue } from './engine-queue'\nimport type { StartQueueOptions } from './engine-queue'\nimport { realize } from './realize'\n\nexport type PlayOptions = {\n readonly position?: Vec3\n /** Meters; overrides `position` for the distance (not for the pan). */\n readonly distance?: number\n readonly volume?: number\n readonly params?: Readonly<Record<string, number>>\n /** This sound's own stream: same seed, same sound. */\n readonly seed?: number\n /** Seconds from now. */\n readonly delay?: number\n /** A named stream of the engine (`streamFor(seed, name)`), so one subject never shifts another's sounds. */\n readonly channel?: string\n}\n\nexport type VoiceHandle = { readonly id: number; readonly plan: SoundPlan; stop(): void }\n\n/** A subject's own random stream; `ownNoise` also gives it its own noise buffer, filled from that stream (the order `createWeaponAudio` uses). */\nexport type AudioChannel = {\n readonly random: () => number\n play(id: string, options?: PlayOptions): VoiceHandle | null\n plan(id: string, options?: PlayOptions): SoundPlan | null\n}\n\nexport type AudioStats = {\n readonly context: 'none' | AudioContextState\n readonly voices: number\n readonly peak: number\n readonly dropped: number\n readonly maxVoices: number\n readonly loops: number\n}\n\nexport type AudioEngineOptions = {\n /** Seeds the engine stream: the noise, the reverb impulse and every sound played without its own seed or channel. Default 1. */\n readonly seed?: number\n /** Default `art_scene` (Q1). */\n readonly mix?: string | MixProfile\n readonly profiles?: Readonly<Record<string, MixProfile>>\n readonly banks?: readonly SoundBank[]\n readonly maxVoices?: number\n readonly groups?: Readonly<Record<string, VoiceGroup>>\n readonly distance?: DistanceMode\n /** 0 (clear) to 1 (behind a wall) for a sound at a point; without it there is no occlusion (Q8). */\n readonly occlusion?: (point: Vec3) => number\n readonly createContext?: () => AudioContext | null\n /** Build voices in timed slices between frames instead of inside `play` (a sound starts a few ms late; a burst never costs a frame). Off by default. */\n readonly deferredStart?: StartQueueOptions\n}\n\nexport type AudioEngine = {\n readonly registry: SoundRegistry\n register(bank: SoundBank): void\n /** Pure: the plan a sound would play with, from `options.seed` or the engine seed; draws nothing from the live streams. */\n plan(id: string, options?: PlayOptions): SoundPlan | null\n play(id: string, options?: PlayOptions): VoiceHandle | null\n playPlan(plan: SoundPlan, options?: { readonly delay?: number }): VoiceHandle | null\n loop(id: string, options?: { readonly volume?: number }): AudioLoopHandle\n channel(options: { readonly seed?: number; readonly random?: () => number; readonly ownNoise?: boolean }): AudioChannel\n setListener(listener: Listener): void\n setBus(id: BusId, setting: BusSetting): void\n setMix(mix: string | MixProfile): void\n /** The master volume of the current chain and of any later one (0 to 1). */\n setMaster(gain: number): void\n /** Creates the context and the mixing chain if needed and resumes it; call from a click or a key. */\n unlock(): void\n /** Creates the context, the mixing chain (reverb impulse included) and the shared noise buffer now, so the first sound does not pay for them inside a frame. Returns false without a context. */\n warmUp(): boolean\n stats(): AudioStats\n /** RMS (0 to 1) of what a bus carries now, read from an analyser the first time you ask; sounds started before that do not reach it. 0 without a context. */\n level(bus: BusId): number\n dispose(): void\n}\n\nconst usesNoise = (recipe: SoundRecipe): boolean => recipe.layers.some((layer) => layer.source.kind !== 'osc')\nconst planUsesNoise = (plan: SoundPlan): boolean => plan.layers.some((layer) => layer.source.kind !== 'osc')\n\n/**\n * The sound engine: plans recipes and builds them on a lazily created AudioContext through one of the team mixing chains. Without an AudioContext\n * (Node, workers) every call except `plan` does nothing.\n */\nexport function createAudioEngine(options: AudioEngineOptions = {}): AudioEngine {\n const seed = options.seed ?? 1\n const registry = createSoundRegistry(options.banks)\n const voices = createVoiceManager({ ...(options.groups ? { groups: options.groups } : {}), ...(options.maxVoices === undefined ? {} : { maxVoices: options.maxVoices }) })\n const engineRandom = createRng(seed)\n const shared = createNoiseStore(engineRandom)\n const busSettings = new Map<BusId, BusSetting>()\n const busNodes = new Map<BusId, GainNode>()\n const meters = new Map<BusId, { readonly analyser: AnalyserNode; readonly data: Float32Array<ArrayBuffer> }>()\n const active = new Map<number, RealizedVoice>()\n const loops = new Set<{ rewire(): void; stop(): void; readonly created: boolean }>()\n const named = new Map<string, () => number>()\n let mix = resolveMix(options.mix ?? DEFAULT_MIX, options.profiles)\n let context: AudioContext | null = null\n let chain: MixChain | null = null\n let listener: Listener = DEFAULT_LISTENER\n let lastTime = 0\n\n const outputFor = (bus: BusId): AudioNode => (busNodes.get(bus) ?? chain?.master) as AudioNode\n\n // a bus somebody asked the level of gets an analyser as soon as it has a node, so sounds started before the first reading are metered too\n const watched = new Set<BusId>()\n const meterOf = (bus: BusId): { readonly analyser: AnalyserNode; readonly data: Float32Array<ArrayBuffer> } | undefined => {\n const existing = meters.get(bus)\n if (existing) return existing\n const node = busNodes.get(bus)\n if (!context || !node || typeof context.createAnalyser !== 'function') return undefined\n const analyser = context.createAnalyser()\n analyser.fftSize = 1024\n node.connect(analyser)\n const made = { analyser, data: new Float32Array(analyser.fftSize) }\n meters.set(bus, made)\n return made\n }\n\n const ensureBus = (bus: BusId): void => {\n if (!context || !chain || busNodes.has(bus)) return\n const node = context.createGain()\n node.connect(chain.master)\n busNodes.set(bus, node)\n if (watched.has(bus)) meterOf(bus)\n }\n\n const ready = (): AudioContext | null => {\n if (context) return context\n const created = (options.createContext ?? globalContext)()\n if (!created) return null\n context = created\n chain = buildChain(created, mix, engineRandom)\n if (mix.noise.create === 'eager') noiseBuffer(shared, created, mix.noise.seconds)\n for (const [bus, setting] of Object.entries(mix.buses ?? {}) as [BusId, BusSetting][]) {\n busSettings.set(bus, { ...setting, ...busSettings.get(bus) })\n }\n for (const [bus, setting] of busSettings) {\n ensureBus(bus)\n ;(busNodes.get(bus) as GainNode).gain.value = busGain(setting)\n }\n lastTime = created.currentTime\n return created\n }\n\n const advance = (): void => {\n if (!context) return\n const elapsed = context.currentTime - lastTime\n lastTime = context.currentTime\n if (elapsed > 0) for (const id of voices.advance(elapsed)) active.delete(id)\n }\n\n const inputFor = (recipe: SoundRecipe, playOptions: PlayOptions, random: () => number): PlanInput => ({\n random,\n listener,\n noiseSeconds: mix.noise.seconds,\n ...(options.distance ? { distanceMode: options.distance } : {}),\n ...(playOptions.volume === undefined ? {} : { volume: playOptions.volume }),\n ...(playOptions.distance === undefined ? {} : { distance: playOptions.distance }),\n ...(playOptions.position ? { position: playOptions.position } : {}),\n ...(playOptions.params ? { params: playOptions.params } : {}),\n ...(options.occlusion && playOptions.position && recipe.falloff?.model !== 'none' ? { occlusion: options.occlusion(playOptions.position) } : {}),\n })\n\n const startQueue = options.deferredStart ? createStartQueue(options.deferredStart) : null\n\n const build = (plan: SoundPlan, store: NoiseStore, delay: number): RealizedVoice => {\n const audio = context as AudioContext\n return realize(plan, {\n context: audio,\n output: outputFor(plan.bus),\n ...(chain?.wet ? { wet: chain.wet } : {}),\n noise: () => noiseBuffer(store, audio, mix.noise.seconds),\n time: audio.currentTime + delay,\n })\n }\n\n // the voice a deferred sound is tracked by until it is built: `stop` cancels it or stops what was built\n const deferVoice = (plan: SoundPlan, store: NoiseStore, voiceId: number | undefined, delay: number): RealizedVoice => {\n let built: RealizedVoice | null = null\n let cancelled = false\n const pending: RealizedVoice = {\n get sources() {\n return built?.sources ?? []\n },\n stop() {\n cancelled = true\n built?.stop()\n },\n }\n ;(startQueue as ReturnType<typeof createStartQueue>).push({\n run() {\n if (!cancelled && context) built = build(plan, store, delay)\n },\n drop() {\n if (voiceId === undefined) return\n voices.release(voiceId)\n active.delete(voiceId)\n },\n })\n return pending\n }\n\n const start = (plan: SoundPlan, store: NoiseStore, voiceId: number | undefined, delay = 0): VoiceHandle | null => {\n const realized = startQueue ? deferVoice(plan, store, voiceId, delay) : build(plan, store, delay)\n if (voiceId === undefined) return null\n voices.setLength(voiceId, plan.length + delay)\n active.set(voiceId, realized)\n return {\n id: voiceId,\n plan,\n stop() {\n realized.stop()\n voices.release(voiceId)\n active.delete(voiceId)\n },\n }\n }\n\n const admit = (recipe: SoundRecipe, gain: number, distance: number): number | null => {\n const decision = voices.request({ ...(recipe.group ? { group: recipe.group } : {}), priority: recipe.priority ?? busPriority(recipe.bus), gain, distance, length: 0 })\n if (!decision.accepted) return null\n for (const id of decision.evicted) {\n active.get(id)?.stop()\n active.delete(id)\n }\n return decision.id\n }\n\n const playWith = (id: string, playOptions: PlayOptions, random: () => number, store: NoiseStore): VoiceHandle | null => {\n const recipe = registry.sound(id)\n if (!recipe || busSettings.get(recipe.bus)?.muted) return null\n const distance = playOptions.distance ?? (playOptions.position ? listenerDistance(listener, playOptions.position, options.distance) : 0)\n const level = falloffGain(recipe.falloff, playOptions.volume ?? 1, distance)\n const silent = isSilent(recipe.falloff, level)\n // a sound too quiet to play does not even start the context (the team routines return before touching it)\n if (silent && !recipe.keepSilent) return null\n const audio = ready()\n if (!audio) return null\n unlockContext(audio)\n advance()\n const voiceId = silent ? undefined : admit(recipe, level, distance)\n if (voiceId === null) return null\n if (!silent && usesNoise(recipe)) prepareNoise(store, audio, mix.noise.seconds)\n const plan = planSound(recipe, inputFor(recipe, playOptions, playOptions.seed === undefined ? random : createRng(playOptions.seed)))\n return plan ? start(plan, store, voiceId, playOptions.delay) : null\n }\n\n const randomFor = (playOptions: PlayOptions): (() => number) => {\n if (playOptions.channel === undefined) return engineRandom\n const existing = named.get(playOptions.channel)\n if (existing) return existing\n const stream = streamFor(seed, playOptions.channel)\n named.set(playOptions.channel, stream)\n return stream\n }\n\n const planWith = (id: string, playOptions: PlayOptions, random: () => number): SoundPlan | null => {\n const recipe = registry.sound(id)\n return recipe ? planSound(recipe, inputFor(recipe, playOptions, random)) : null\n }\n\n const loopHost: LoopHost = {\n ready,\n output: outputFor,\n prepareNoise: () => context && prepareNoise(shared, context, mix.noise.seconds),\n noise: () => noiseBuffer(shared, context as AudioContext, mix.noise.seconds),\n register: (loop) => void loops.add(loop),\n unregister: (loop) => void loops.delete(loop),\n }\n\n return {\n registry,\n register: (bank) => registry.register(bank),\n plan: (id, playOptions = {}) => planWith(id, playOptions, createRng(playOptions.seed ?? seed)),\n play: (id, playOptions = {}) => playWith(id, playOptions, randomFor(playOptions), shared),\n playPlan(plan, playOptions = {}) {\n const audio = ready()\n if (!audio) return null\n advance()\n const recipe = { id: plan.id, bus: plan.bus, layers: [], sources: [], priority: plan.priority, ...(plan.group ? { group: plan.group } : {}) }\n const voiceId = plan.silent ? undefined : admit(recipe, plan.level, plan.distance)\n if (voiceId === null) return null\n if (planUsesNoise(plan)) prepareNoise(shared, audio, mix.noise.seconds)\n return start(plan, shared, voiceId, playOptions.delay)\n },\n loop: (id, loopOptions = {}) => createLoopHandle(registry.loop(id), loopHost, loopOptions.volume),\n channel(channelOptions) {\n const random = channelOptions.random ?? (channelOptions.seed === undefined ? engineRandom : createRng(channelOptions.seed))\n const store = channelOptions.ownNoise ? createNoiseStore(random) : shared\n return {\n random,\n play: (id, playOptions = {}) => playWith(id, playOptions, random, store),\n plan: (id, playOptions = {}) => planWith(id, playOptions, createRng(playOptions.seed ?? seed)),\n }\n },\n setListener(next) {\n listener = next\n },\n setBus(id, setting) {\n busSettings.set(id, { ...busSettings.get(id), ...setting })\n if (!context) return\n ensureBus(id)\n ;(busNodes.get(id) as GainNode).gain.value = busGain(busSettings.get(id))\n },\n setMix(next) {\n const previous = mix\n mix = resolveMix(next, options.profiles)\n if (!context || !chain) return\n const old = chain\n chain = buildChain(context, mix, engineRandom)\n if (mix.noise.seconds !== previous.noise.seconds) shared.reset()\n for (const node of busNodes.values()) {\n node.disconnect()\n node.connect(chain.master)\n }\n for (const loop of loops) loop.rewire()\n old.disconnect()\n },\n setMaster(gain) {\n mix = { ...mix, master: gain }\n if (chain) chain.master.gain.value = gain\n },\n unlock() {\n const audio = ready()\n if (audio) unlockContext(audio)\n },\n warmUp() {\n const audio = ready()\n if (!audio) return false\n noiseBuffer(shared, audio, mix.noise.seconds)\n return true\n },\n level(bus) {\n watched.add(bus)\n ensureBus(bus)\n const meter = meterOf(bus)\n if (!meter) return 0\n meter.analyser.getFloatTimeDomainData(meter.data)\n let sum = 0\n for (const sample of meter.data) sum += sample * sample\n return Math.sqrt(sum / meter.data.length)\n },\n stats() {\n advance()\n const voiceStats = voices.stats()\n return {\n context: context ? context.state : 'none',\n voices: voiceStats.active,\n peak: voiceStats.peak,\n dropped: voiceStats.dropped,\n maxVoices: voiceStats.maxVoices,\n loops: [...loops].filter((loop) => loop.created).length,\n }\n },\n dispose() {\n // closing the context ends every voice; they only leave the limiter\n startQueue?.clear()\n for (const id of active.keys()) voices.release(id)\n for (const loop of [...loops]) loop.stop()\n active.clear()\n void context?.close()\n context = null\n chain = null\n busNodes.clear()\n meters.clear()\n shared.reset()\n },\n }\n}\n","import type { AudioListener, AudioSink, LoopHandle, SoundHandle, SoundRequest } from '@adatechnology/game-contracts'\n\nimport type { SoundBank } from '../recipe'\nimport type { AudioEngine, VoiceHandle } from './engine'\n\n/** An `AudioSink` (what a host lends its modules) that carries the engine behind it, so a module that wants more than `play` can use the engine. */\nexport type EngineSink = AudioSink & { readonly engine: AudioEngine }\n\n/** Lends an engine as the host's `audio`: sounds by key, loops, the listener, and banks registered into the same engine. */\nexport function createAudioSink(engine: AudioEngine): EngineSink {\n const playing = new Map<number, VoiceHandle>()\n return {\n engine,\n play(request: SoundRequest): SoundHandle | null {\n const handle = engine.play(request.sound, {\n ...(request.position ? { position: request.position } : {}),\n ...(request.volume === undefined ? {} : { volume: request.volume }),\n ...(request.params ? { params: request.params } : {}),\n ...(request.seed === undefined ? {} : { seed: request.seed }),\n })\n if (!handle) return null\n playing.set(handle.id, handle)\n return { id: handle.id }\n },\n stop(handle) {\n playing.get(handle.id)?.stop()\n playing.delete(handle.id)\n },\n loop(sound): LoopHandle {\n const loop = engine.loop(sound)\n return { set: (state) => loop.set({ ...(state.volume === undefined ? {} : { volume: state.volume }), ...(state.frequency === undefined ? {} : { frequency: state.frequency }) }), stop: () => loop.stop() }\n },\n setListener(listener: AudioListener) {\n engine.setListener(listener)\n },\n register(bank: unknown) {\n engine.register(bank as SoundBank)\n },\n }\n}\n","export type AudioBenchText = {\n readonly group: string\n readonly name: string\n readonly probe: string\n readonly controls: Readonly<Record<'sound' | 'distance' | 'bearing' | 'volume' | 'mix' | 'seed' | 'play' | 'voice_storm' | 'loop' | 'loop_level' | 'loop_pitch' | 'source', string>>\n readonly hints: Readonly<Record<'distance' | 'bearing' | 'mix' | 'seed' | 'voice_storm' | 'loop_pitch' | 'source', string>>\n readonly options: Readonly<Record<'none' | 'bench' | 'probe', string>>\n readonly layers: Readonly<Record<'reverb' | 'compressor' | 'panning' | 'muffling', string>>\n readonly metrics: Readonly<Record<'voices' | 'peak' | 'dropped' | 'loops' | 'context' | 'plan_length' | 'plan_level' | 'plan_pan' | 'level_weapons' | 'level_voices' | 'level_world' | 'level_ambience' | 'level_weather' | 'level_ui', string>>\n readonly scenarios: Readonly<Record<'panning' | 'distance' | 'determinism' | 'limiter', string>>\n readonly mixes: Readonly<Record<string, string>>\n /** Text of a sound or loop id when the game has one; the bench falls back to the id. */\n readonly sheet: Readonly<Record<'title' | 'level' | 'distance' | 'pan' | 'length' | 'silent' | 'layer' | 'none', string>>\n}\n\nconst EN: AudioBenchText = {\n group: 'Sound',\n name: 'Sound area',\n probe: 'Sound probe',\n controls: {\n sound: 'Sound',\n distance: 'Distance',\n bearing: 'Bearing',\n volume: 'Volume',\n mix: 'Mixing chain',\n seed: 'Seed',\n play: 'Play',\n voice_storm: 'Voice storm',\n loop: 'Loop',\n loop_level: 'Loop level',\n loop_pitch: 'Loop pitch',\n source: 'Source',\n },\n hints: {\n distance: 'Metres from the listener: the level falls and the sound is muffled with it.',\n bearing: 'Degrees from straight ahead, positive to the right: shows the panning.',\n mix: 'The game chain (master only) or the art scene chain (compressor and reverb).',\n seed: 'The same seed gives the same sound.',\n voice_storm: 'Twenty of the chosen sound at once: the voice limiter drops what its group and the global cap refuse.',\n loop_pitch: 'Frequency of an oscillator loop, in hertz (0 leaves it where it is).',\n source: 'Where the sound comes from: the distance and bearing set here, or the probe standing in the area.',\n },\n options: { none: 'None', bench: 'Bench settings', probe: 'The probe' },\n layers: { reverb: 'Reverb', compressor: 'Compressor', panning: 'Panning', muffling: 'Muffling by distance' },\n metrics: {\n voices: 'Voices playing',\n peak: 'Most voices at once',\n dropped: 'Voices dropped',\n loops: 'Loops running',\n context: 'Audio context',\n plan_length: 'Chosen sound: length',\n plan_level: 'Chosen sound: level',\n plan_pan: 'Chosen sound: pan',\n level_weapons: 'Level: weapons',\n level_voices: 'Level: voices',\n level_world: 'Level: world',\n level_ambience: 'Level: ambience',\n level_weather: 'Level: weather',\n level_ui: 'Level: interface',\n },\n scenarios: {\n panning: 'A sound to the right is heard on the right, one to the left on the left, one ahead in the middle',\n distance: 'The farther a sound, the quieter, until it is not played',\n determinism: 'The same seed gives the same sound plan, another seed another one',\n limiter: 'The voice limiter holds a storm of voices to its group rule and its global cap',\n },\n mixes: { game: 'Game', art_scene: 'Art scene' },\n sheet: { title: 'Sound plan', level: 'level', distance: 'distance', pan: 'pan', length: 'seconds', silent: 'too far to be heard', layer: 'layer', none: 'no sound chosen' },\n}\n\nconst PT_BR: AudioBenchText = {\n group: 'Som',\n name: 'Área de som',\n probe: 'Sonda de som',\n controls: {\n sound: 'Som',\n distance: 'Distância',\n bearing: 'Direção',\n volume: 'Volume',\n mix: 'Cadeia de mixagem',\n seed: 'Semente',\n play: 'Tocar',\n voice_storm: 'Tempestade de vozes',\n loop: 'Laço',\n loop_level: 'Nível do laço',\n loop_pitch: 'Altura do laço',\n source: 'Origem',\n },\n hints: {\n distance: 'Metros até o ouvinte: o nível cai e o som abafa junto.',\n bearing: 'Graus a partir da frente, positivo para a direita: mostra o pan.',\n mix: 'A cadeia do jogo (só o master) ou a da Cena de Arte (compressor e reverb).',\n seed: 'A mesma semente dá o mesmo som.',\n voice_storm: 'Vinte do som escolhido de uma vez: o limite de vozes derruba o que a regra do grupo e o teto global recusam.',\n loop_pitch: 'Frequência de um laço de oscilador, em hertz (0 deixa onde está).',\n source: 'De onde vem o som: a distância e a direção daqui, ou a sonda que está na área.',\n },\n options: { none: 'Nenhum', bench: 'Ajustes da bancada', probe: 'A sonda' },\n layers: { reverb: 'Reverb', compressor: 'Compressor', panning: 'Pan', muffling: 'Abafamento pela distância' },\n metrics: {\n voices: 'Vozes tocando',\n peak: 'Mais vozes de uma vez',\n dropped: 'Vozes derrubadas',\n loops: 'Laços ligados',\n context: 'Contexto de áudio',\n plan_length: 'Som escolhido: duração',\n plan_level: 'Som escolhido: nível',\n plan_pan: 'Som escolhido: pan',\n level_weapons: 'Nível: armas',\n level_voices: 'Nível: vozes',\n level_world: 'Nível: mundo',\n level_ambience: 'Nível: ambiente',\n level_weather: 'Nível: clima',\n level_ui: 'Nível: interface',\n },\n scenarios: {\n panning: 'Um som à direita sai à direita, à esquerda sai à esquerda, à frente sai no meio',\n distance: 'Quanto mais longe, mais baixo, até não tocar',\n determinism: 'A mesma semente dá o mesmo plano de som, outra semente dá outro',\n limiter: 'O limite de vozes segura uma tempestade de vozes na regra do grupo e no teto global',\n },\n mixes: { game: 'Jogo', art_scene: 'Cena de Arte' },\n sheet: { title: 'Plano do som', level: 'nível', distance: 'distância', pan: 'pan', length: 'segundos', silent: 'longe demais para ouvir', layer: 'camada', none: 'nenhum som escolhido' },\n}\n\nexport const audioBenchTextOf = (locale: string): AudioBenchText => (locale === 'en' ? EN : PT_BR)\n","import type { AreaPlacement, BenchChange, ChoiceOption, ModuleControl, ModuleHost, ModuleLayer, ModuleMetric, ModuleScenario, TestableItem, TestableModule, Vec3 } from '@adatechnology/game-contracts'\n\nimport { GAME_SFX_BANK } from '../recipes/noise-class'\nimport { LIBRARY_BANK } from '../library'\nimport { DISTANCE_CURVES } from '../falloff'\nimport { MIX_PROFILES } from '../mix'\nimport type { MixProfile } from '../mix'\nimport { BUS_IDS } from '../recipe'\nimport type { BusId, Layer, SoundBank } from '../recipe'\nimport type { SoundPlan } from '../plan'\nimport { createVoiceManager } from '../voices'\nimport { createAudioEngine } from '../webaudio/engine'\nimport type { AudioEngine, VoiceHandle } from '../webaudio/engine'\nimport type { EngineSink } from '../webaudio/sink'\nimport type { AudioLoopHandle } from '../webaudio/engine-loop'\nimport { audioBenchTextOf } from './locale'\nimport type { AudioBenchText } from './locale'\n\n/**\n * Two probe sounds of the bench itself (a ping and a hiss), so panning, distance and the seed can be heard on something that has no subject. They are\n * made by the kit, not delivered by any team (`sdk` in the sources).\n */\nexport const BENCH_BANK: SoundBank = {\n id: 'audio_bench',\n sounds: [\n {\n id: 'bench_ping',\n bus: 'ui',\n pan: true,\n duration: 0.25,\n layers: [\n {\n gain: 0.5,\n wet: 0.3,\n source: { kind: 'osc', wave: 'sine', frequency: { from: 880 } },\n envelope: [\n [0, 0.0001, 'set', 'fixed'],\n [0.002, 1, 'exp'],\n ['1d', 0.0001, 'exp', 'fixed'],\n ],\n } satisfies Layer,\n ],\n falloff: DISTANCE_CURVES.art_scene,\n sources: ['sdk: bench probe, a 880 Hz ping through the art scene output stage'],\n },\n {\n id: 'bench_hiss',\n bus: 'ui',\n pan: true,\n duration: [0.3, 0.9],\n layers: [\n {\n gain: 0.4,\n source: { kind: 'noise', offset: 'random', offsetScale: 1.5, loop: true },\n filters: { mode: 'serial', bank: [{ type: 'highpass', frequency: { set: 3000 }, q: 0.7 }] },\n envelope: [\n [0, 0.0001, 'set', 'fixed'],\n [0.002, 1, 'exp'],\n ['1d', 0.0001, 'exp', 'fixed'],\n ],\n } satisfies Layer,\n ],\n falloff: DISTANCE_CURVES.art_scene,\n sources: ['sdk: bench probe, a highpass hiss of random length'],\n },\n ],\n}\n\nexport type AudioTestModuleOptions = {\n /** The engine to test (default: the one the host lends in `host.audio`, else a new one of the module's own). */\n engine?: AudioEngine\n /** Banks registered in the engine besides the bench's probes (default the game's noise classes and the library). */\n banks?: readonly SoundBank[]\n locale?: string\n /** Text of a sound or loop id (`sound.glass`) in the game's words; without it the bench shows the bank and the id. */\n translate?: (key: string, locale: string) => string | undefined\n seed?: number\n /** First sound chosen (default `bench_ping`). */\n sound?: string\n /**\n * Draws the thing the probe stands for, where the sound comes from with the `probe` source (for example a portable radio of `game-items-three`). The module\n * draws nothing of its own: without it the area has no probe and the sounds come from the bench settings.\n */\n probe?: (THREE: unknown) => unknown\n}\n\n/** What the scenarios use of the context the test room passes. */\nexport interface AudioScenarioLike {\n expect(condition: boolean, label: string, data?: Record<string, unknown>): void\n}\n\nexport type AudioTestModule = TestableModule<AudioScenarioLike> & {\n /** The engine in use: the host's when it lent one, the module's own otherwise. */\n readonly engine: AudioEngine\n /** The resolved plan of the chosen sound as text (the S3 view: every number decided). */\n sheet(): string\n /** Plays the chosen sound as the bench is set. */\n play(): VoiceHandle | null\n}\n\nconst DISTANCES = [0, 10, 40, 60, 120, 160] as const\nconst STORM = 20\nconst PROBE_ID = 'audio_probe'\nconst PROBE_AT: Vec3 = [0, 0, -10]\nconst NONE = 'none'\nconst DEFAULT_SOUND = 'bench_ping'\n\ntype Camera = { readonly position: { readonly x: number; readonly y: number; readonly z: number }; getWorldDirection(target: unknown): { readonly x: number; readonly z: number } }\ntype ThreeLike = { Vector3: new () => unknown }\ntype ProbeObject = { userData?: Record<string, unknown>; getWorldPosition(target: unknown): { readonly x: number; readonly y: number; readonly z: number }; traverse(visit: (object: ProbeObject) => void): void }\n\nconst variantOf = (profile: MixProfile, reverb: boolean, compressor: boolean): MixProfile => {\n const { reverb: wet, compressor: chain, ...rest } = profile\n return { ...rest, id: `${profile.id}${reverb ? '' : '+dry'}${compressor ? '' : '+open'}`, ...(wet && reverb ? { reverb: wet } : {}), ...(chain && compressor ? { compressor: chain } : {}) }\n}\n\n/**\n * The sound area of a test room, written against the generic contracts only: it plays any sound of an engine at a distance and bearing from the listener\n * (or from a probe standing in the area), runs loops, throws a storm of voices at the limiter, switches the mixing chain and its parts, reads the\n * level of each bus and prints the resolved plan. The engine is the host's (`host.audio`) when lent, else the module's own.\n */\nexport function createAudioTestModule(options: AudioTestModuleOptions = {}): AudioTestModule {\n const locale = options.locale ?? 'pt-BR'\n const text: AudioBenchText = audioBenchTextOf(locale)\n const banks = [BENCH_BANK, ...(options.banks ?? [GAME_SFX_BANK, LIBRARY_BANK])]\n let engine: AudioEngine = options.engine ?? createAudioEngine({ seed: options.seed ?? 1, banks })\n for (const bank of banks) engine.register(bank)\n let host: ModuleHost | null = null\n let loop: AudioLoopHandle | undefined\n const state = {\n sound: options.sound ?? DEFAULT_SOUND,\n distance: 10,\n bearing: 0,\n volume: 1,\n mix: 'art_scene',\n seed: options.seed ?? 1,\n source: 'bench',\n loop: NONE,\n loopLevel: 0.1,\n loopPitch: 0,\n reverb: true,\n compressor: true,\n panning: true,\n muffling: true,\n }\n\n const nameOf = (id: string): string => {\n const translated = options.translate?.(`sound.${id}`, locale)\n if (translated) return translated\n const bank = engine.registry.bankOf(id)\n return bank ? `${bank} / ${id}` : id\n }\n const soundOptions = (): ChoiceOption[] => engine.registry.soundIds().map((id) => ({ id, name: nameOf(id) }))\n const loopOptions = (): ChoiceOption[] => [{ id: NONE, name: text.options.none }, ...engine.registry.loopIds().map((id) => ({ id, name: nameOf(id) }))]\n\n // the listener is the host camera; without a host (a test) it stands at the origin looking along -z\n const listener = (): { position: Vec3; yaw: number } => {\n const camera = host?.camera as Camera | undefined\n const three = host?.THREE as ThreeLike | undefined\n if (typeof camera?.getWorldDirection !== 'function' || typeof three?.Vector3 !== 'function' || !camera.position) return { position: [0, 0, 0], yaw: 0 }\n const direction = camera.getWorldDirection(new three.Vector3())\n return { position: [camera.position.x, camera.position.y, camera.position.z], yaw: Math.atan2(-direction.x, -direction.z) }\n }\n\n const probePosition = (): Vec3 | undefined => {\n const scene = host?.scene as ProbeObject | undefined\n const three = host?.THREE as ThreeLike | undefined\n if (typeof scene?.traverse !== 'function' || typeof three?.Vector3 !== 'function') return undefined\n let found: Vec3 | undefined\n scene.traverse((object) => {\n if (found || object.userData?.[PROBE_ID] !== true) return\n const at = object.getWorldPosition(new three.Vector3())\n found = [at.x, at.y, at.z]\n })\n return found\n }\n\n // `bearing` is degrees to the right of straight ahead\n const sourceAt = (ears: { position: Vec3; yaw: number }): Vec3 => {\n const probe = state.source === 'probe' ? probePosition() : undefined\n if (probe) return probe\n const bearing = (state.bearing * Math.PI) / 180\n const forward = [-Math.sin(ears.yaw), -Math.cos(ears.yaw)] as const\n const right = [Math.cos(ears.yaw), -Math.sin(ears.yaw)] as const\n return [ears.position[0] + state.distance * (forward[0] * Math.cos(bearing) + right[0] * Math.sin(bearing)), ears.position[1], ears.position[2] + state.distance * (forward[1] * Math.cos(bearing) + right[1] * Math.sin(bearing))]\n }\n\n const playOptions = (): { position?: Vec3; distance?: number; volume: number; seed: number } => {\n const ears = listener()\n engine.setListener(ears)\n const at = sourceAt(ears)\n const distance = Math.hypot(at[0] - ears.position[0], at[2] - ears.position[2])\n return { ...(state.panning ? { position: at } : { distance }), volume: state.volume, seed: state.seed }\n }\n\n // no muffling: the recipe's cutoff divisor is switched off for the one call\n const withMuffling = <T>(id: string, run: () => T): T => {\n const falloff = engine.registry.sound(id)?.falloff\n if (state.muffling || !falloff || falloff.model !== 'inverse_square' || falloff.cutoffK === undefined) return run()\n engine.registry.patch(id, { falloff: { cutoffK: 0 } })\n try {\n return run()\n } finally {\n engine.registry.patch(id, { falloff: { cutoffK: falloff.cutoffK } })\n }\n }\n\n const play = (): VoiceHandle | null => {\n engine.unlock()\n const chosen = state.sound\n return withMuffling(chosen, () => engine.play(chosen, playOptions()))\n }\n\n const plan = (): SoundPlan | null => {\n const chosen = state.sound\n return withMuffling(chosen, () => engine.plan(chosen, playOptions()))\n }\n\n const sheet = (): string => {\n const planned = plan()\n if (!planned) return `${text.sheet.title}: ${engine.registry.sound(state.sound) ? text.sheet.silent : text.sheet.none}`\n const head = `${text.sheet.title}: ${planned.id} (${planned.bus}) ${text.sheet.level} ${planned.level.toFixed(4)} ${text.sheet.distance} ${planned.distance.toFixed(1)} m` + `${planned.pan === undefined ? '' : ` ${text.sheet.pan} ${planned.pan.toFixed(3)}`} ${planned.length.toFixed(3)} ${text.sheet.length}`\n const layers = planned.layers.map((layer, index) => {\n const source = layer.source.kind === 'noise' ? `noise@${layer.source.offset.toFixed(4)}` : layer.source.kind === 'osc' ? `${layer.source.wave}@${layer.source.frequency.value ?? layer.source.frequency.from}` : layer.source.kind\n const filters = (layer.filters?.bank ?? []).map((filter) => `${filter.type}@${(filter.frequency.value ?? filter.frequency.from ?? 0).toFixed(1)}`).join(' ')\n const envelope = layer.envelope.map((point) => `${point.time.toFixed(3)}:${point.value.toPrecision(3)}`).join(' ')\n return ` ${text.sheet.layer} ${index + 1}: ${source} at ${layer.at.toFixed(3)} for ${layer.duration.toFixed(3)} ${filters} [${envelope}]`\n })\n return [head, ...layers].join('\\n')\n }\n\n const applyMix = (): void => {\n const profile = engine && MIX_PROFILES[state.mix]\n if (profile) engine.setMix(variantOf(profile, state.reverb, state.compressor))\n }\n\n const setLoop = (): void => {\n loop?.stop()\n loop = undefined\n if (state.loop === NONE) return\n engine.unlock()\n loop = engine.loop(state.loop)\n loop.set({ volume: state.loopLevel, ...(state.loopPitch > 0 ? { frequency: state.loopPitch } : {}) })\n }\n\n const storm = (): void => {\n engine.unlock()\n const base = playOptions()\n const chosen = state.sound\n for (let index = 0; index < STORM; index++) {\n const spread = base.position ? { position: [base.position[0] + (index - STORM / 2), base.position[1], base.position[2]] as Vec3 } : {}\n withMuffling(chosen, () => engine.play(chosen, { ...base, ...spread, seed: state.seed + index }))\n }\n }\n\n const onChange = (change: BenchChange): void => {\n if (change.kind === 'layer') {\n if (change.id === 'reverb' || change.id === 'compressor') {\n state[change.id] = change.on\n applyMix()\n } else if (change.id === 'panning' || change.id === 'muffling') state[change.id] = change.on\n return\n }\n if (change.kind === 'action') {\n if (change.id === 'play') play()\n else if (change.id === 'voice_storm') storm()\n return\n }\n const { id, value } = change\n if (id === 'sound' && typeof value === 'string') state.sound = value\n else if (id === 'distance' && typeof value === 'string') state.distance = Number(value)\n else if (id === 'bearing' && typeof value === 'number') state.bearing = value\n else if (id === 'volume' && typeof value === 'number') state.volume = value\n else if (id === 'seed' && typeof value === 'number') state.seed = value\n else if (id === 'source' && typeof value === 'string') state.source = value\n else if (id === 'mix' && typeof value === 'string') {\n state.mix = value\n applyMix()\n } else if (id === 'loop' && typeof value === 'string') {\n state.loop = value\n setLoop()\n } else if (id === 'loop_level' && typeof value === 'number') {\n state.loopLevel = value\n loop?.set({ volume: value })\n } else if (id === 'loop_pitch' && typeof value === 'number') {\n state.loopPitch = value\n if (value > 0) loop?.set({ frequency: value })\n }\n }\n\n const items = (): TestableItem[] => {\n const probe = options.probe\n if (!probe) return []\n return [\n {\n id: PROBE_ID,\n name: text.probe,\n movable: false,\n build: (THREE) => {\n const root = probe(THREE) as { userData?: Record<string, unknown> }\n root.userData = root.userData ?? {}\n root.userData[PROBE_ID] = true\n return root\n },\n },\n ]\n }\n\n const controls = (): ModuleControl[] => [\n { kind: 'choice', id: 'sound', name: text.controls.sound, options: soundOptions(), default: state.sound },\n { kind: 'choice', id: 'source', name: text.controls.source, hint: text.hints.source, options: [{ id: 'bench', name: text.options.bench }, { id: 'probe', name: text.options.probe }], default: state.source },\n { kind: 'choice', id: 'distance', name: text.controls.distance, hint: text.hints.distance, options: DISTANCES.map((meters) => ({ id: String(meters), name: `${meters} m` })), default: String(state.distance) },\n { kind: 'number', id: 'bearing', name: text.controls.bearing, hint: text.hints.bearing, unit: '°', min: -180, max: 180, step: 15, default: state.bearing },\n { kind: 'number', id: 'volume', name: text.controls.volume, min: 0, max: 1, step: 0.05, default: state.volume },\n { kind: 'choice', id: 'mix', name: text.controls.mix, hint: text.hints.mix, options: Object.keys(MIX_PROFILES).map((id) => ({ id, name: text.mixes[id] ?? id })), default: state.mix },\n { kind: 'number', id: 'seed', name: text.controls.seed, hint: text.hints.seed, min: 1, max: 9999, step: 1, default: state.seed },\n { kind: 'action', id: 'play', name: text.controls.play },\n { kind: 'action', id: 'voice_storm', name: text.controls.voice_storm, hint: text.hints.voice_storm },\n { kind: 'choice', id: 'loop', name: text.controls.loop, options: loopOptions(), default: state.loop },\n { kind: 'number', id: 'loop_level', name: text.controls.loop_level, min: 0, max: 0.6, step: 0.01, default: state.loopLevel },\n { kind: 'number', id: 'loop_pitch', name: text.controls.loop_pitch, hint: text.hints.loop_pitch, unit: 'Hz', min: 0, max: 400, step: 5, default: state.loopPitch },\n ]\n\n const layers = (): ModuleLayer[] => (['reverb', 'compressor', 'panning', 'muffling'] as const).map((id) => ({ id, name: text.layers[id] }))\n\n const metrics = (): ModuleMetric[] => [\n { id: 'voices', name: text.metrics.voices, digits: 0, read: () => engine.stats().voices },\n { id: 'peak', name: text.metrics.peak, digits: 0, max: engine.stats().maxVoices > 0 && Number.isFinite(engine.stats().maxVoices) ? engine.stats().maxVoices : undefined, read: () => engine.stats().peak },\n { id: 'dropped', name: text.metrics.dropped, digits: 0, read: () => engine.stats().dropped },\n { id: 'loops', name: text.metrics.loops, digits: 0, read: () => engine.stats().loops },\n { id: 'context', name: text.metrics.context, read: () => engine.stats().context },\n { id: 'plan_length', name: text.metrics.plan_length, unit: 's', digits: 2, read: () => plan()?.length ?? 0 },\n { id: 'plan_level', name: text.metrics.plan_level, digits: 3, read: () => plan()?.level ?? 0 },\n { id: 'plan_pan', name: text.metrics.plan_pan, digits: 2, read: () => plan()?.pan ?? 0 },\n ...BUS_IDS.map((bus) => ({ id: `level_${bus}`, name: text.metrics[`level_${bus}` as keyof AudioBenchText['metrics']], digits: 3, read: () => engine.level(bus as BusId) })),\n ]\n\n const scenarios = (): Array<ModuleScenario<AudioScenarioLike>> => [\n {\n id: 'panning',\n name: text.scenarios.panning,\n run: ({ expect }) => {\n engine.setListener({ position: [0, 0, 0], yaw: 0 })\n const pan = (at: Vec3): number => engine.plan('bench_ping', { position: at })?.pan ?? Number.NaN\n expect(pan([10, 0, 0]) > 0.3, 'a sound to the right pans right', { pan: pan([10, 0, 0]) })\n expect(pan([-10, 0, 0]) < -0.3, 'a sound to the left pans left', { pan: pan([-10, 0, 0]) })\n expect(Math.abs(pan([0, 0, -10])) < 0.05, 'a sound ahead stays in the middle', { pan: pan([0, 0, -10]) })\n engine.setListener({ position: [0, 0, 0], yaw: Math.PI / 2 })\n expect(pan([0, 0, -10]) > 0.3, 'turning the listener left puts what was ahead on the right', { pan: pan([0, 0, -10]) })\n engine.setListener(listener())\n },\n },\n {\n id: 'distance',\n name: text.scenarios.distance,\n run: ({ expect }) => {\n const levels = [0, 10, 40, 100].map((meters) => engine.plan('bench_ping', { distance: meters })?.level ?? Number.NaN)\n expect(levels.every((level, index) => index === 0 || level < (levels[index - 1] as number)), 'the level falls with the distance', { levels })\n const far = engine.registry.sound('sfx_pistol') ? engine.plan('sfx_pistol', { distance: 400 }) : null\n expect(far === null, 'a game noise class is not played too far to be heard')\n },\n },\n {\n id: 'determinism',\n name: text.scenarios.determinism,\n run: ({ expect }) => {\n const a = engine.plan('bench_hiss', { seed: 5 })\n const b = engine.plan('bench_hiss', { seed: 5 })\n const c = engine.plan('bench_hiss', { seed: 6 })\n expect(JSON.stringify(a) === JSON.stringify(b), 'the same seed gives the same plan')\n expect(JSON.stringify(a) !== JSON.stringify(c), 'another seed gives another plan')\n },\n },\n {\n id: 'limiter',\n name: text.scenarios.limiter,\n run: ({ expect }) => {\n const voices = createVoiceManager({ maxVoices: 8 })\n const request = (group?: string) => voices.request({ ...(group ? { group } : {}), priority: 3, gain: 0.5, distance: 5, length: 1 })\n const grouped = Array.from({ length: STORM }, () => request('undead_voice')).filter((decision) => decision.accepted).length\n expect(grouped === 1, 'the group cooldown lets one voice of a storm through', { accepted: grouped })\n const plain = Array.from({ length: STORM }, () => request()).filter((decision) => decision.accepted).length\n expect(plain <= 8, 'the global cap holds the rest', { accepted: plain })\n },\n },\n ]\n\n const area = (): { placements: AreaPlacement[]; view: { eye: Vec3; target: Vec3 } } => ({\n placements: options.probe ? [{ item: PROBE_ID, position: PROBE_AT, asleep: true }] : [],\n view: { eye: [0, 1.7, 3], target: [0, 1.4, -10] },\n })\n\n const module: AudioTestModule = {\n id: 'audio_area',\n name: text.name,\n ground: { surface: 'concrete_slab' },\n group: text.group,\n get engine() {\n return engine\n },\n items: () => items(),\n area: () => area(),\n controls: () => controls(),\n layers: () => layers(),\n metrics: () => metrics(),\n scenarios: () => scenarios(),\n sheet,\n play,\n attach(lent) {\n host = lent\n const shared = (lent.audio as EngineSink | undefined)?.engine\n if (shared && shared !== engine) {\n for (const bank of banks) shared.register(bank)\n engine = shared\n }\n // buses get their nodes now, so the level meters see every sound\n for (const bus of BUS_IDS) {\n engine.setBus(bus, {})\n engine.level(bus)\n }\n lent.bench?.patch('sound', { options: soundOptions() })\n lent.bench?.patch('loop', { options: loopOptions() })\n const off = lent.bench?.onChange(onChange)\n const offFrame = lent.onFrame(() => {\n if (!shared) engine.setListener(listener())\n })\n return () => {\n off?.()\n offFrame()\n loop?.stop()\n loop = undefined\n host = null\n }\n },\n }\n return module\n}\n","// Rain, wind and thunder as recipes of @adatechnology/game-audio. The loops are the teams' (the art scene's `initAudio`, cena L1314-L1315, and the game's\n// `chuva` loop, jogo L62116); the thunder is the game's `boom` (jogo L68278, a sine from 70 to 28 Hz) with a low rumble under it, the kit's choice.\nimport { sceneNoise, sceneTone } from '@adatechnology/game-audio'\nimport type { LoopRecipe, SoundBank, SoundRecipe } from '@adatechnology/game-audio'\n\n/** Rain of the art scene: noise through a highpass at 600 Hz and a lowpass at 6000 Hz (cena L1314), opened at its volume. */\nexport const RAIN_LOOP: LoopRecipe = {\n id: 'rain_loop',\n bus: 'weather',\n order: 'chain_loop',\n source: { kind: 'noise', loop: true },\n filters: {\n mode: 'serial',\n bank: [\n { type: 'highpass', frequency: { set: 600 } },\n { type: 'lowpass', frequency: { set: 6000 } },\n ],\n },\n sources: ['cena:L1314'],\n}\n\n/** Wind of the art scene: noise through a lowpass at 320 Hz whose gain swings 0.035 at 0.07 Hz (cena L1315). */\nexport const WIND_LOOP: LoopRecipe = {\n id: 'wind_loop',\n bus: 'weather',\n order: 'chain_loop',\n source: { kind: 'noise', loop: true },\n filters: { mode: 'serial', bank: [{ type: 'lowpass', frequency: { set: 320 } }] },\n modulators: [{ target: 'gain', rate: 0.07, depth: 0.035 }],\n sources: ['cena:L1315'],\n}\n\n/** Rain of the game: noise through a highpass at 2500 Hz, created when the volume first passes 0.002 (jogo L62116 `chuva`). */\nexport const RAIN_HISS_GAME: LoopRecipe = {\n id: 'rain_hiss_game',\n bus: 'weather',\n order: 'game_loop',\n source: { kind: 'noise', loop: true },\n filters: { mode: 'serial', bank: [{ type: 'highpass', frequency: { set: 2500 } }] },\n sources: ['jogo:L62116 chuva'],\n}\n\n/**\n * Thunder. The thump is the game's boom (a sine from 70 to 28 Hz over 1.2 s); the rumble is a lowpassed noise that slides from 260 to 50 Hz over 3.2 s\n * with a reverb send, the kit's choice (no team has a thunder). It carries much farther than a blast: `k` is 0.00001, so a strike 1 km away is still at\n * 0.13 and one 4 km away (the farthest the weather places) at 0.009.\n */\nexport const THUNDER: SoundRecipe = {\n id: 'thunder',\n bus: 'weather',\n pan: true,\n layers: [sceneTone({ f: 70, f2: 28, d: 1.2, vol: 1 }), sceneNoise({ f: 260, f2: 50, d: 3.2, vol: 0.9, wet: 0.8 })],\n falloff: { model: 'inverse_square', k: 0.00001, scale: 1.4 },\n sources: ['jogo:L68278-68285 boom', 'sdk: rumble and falloff'],\n}\n\nexport const WEATHER_BANK: SoundBank = { id: 'weather', sounds: [THUNDER], loops: [RAIN_LOOP, WIND_LOOP, RAIN_HISS_GAME] }\nexport const WEATHER_SOUNDS: readonly SoundRecipe[] = WEATHER_BANK.sounds ?? []\nexport const WEATHER_LOOPS: readonly LoopRecipe[] = WEATHER_BANK.loops ?? []\n","import type { AudioSink, LoopHandle, Vec3 } from '@adatechnology/game-contracts'\n\nimport type { Strike, WeatherSample } from '../types'\nimport { WEATHER_BANK } from './recipes'\n\nexport type WeatherSoundStyle = 'art_scene' | 'game'\n\nexport type WeatherSoundOptions = {\n /** Which team's rain: `art_scene` (default, the kit mix) or `game` (a highpass hiss). The wind and the thunder are the same. */\n readonly style?: WeatherSoundStyle\n /**\n * Level of the rain loop. It is `light` at `lightAt` mm/h (the game's rain, 0.035) and `heavy` from `heavyAt` mm/h (the art scene's, 0.06), a straight\n * line between, and 0 when it does not rain.\n */\n readonly rain?: { readonly light?: number; readonly heavy?: number; readonly lightAt?: number; readonly heavyAt?: number }\n /** Level of the wind loop: `base` (the art scene's 0.05) at `referenceSpeed` m/s, in proportion to the speed, at most `max`. */\n readonly wind?: { readonly base?: number; readonly referenceSpeed?: number; readonly max?: number }\n /** Where the listener is, to place the thunder around it (default the origin). */\n readonly listener?: () => Vec3\n /** Level of the thunder (default 1). */\n readonly thunderVolume?: number\n /** `false` keeps the sink's bank as it is (the host registered `WEATHER_BANK` itself). */\n readonly register?: boolean\n}\n\nexport const DEFAULT_RAIN_SOUND = { light: 0.035, heavy: 0.06, lightAt: 2.5, heavyAt: 15 } as const\n/** The art scene opens its wind at 0.05 with the breeze of its day (3.8 m/s, the `clear` state's wind): the level follows the speed from there. */\nexport const DEFAULT_WIND_SOUND = { base: 0.05, referenceSpeed: 3.8, max: 0.12 } as const\n\n/** The level of the rain loop for `rain` mm/h. */\nexport function rainVolume(rain: number, options: WeatherSoundOptions['rain'] = {}): number {\n if (!(rain > 0)) return 0\n const { light, heavy, lightAt, heavyAt } = { ...DEFAULT_RAIN_SOUND, ...options }\n if (heavyAt <= lightAt) return rain >= heavyAt ? heavy : light\n const t = Math.min(1, Math.max(0, (rain - lightAt) / (heavyAt - lightAt)))\n return light + (heavy - light) * t\n}\n\n/** The level of the wind loop for a wind of `speed` m/s. */\nexport function windVolume(speed: number, options: WeatherSoundOptions['wind'] = {}): number {\n const { base, referenceSpeed, max } = { ...DEFAULT_WIND_SOUND, ...options }\n return Math.min(max, (base * Math.max(0, speed)) / referenceSpeed)\n}\n\n/** Where a strike is, around `origin`: `bearing` is degrees clockwise from north, and north is -z. */\nexport function strikePosition(strike: Pick<Strike, 'distance' | 'bearing'>, origin: Vec3 = [0, 0, 0]): Vec3 {\n const angle = (strike.bearing * Math.PI) / 180\n return [origin[0] + Math.sin(angle) * strike.distance, origin[1], origin[2] - Math.cos(angle) * strike.distance]\n}\n\nexport type WeatherSound = {\n /** Sets the level of the loops from the weather of now (opens a loop the first time it is above 0). */\n update(sample: Pick<WeatherSample, 'rain' | 'windSpeed'>): void\n /** Plays a clap of thunder at the strike (call it when the thunder arrives: the weather delays it by `distance / 343`). */\n thunder(strike: Pick<Strike, 'distance' | 'bearing'>): void\n dispose(): void\n}\n\n/** The sound of the weather on the host's `audio`: the rain loop, the wind loop and the thunder. Without a sink (or one without loops) it does nothing. */\nexport function createWeatherSound(sink: AudioSink | undefined, options: WeatherSoundOptions = {}): WeatherSound {\n if (sink && options.register !== false) sink.register?.(WEATHER_BANK)\n const rainSound = options.style === 'game' ? 'rain_hiss_game' : 'rain_loop'\n const loops = new Map<string, LoopHandle>()\n const set = (id: string, volume: number): void => {\n let loop = loops.get(id)\n if (!loop) {\n if (!(volume > 0) || !sink?.loop) return\n loop = sink.loop(id)\n loops.set(id, loop)\n }\n loop.set({ volume })\n }\n return {\n update(sample) {\n set(rainSound, rainVolume(sample.rain, options.rain))\n set('wind_loop', windVolume(sample.windSpeed, options.wind))\n },\n thunder(strike) {\n sink?.play({ sound: 'thunder', position: strikePosition(strike, options.listener?.()), ...(options.thunderVolume === undefined ? {} : { volume: options.thunderVolume }) })\n },\n dispose() {\n for (const loop of loops.values()) loop.stop()\n loops.clear()\n },\n }\n}\n","import type { AudioSink, EnvironmentBus, EnvironmentState } from '@adatechnology/game-contracts'\n\nimport { createWeatherSound } from './audio/sound'\nimport type { WeatherSound, WeatherSoundOptions } from './audio/sound'\nimport type { Strike, WeatherSample } from './types'\nimport type { Weather } from './weather'\n\n/** The shape of the film of `game-fluids-three` (`FilmBridge`) this needs: no import, so the weather does not depend on it. */\nexport type FilmTarget = {\n setWeather(weather: { rain: number; drying?: number }): void\n wash?(maxDryness?: number): number\n}\n/** The interactables world (`game-interactables`): its water takes the rain, the soft volumes the wind. */\nexport type InteractablesTarget = { water: { setRain(mmPerHour: number): void }; setWind(x: number, z: number): void }\n/** The vegetation renderer (`game-vegetation`). `direction` is normalised by the renderer, `strength` and `gust` are 0..1. */\nexport type VegetationTarget = { setWind(wind: { direction?: [number, number]; strength?: number; gust?: number }): void }\n/** The effects world (`game-vfx`'s `VfxWorld`): smoke and dust drift with its wind. */\nexport type VfxTarget = { setWind(wind: readonly [number, number, number]): void }\n\nexport type WeatherTargets = {\n readonly film?: FilmTarget\n readonly interactables?: InteractablesTarget\n readonly vegetation?: VegetationTarget\n readonly vfx?: VfxTarget\n /** The host's environment bus (`ModuleHost.environment`). */\n readonly environment?: Pick<EnvironmentBus, 'set'>\n /** The host's sound (`ModuleHost.audio`): the rain and wind loops, and the thunder. */\n readonly audio?: AudioSink\n}\n\n/** The rules the bridges apply; every number can be replaced. */\nexport type WeatherBridgeRules = {\n /** Rain in mm/h at which the film counts as fully rained on (it stops drying, spreads 1.5 times faster). Default 2.5, the light rain. */\n readonly filmRainFullAt?: number\n /** Rain in mm/h from which the rain washes the fresh stain (Workshop and visor: heavy rain and stronger). Default 15; light rain does not wash. */\n readonly washFrom?: number\n /** Stains less dry than this are washed (visor L13494). Default 0.5. */\n readonly washBelow?: number\n /** Seconds between two washes while it pours. Default 1. */\n readonly washEvery?: number\n /** Wind speed in m/s that is 1 for the vegetation (`strength = min(1, speed / this)`). Default 20. */\n readonly windFull?: number\n}\n\nexport type WeatherBridgeOptions = WeatherBridgeRules & {\n readonly sound?: WeatherSoundOptions | false\n}\n\nexport const DEFAULT_BRIDGE_RULES = { filmRainFullAt: 2.5, washFrom: 15, washBelow: 0.5, washEvery: 1, windFull: 20 } as const\n\n/** The weather as the environment bus holds it. */\nexport function environmentOf(sample: WeatherSample): Partial<EnvironmentState> {\n return {\n rain: sample.rain,\n windX: sample.windX,\n windZ: sample.windZ,\n windSpeed: sample.windSpeed,\n gust: sample.gust,\n fogFar: sample.fogFar,\n visibility: sample.effects.visibility,\n nightFactor: sample.nightFactor,\n temperatureC: sample.temperatureC,\n wetness: sample.groundWetness,\n }\n}\n\nexport type WeatherBridges = {\n /** Follows this weather (the one before is let go) and pushes its values at once. */\n bind(weather: Weather): void\n /** Pushes the weather of now to every target; call it each frame after `weather.advance(dt)`. */\n update(dt: number): void\n dispose(): void\n}\n\n/**\n * Pushes one weather to the packages that react to it, each through a small interface of its own, so the weather depends on none of them and every\n * target is optional:\n * - fluids film: `setWeather({ rain: min(1, rain / filmRainFullAt), drying: dryingScale })`, and `wash()` every `washEvery` seconds while the rain is at\n * `washFrom` or stronger (light rain stops the drying and does not wash);\n * - interactables: `water.setRain(mm/h)` and `setWind(x, z)`;\n * - vegetation: `setWind({ direction: [x, z], strength: min(1, speed / windFull), gust: min(1, gust / windFull) })`;\n * - vfx: `setWind([x, 0, z])`;\n * - the environment bus: every value of `environmentOf`;\n * - sound: the rain and wind loops follow the weather, and each thunder that arrives plays at its bearing.\n */\nexport function createWeatherBridges(targets: WeatherTargets, options: WeatherBridgeOptions = {}): WeatherBridges {\n const rules = { ...DEFAULT_BRIDGE_RULES, ...options }\n const sound: WeatherSound | undefined = options.sound === false || !targets.audio ? undefined : createWeatherSound(targets.audio, options.sound ?? {})\n let weather: Weather | undefined\n let offThunder: (() => void) | undefined\n let sinceWash = 0\n let last: { rain?: number; windX?: number; windZ?: number; filmRain?: number; drying?: number } = {}\n\n const push = (): void => {\n if (!weather) return\n const sample = weather.sample()\n const { film, interactables, vegetation, vfx, environment } = targets\n environment?.set(environmentOf(sample))\n if (film) {\n const rain = Math.min(1, Math.max(0, sample.rain / rules.filmRainFullAt))\n const drying = sample.effects.dryingScale\n if (rain !== last.filmRain || drying !== last.drying) film.setWeather({ rain, drying })\n last.filmRain = rain\n last.drying = drying\n }\n if (interactables && sample.rain !== last.rain) interactables.water.setRain(sample.rain)\n const windChanged = sample.windX !== last.windX || sample.windZ !== last.windZ\n if (windChanged) {\n interactables?.setWind(sample.windX, sample.windZ)\n vfx?.setWind([sample.windX, 0, sample.windZ])\n if (vegetation) {\n const wind = { strength: Math.min(1, sample.windSpeed / rules.windFull), gust: Math.min(1, sample.gust / rules.windFull) }\n vegetation.setWind(sample.windSpeed > 1e-6 ? { direction: [sample.windX, sample.windZ], ...wind } : wind)\n }\n }\n last = { ...last, rain: sample.rain, windX: sample.windX, windZ: sample.windZ }\n sound?.update(sample)\n }\n\n const onThunder = (strike: Strike): void => sound?.thunder(strike)\n\n return {\n bind(next) {\n offThunder?.()\n weather = next\n offThunder = next.on('thunder', onThunder)\n last = {}\n sinceWash = 0\n push()\n },\n update(dt) {\n push()\n if (!weather || !targets.film?.wash) return\n if (weather.sample().rain < rules.washFrom) {\n sinceWash = 0\n return\n }\n sinceWash += dt\n if (sinceWash >= rules.washEvery) {\n sinceWash = 0\n targets.film.wash(rules.washBelow)\n }\n },\n dispose() {\n offThunder?.()\n offThunder = undefined\n weather = undefined\n sound?.dispose()\n },\n }\n}\n","export type WeatherBenchText = {\n readonly group: string\n readonly name: string\n readonly controls: Readonly<Record<'state' | 'transition' | 'time_of_day' | 'auto_schedule' | 'seed', string>>\n readonly hints: Readonly<Record<'transition' | 'time_of_day' | 'auto_schedule' | 'seed', string>>\n readonly options: Readonly<Record<'instant' | 'ten_seconds', string>>\n readonly layers: Readonly<Record<'environment' | 'fog', string>>\n readonly metrics: Readonly<Record<'state' | 'hour' | 'rain' | 'visibility' | 'sight' | 'hearing' | 'wind' | 'heat_index' | 'wetness' | 'strikes' | 'blend' | 'temperature', string>>\n readonly scenarios: Readonly<Record<'storm' | 'blend' | 'determinism' | 'dry_ground', string>>\n /** Names of the states and climates by id; the bench falls back to the id. */\n readonly states: Readonly<Record<string, string>>\n}\n\nconst EN: WeatherBenchText = {\n group: 'Weather',\n name: 'Weather area',\n controls: { state: 'Weather', transition: 'Transition', time_of_day: 'Time of day', auto_schedule: 'Automatic schedule', seed: 'Seed' },\n hints: {\n transition: 'How long the change to the chosen weather takes, in real seconds.',\n time_of_day: 'Hour of the day (the world restarts the clock there and keeps the weather).',\n auto_schedule: 'On: the climate changes the weather by itself and the clock runs 600 times faster than real time. Off: the weather stays until you change it.',\n seed: 'The same seed gives the same schedule and the same lightning.',\n },\n options: { instant: 'At once', ten_seconds: '10 seconds' },\n layers: { environment: 'Write host environment', fog: 'Fog in the scene' },\n metrics: {\n state: 'Weather',\n hour: 'Hour',\n rain: 'Rain',\n visibility: 'Visibility',\n sight: 'Sight scale',\n hearing: 'Hearing scale',\n wind: 'Wind',\n heat_index: 'Heat index',\n wetness: 'Ground wetness',\n strikes: 'Lightning strikes',\n blend: 'Blend',\n temperature: 'Temperature',\n },\n scenarios: {\n storm: 'A storm sees less and is heard less than a clear day, and strikes lightning',\n blend: 'A change blends over its time and settles in the new weather',\n determinism: 'The same seed gives the same day, another seed another one',\n dry_ground: 'Rain wets the ground and it dries after the rain stops',\n },\n states: {\n clear: 'Clear',\n overcast: 'Overcast',\n light_rain: 'Light rain',\n heavy_rain: 'Heavy rain',\n storm: 'Storm',\n fog: 'Fog',\n windy: 'Windy',\n cyclone: 'Cyclone',\n typhoon: 'Typhoon',\n extreme_heat: 'Extreme heat',\n },\n}\n\nconst PT_BR: WeatherBenchText = {\n group: 'Clima',\n name: 'Área de clima',\n controls: { state: 'Clima', transition: 'Transição', time_of_day: 'Hora do dia', auto_schedule: 'Sorteio automático', seed: 'Semente' },\n hints: {\n transition: 'Quanto demora a mudança para o clima escolhido, em segundos reais.',\n time_of_day: 'Hora do dia (o mundo reinicia o relógio nela e mantém o clima).',\n auto_schedule: 'Ligado: o clima do lugar muda sozinho e o relógio corre 600 vezes mais rápido que o real. Desligado: o clima fica até você trocar.',\n seed: 'A mesma semente dá o mesmo sorteio e os mesmos raios.',\n },\n options: { instant: 'De uma vez', ten_seconds: '10 segundos' },\n layers: { environment: 'Escrever no ambiente do host', fog: 'Névoa na cena' },\n metrics: {\n state: 'Clima',\n hour: 'Hora',\n rain: 'Chuva',\n visibility: 'Visibilidade',\n sight: 'Escala de visão',\n hearing: 'Escala de audição',\n wind: 'Vento',\n heat_index: 'Índice de calor',\n wetness: 'Chão molhado',\n strikes: 'Raios',\n blend: 'Mistura',\n temperature: 'Temperatura',\n },\n scenarios: {\n storm: 'Tempestade enxerga e é ouvida menos que um dia limpo, e solta raios',\n blend: 'Uma mudança se mistura no tempo dela e assenta no clima novo',\n determinism: 'A mesma semente dá o mesmo dia, outra semente dá outro',\n dry_ground: 'A chuva molha o chão e ele seca depois que a chuva para',\n },\n states: {\n clear: 'Limpo',\n overcast: 'Nublado',\n light_rain: 'Chuva fraca',\n heavy_rain: 'Chuva forte',\n storm: 'Tempestade',\n fog: 'Neblina',\n windy: 'Ventania',\n cyclone: 'Ciclone',\n typhoon: 'Tufão',\n extreme_heat: 'Calor extremo',\n },\n}\n\nexport const weatherBenchTextOf = (locale: string): WeatherBenchText => (locale === 'en' ? EN : PT_BR)\n","import type { BenchChange, ModuleControl, ModuleHost, ModuleLayer, ModuleMetric, ModuleScenario, TestableModule } from '@adatechnology/game-contracts'\n\nimport { createWeatherBridges, environmentOf } from '../bridges'\nimport type { WeatherBridges, WeatherTargets } from '../bridges'\nimport type { WeatherSoundOptions } from '../audio/sound'\nimport { createWeather } from '../weather'\nimport type { Weather, WeatherOptions } from '../weather'\nimport type { Strike, WeatherSample } from '../types'\nimport { weatherBenchTextOf } from './locale'\nimport type { WeatherBenchText } from './locale'\n\n/** What a renderer of the weather (game-weather-three) gives the bench: its layers, a step per frame and the switch of each layer. */\nexport type WeatherVisual = {\n update(dt: number): void\n setLayer(id: string, on: boolean): void\n dispose(): void\n}\n\n/** The bench hands a visual the weather it shows (always the live one, even after the bench restarts it) and what the room lends. */\nexport type WeatherVisualContext = { readonly host: ModuleHost; readonly locale: string; readonly weather: { sample(): WeatherSample } }\n\n/** The bench declares its layers before the room lends anything, so the factory tells them first (`fog` is the bench's own layer and reaches the visual through `setLayer` too). */\nexport type WeatherVisualFactory = {\n layers(locale: string): readonly ModuleLayer[]\n create(context: WeatherVisualContext): WeatherVisual\n}\n\nexport type WeatherTestModuleOptions = WeatherOptions & {\n locale?: string\n /** Text of a state id in the game's words; without it the bench uses its own names, then the id. */\n translate?: (key: string, locale: string) => string | undefined\n /** The weather to test (default a new one built from the other options). */\n weather?: Weather\n /** Whether the scene fog follows the weather at first (default true; the layer can be switched either way). A room that is not about weather starts it off. */\n fog?: boolean\n /** The packages the weather drives besides the host's environment bus and sound (fluids film, interactables, vegetation, vfx), from the example. */\n targets?: Omit<WeatherTargets, 'environment' | 'audio'>\n /** The rain, wind and thunder on the host's `audio`; `false` keeps the area silent. */\n sound?: WeatherSoundOptions | false\n /** The picture of the weather (rain, fog, sky, lightning, headlights, haze), from `@adatechnology/game-weather-three`. With it the bench leaves the scene fog to the visual. */\n visual?: WeatherVisualFactory\n}\n\n/** What the scenarios use of the context the test room passes. */\nexport interface WeatherScenarioLike {\n expect(condition: boolean, label: string, data?: Record<string, unknown>): void\n}\n\nexport type WeatherTestModule = TestableModule<WeatherScenarioLike> & {\n /** The weather in use (replaced when the bench changes the seed or the hour). */\n readonly weather: Weather\n}\n\nconst FAST_SCALE = 600\nconst NORMAL_SCALE = 72\nconst BLEND_SECONDS = 10\nconst HOURS_PER_DAY = 24\nconst FOG_NEAR = 1\n\ntype FogLike = new (color: number, near: number, far: number) => { far: number }\ntype SceneLike = { fog?: unknown }\ntype ThreeLike = { Fog?: FogLike }\n\n/**\n * The weather area of a test room, written against the generic contracts only: it drives a `createWeather` world, writes the result into the host's\n * environment bus (`host.environment`) every frame and shows the numbers a game reads from it. It also sets the scene fog from the weather's render fog.\n * Pure: no three.js beyond the host's own `Fog`, and the sky, rain, lightning and sound wait for the packages that draw and play them.\n */\nexport function createWeatherTestModule(options: WeatherTestModuleOptions = {}): WeatherTestModule {\n const locale = options.locale ?? 'pt-BR'\n const text: WeatherBenchText = weatherBenchTextOf(locale)\n const { locale: _locale, translate, weather: given, fog: _fog, targets: _targets, sound: _sound, visual: _visual, ...weatherOptions } = options\n void _locale\n void _fog\n void _targets\n void _sound\n void _visual\n const state = {\n seed: options.seed ?? 1,\n hour: options.startHour ?? 8,\n transition: 0,\n auto: options.autoSchedule ?? false,\n environment: true,\n fog: options.fog ?? true,\n }\n let host: ModuleHost | null = null\n let strikes = 0\n let offStrike: (() => void) | undefined\n let bridges: WeatherBridges | undefined\n let visual: WeatherVisual | undefined\n let weather: Weather = given ?? createWeather({ ...weatherOptions, autoSchedule: state.auto, ...(state.auto ? { timeScale: FAST_SCALE } : {}) })\n\n const watch = (): void => {\n offStrike?.()\n offStrike = weather.on('strike', (_strike: Strike) => void (strikes += 1))\n }\n watch()\n\n const nameOf = (id: string): string => translate?.(`weather.${id}`, locale) ?? text.states[id] ?? id\n\n // the world restarts with the new seed or hour and keeps the weather it was in\n const rebuild = (): void => {\n const current = weather.state()\n weather = createWeather({ ...weatherOptions, seed: state.seed, start: current, startHour: state.hour, autoSchedule: state.auto, timeScale: state.auto ? FAST_SCALE : NORMAL_SCALE })\n strikes = 0\n watch()\n bridges?.bind(weather)\n }\n\n const applyFog = (far: number): void => {\n const scene = host?.scene as SceneLike | undefined\n const Fog = (host?.THREE as ThreeLike | undefined)?.Fog\n if (!scene || typeof Fog !== 'function') return\n const fog = scene.fog as { far: number } | undefined | null\n if (fog && typeof fog.far === 'number') fog.far = far\n else scene.fog = new Fog(0xa8b4c0, FOG_NEAR, far)\n }\n\n const publish = (): void => {\n const sample = weather.sample()\n if (state.environment) host?.environment?.set(environmentOf(sample))\n if (state.fog && !visual) applyFog(sample.fogFar)\n }\n\n const onChange = (change: BenchChange): void => {\n if (change.kind === 'layer') {\n if (change.id === 'environment' || change.id === 'fog') state[change.id] = change.on\n visual?.setLayer(change.id, change.on)\n return\n }\n if (change.kind === 'action') return\n const { id, value } = change\n if (id === 'state' && typeof value === 'string') weather.force(value, state.transition)\n else if (id === 'transition' && typeof value === 'string') state.transition = value === 'instant' ? 0 : BLEND_SECONDS\n else if (id === 'time_of_day' && typeof value === 'number') {\n state.hour = value\n rebuild()\n } else if (id === 'auto_schedule' && typeof value === 'boolean') {\n state.auto = value\n weather.setAutoSchedule(value)\n weather.setTimeScale(value ? FAST_SCALE : NORMAL_SCALE)\n } else if (id === 'seed' && typeof value === 'number') {\n state.seed = value\n rebuild()\n }\n }\n\n const controls = (): ModuleControl[] => [\n { kind: 'choice', id: 'state', name: text.controls.state, options: weather.registry.stateIds().map((id) => ({ id, name: nameOf(id) })), default: weather.state() },\n {\n kind: 'choice',\n id: 'transition',\n name: text.controls.transition,\n hint: text.hints.transition,\n options: [\n { id: 'instant', name: text.options.instant },\n { id: 'ten_seconds', name: text.options.ten_seconds },\n ],\n default: 'instant',\n },\n { kind: 'number', id: 'time_of_day', name: text.controls.time_of_day, hint: text.hints.time_of_day, unit: 'h', min: 0, max: HOURS_PER_DAY - 1, step: 1, default: state.hour },\n { kind: 'boolean', id: 'auto_schedule', name: text.controls.auto_schedule, hint: text.hints.auto_schedule, default: state.auto },\n { kind: 'number', id: 'seed', name: text.controls.seed, hint: text.hints.seed, min: 1, max: 9999, step: 1, default: state.seed },\n ]\n\n const layers = (): ModuleLayer[] => [\n { id: 'environment', name: text.layers.environment },\n { id: 'fog', name: text.layers.fog, default: state.fog },\n ...(options.visual?.layers(locale) ?? []),\n ]\n\n const metrics = (): ModuleMetric[] => [\n { id: 'current_state', name: text.metrics.state, read: () => nameOf(weather.state()) },\n { id: 'hour', name: text.metrics.hour, unit: 'h', digits: 1, read: () => weather.sample().hour },\n { id: 'blend', name: text.metrics.blend, digits: 2, read: () => weather.sample().blend },\n { id: 'rain', name: text.metrics.rain, unit: 'mm/h', digits: 1, read: () => weather.sample().rain },\n { id: 'visibility', name: text.metrics.visibility, unit: 'm', digits: 0, read: () => weather.sample().effects.visibility },\n { id: 'sight', name: text.metrics.sight, digits: 2, read: () => weather.sample().effects.sightScale },\n { id: 'hearing', name: text.metrics.hearing, digits: 2, read: () => weather.sample().effects.hearingScale },\n { id: 'wind', name: text.metrics.wind, unit: 'm/s', digits: 1, read: () => weather.sample().windSpeed },\n { id: 'temperature', name: text.metrics.temperature, unit: '°C', digits: 1, read: () => weather.sample().temperatureC },\n { id: 'heat_index', name: text.metrics.heat_index, unit: '°C', digits: 1, read: () => weather.sample().effects.heatIndexC },\n { id: 'wetness', name: text.metrics.wetness, digits: 2, read: () => weather.sample().groundWetness },\n { id: 'strikes', name: text.metrics.strikes, digits: 0, read: () => strikes },\n ]\n\n // each scenario runs its own worlds, so the bench's live one is not touched\n const fresh = (extra: WeatherOptions = {}): Weather => createWeather({ ...weatherOptions, autoSchedule: false, ...extra })\n const run = (world: Weather, seconds: number): void => {\n for (let elapsed = 0; elapsed < seconds; elapsed += 0.5) world.advance(0.5)\n }\n\n const scenarios = (): Array<ModuleScenario<WeatherScenarioLike>> => [\n {\n id: 'storm',\n name: text.scenarios.storm,\n run: ({ expect }) => {\n const clear = fresh({ seed: 3, start: 'clear' })\n const storm = fresh({ seed: 3, start: 'storm' })\n let hits = 0\n storm.on('strike', () => void (hits += 1))\n run(storm, 120)\n const calm = clear.sample().effects\n const rough = storm.sample().effects\n expect(rough.visibility < calm.visibility, 'a storm sees less far', { storm: rough.visibility, clear: calm.visibility })\n expect(rough.hearingScale < calm.hearingScale, 'rain masks sound', { storm: rough.hearingScale, clear: calm.hearingScale })\n expect(hits > 0, 'a storm strikes lightning', { strikes: hits })\n },\n },\n {\n id: 'blend',\n name: text.scenarios.blend,\n run: ({ expect }) => {\n const world = fresh({ seed: 3, start: 'clear' })\n world.force('heavy_rain', 10)\n run(world, 5)\n const middle = world.sample()\n expect(middle.blend > 0 && middle.blend < 1, 'halfway through the change it is still blending', { blend: middle.blend })\n expect(middle.rain > 0 && middle.rain < 15, 'the rain is between the two weathers', { rain: middle.rain })\n run(world, 6)\n expect(world.sample().blend === 1 && world.sample().state === 'heavy_rain', 'after its time it has settled', { blend: world.sample().blend })\n },\n },\n {\n id: 'determinism',\n name: text.scenarios.determinism,\n run: ({ expect }) => {\n const day = (seed: number): string => JSON.stringify(createWeather({ ...weatherOptions, seed }).timeline(48).samples)\n expect(day(5) === day(5), 'the same seed gives the same two days')\n expect(day(5) !== day(6), 'another seed gives another one')\n },\n },\n {\n id: 'dry_ground',\n name: text.scenarios.dry_ground,\n run: ({ expect }) => {\n const world = fresh({ seed: 3, start: 'heavy_rain' })\n run(world, 60)\n const wet = world.sample().groundWetness\n expect(wet > 0.3, 'rain wets the ground', { wetness: wet })\n world.force('clear', 0)\n run(world, 600)\n expect(world.sample().groundWetness < wet, 'it dries after the rain stops', { wet, after: world.sample().groundWetness })\n },\n },\n ]\n\n const module: WeatherTestModule = {\n id: 'weather_area',\n name: text.name,\n ground: { surface: 'lawn' },\n group: text.group,\n get weather() {\n return weather\n },\n items: () => [],\n controls: () => controls(),\n layers: () => layers(),\n metrics: () => metrics(),\n scenarios: () => scenarios(),\n attach(lent) {\n host = lent\n const off = lent.bench?.onChange(onChange)\n bridges = createWeatherBridges({ ...options.targets, ...(lent.audio ? { audio: lent.audio } : {}) }, { sound: options.sound ?? {} })\n bridges.bind(weather)\n visual = options.visual?.create({ host: lent, locale, weather: { sample: () => weather.sample() } })\n if (visual && !state.fog) visual.setLayer('fog', false)\n publish()\n const offFrame = lent.onFrame((dt) => {\n weather.advance(dt)\n publish()\n bridges?.update(dt)\n visual?.update(dt)\n })\n return () => {\n visual?.dispose()\n visual = undefined\n off?.()\n offFrame()\n bridges?.dispose()\n bridges = undefined\n host = null\n }\n },\n }\n return module\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/blend.ts","../src/daylight.ts","../src/heat.ts","../src/effects.ts","../src/rng.ts","../src/lightning.ts","../src/data/climates.json","../src/data/states.json","../src/registry.ts","../src/scheduler.ts","../src/wetness.ts","../src/weather.ts","../../game-contracts/src/blast-shove.ts","../../game-contracts/src/blast-knock.ts","../../game-contracts/src/impact-cost.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/seal.ts","../../game-contracts/src/twist.ts","../../game-contracts/src/animation.ts","../../game-contracts/src/tool-animation.ts","../../game-contracts/src/frame.ts","../../game-contracts/src/warn.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","../../game-contracts/src/foley.ts","../../game-contracts/src/index.ts","../../game-audio/src/manifest.ts","../../game-audio/src/kit-samples.ts","../../game-audio/src/recipes/scene.ts","../../game-audio/src/recipes/foley.ts","../../game-audio/src/recipes/body-foley.ts","../../game-audio/src/webaudio/sink.ts","../src/audio/recipes.ts","../src/audio/sound.ts","../src/bridges.ts","../src/bench/locale.ts","../src/bench/module.ts"],"sourcesContent":["/**\n * @adatechnology/game-weather: pure, seeded weather. States (clear to typhoon) with the numbers of the teams' pages, climates that move between\n * them in game time (2 to 6 hours each, fronts from the west), lightning with the thunder delay, ground wetness, what the weather means for\n * sight, hearing and footsteps, the heat index and the day-night formula, plus the bridges that push it to fluids, interactables, vegetation, vfx, the environment bus and the sound (rain, wind and thunder recipes of game-audio). No three.js, no game content: renderers read it.\n */\nexport { createWeather, createTimeline, DEFAULT_START_HOUR } from './weather'\nexport type { TimelineOptions, Weather, WeatherOptions, WeatherTimeline } from './weather'\nexport { CLIMATES, WEATHER_STATES, createWeatherRegistry } from './registry'\nexport type { RegistryOptions, WeatherRegistry } from './registry'\nexport { blendValues, STATE_VALUE_KEYS, valuesOf } from './blend'\nexport { createScheduler } from './scheduler'\nexport type { Scheduler } from './scheduler'\nexport { FLASH_END_SECONDS, SPEED_OF_SOUND, createLightningStream, flashIntensity, thunderDelay } from './lightning'\nexport type { LightningStream } from './lightning'\nexport { DEFAULT_DRY_OUT_SECONDS, DEFAULT_WET_RISE_SECONDS, createWetness } from './wetness'\nexport type { Wetness, WetnessOptions } from './wetness'\nexport { DRY_VISIBILITY_METERS, NIGHT_SIGHT_LOSS, deriveEffects } from './effects'\nexport type { EffectsInput } from './effects'\nexport { heatIndexC, heatStress } from './heat'\nexport { DAY_REAL_SECONDS, DEFAULT_TIME_SCALE, GAME_DAY_SECONDS, dayFractionAt, daylight } from './daylight'\nexport type { Daylight } from './daylight'\nexport { createRng, createStream, streamSeed } from './rng'\nexport type {\n Climate,\n DeepPartial,\n LightningConfig,\n StateValueKey,\n StateValues,\n Strike,\n WeatherEffects,\n WeatherEventMap,\n WeatherSample,\n WeatherState,\n} from './types'\n\nexport { createWeatherBridges, environmentOf, DEFAULT_BRIDGE_RULES } from './bridges'\nexport type { FilmTarget, InteractablesTarget, VegetationTarget, VfxTarget, WeatherBridgeOptions, WeatherBridgeRules, WeatherBridges, WeatherTargets } from './bridges'\nexport { RAIN_HISS_GAME, RAIN_LOOP, THUNDER, WEATHER_BANK, WEATHER_LOOPS, WEATHER_SOUNDS, WIND_LOOP } from './audio/recipes'\nexport { DEFAULT_RAIN_SOUND, DEFAULT_WIND_SOUND, createWeatherSound, rainVolume, strikePosition, windVolume } from './audio/sound'\nexport type { WeatherSound, WeatherSoundOptions, WeatherSoundStyle } from './audio/sound'\n\n/** @deprecated import from '@adatechnology/game-weather/testing' */\nexport { createWeatherTestModule } from './testing'\n/** @deprecated import from '@adatechnology/game-weather/testing' */\nexport type { WeatherScenarioLike, WeatherTestModule, WeatherTestModuleOptions, WeatherVisual, WeatherVisualContext, WeatherVisualFactory } from './testing'\n/** @deprecated import from '@adatechnology/game-weather/testing' */\nexport { weatherBenchTextOf } from './testing'\n/** @deprecated import from '@adatechnology/game-weather/testing' */\nexport type { WeatherBenchText } from './testing'\n","import type { StateValueKey, StateValues, WeatherState } from './types'\n\nexport const STATE_VALUE_KEYS: readonly StateValueKey[] = [\n 'rain',\n 'drops',\n 'sheets',\n 'windX',\n 'windZ',\n 'gust',\n 'fogFar',\n 'fogDensity',\n 'skyScale',\n 'visibility',\n 'humidity',\n 'temperatureDeltaC',\n 'wetness',\n 'hearingScale',\n 'footstepScale',\n 'dryingScale',\n 'debrisPush',\n 'haze',\n 'mistPerSecond',\n 'ashPerSecond',\n]\n\nexport function valuesOf(state: WeatherState): StateValues {\n const values: Record<string, number> = {}\n for (const key of STATE_VALUE_KEYS) values[key] = state[key]\n return values as unknown as StateValues\n}\n\n/** Every number between two states: `progress` 0 is `from`, 1 is `to`. */\nexport function blendValues(from: StateValues, to: StateValues, progress: number): StateValues {\n const amount = Math.min(1, Math.max(0, progress))\n const blended: Record<string, number> = {}\n for (const key of STATE_VALUE_KEYS) blended[key] = from[key] + (to[key] - from[key]) * amount\n return blended as unknown as StateValues\n}\n","/** A game day lasts 20 real minutes in the slice (jogo.js L69127: `dayT + dt / 1200`). */\nexport const DAY_REAL_SECONDS = 1200\nexport const GAME_DAY_SECONDS = 86400\n/** Game seconds per real second of the 20-minute day. */\nexport const DEFAULT_TIME_SCALE = GAME_DAY_SECONDS / DAY_REAL_SECONDS\n\nconst NIGHT_SPAN = 2.2\n\nconst clamp = (value: number, low: number, high: number): number => (value < low ? low : value > high ? high : value)\n\nexport type Daylight = {\n /** Sun height 0..1 (`s` of the game). */\n readonly sun: number\n /** 0 in full daylight, 1 in the darkest night. */\n readonly nightFactor: number\n}\n\n/** jogo.js L69128-L69130: `s = clamp(sin(dayT·2π − π/2)·0.5 + 0.5, 0, 1)`, `nightF = 1 − clamp(s·2.2, 0, 1)`. `dayFraction` 0 is midnight. */\nexport function daylight(dayFraction: number): Daylight {\n const sun = clamp(Math.sin(dayFraction * Math.PI * 2 - Math.PI / 2) * 0.5 + 0.5, 0, 1)\n return { sun, nightFactor: 1 - clamp(sun * NIGHT_SPAN, 0, 1) }\n}\n\n/** The fraction of the day after `gameSeconds` from `startHour`. */\nexport function dayFractionAt(startHour: number, gameSeconds: number): number {\n const fraction = (startHour / 24 + gameSeconds / GAME_DAY_SECONDS) % 1\n return fraction < 0 ? fraction + 1 : fraction\n}\n","const HEAT_STRESS_BANDS: ReadonlyArray<readonly [indexC: number, stress: number]> = [\n [27, 0],\n [32, 1 / 3],\n [41, 2 / 3],\n [54, 1],\n]\nconst NOAA_REGRESSION_FROM_F = 80\n\nconst toFahrenheit = (celsius: number): number => (celsius * 9) / 5 + 32\nconst toCelsius = (fahrenheit: number): number => ((fahrenheit - 32) * 5) / 9\n\n/**\n * NOAA heat index in °C: the Steadman estimate averaged with the temperature, and from 80 °F up the Rothfusz regression with the low-humidity and\n * high-humidity adjustments. Built from + - * / and `sqrt` only, so it is the same on every engine.\n */\nexport function heatIndexC(temperatureC: number, relativeHumidityPercent: number): number {\n const t = toFahrenheit(temperatureC)\n const r = relativeHumidityPercent\n const steadman = 0.5 * (t + 61 + (t - 68) * 1.2 + r * 0.094)\n const simple = (steadman + t) / 2\n if (simple < NOAA_REGRESSION_FROM_F) return toCelsius(simple)\n let index =\n -42.379 +\n 2.04901523 * t +\n 10.14333127 * r -\n 0.22475541 * t * r -\n 0.00683783 * t * t -\n 0.05481717 * r * r +\n 0.00122874 * t * t * r +\n 0.00085282 * t * r * r -\n 0.00000199 * t * t * r * r\n if (r < 13 && t >= 80 && t <= 112) index -= ((13 - r) / 4) * Math.sqrt((17 - Math.abs(t - 95)) / 17)\n else if (r > 85 && t >= 80 && t <= 87) index += ((r - 85) / 10) * ((87 - t) / 5)\n return toCelsius(index)\n}\n\n/** 0..1 across the NOAA bands (caution 27, extreme caution 32, danger 41, extreme danger 54 °C), linear between them. */\nexport function heatStress(heatIndex: number): number {\n const first = HEAT_STRESS_BANDS[0]!\n const last = HEAT_STRESS_BANDS[HEAT_STRESS_BANDS.length - 1]!\n if (heatIndex <= first[0]) return first[1]\n if (heatIndex >= last[0]) return last[1]\n for (let index = 1; index < HEAT_STRESS_BANDS.length; index++) {\n const [upperC, upperStress] = HEAT_STRESS_BANDS[index]!\n const [lowerC, lowerStress] = HEAT_STRESS_BANDS[index - 1]!\n if (heatIndex <= upperC) return lowerStress + ((heatIndex - lowerC) / (upperC - lowerC)) * (upperStress - lowerStress)\n }\n return last[1]\n}\n","import { heatIndexC, heatStress } from './heat'\nimport type { StateValues, WeatherEffects } from './types'\n\n/** Gameplay visibility of a dry day (the game's dry fog far, jogo.js L69141): `sightScale` is the visibility against it. */\nexport const DRY_VISIBILITY_METERS = 220\n/** jogo.js L66872: sight is `perc[0] × (1 − 0.4 × nightF)`. */\nexport const NIGHT_SIGHT_LOSS = 0.4\n\nexport type EffectsInput = {\n readonly values: StateValues\n readonly nightFactor: number\n readonly temperatureC: number\n readonly humidityPercent: number\n readonly lightningReveal: number\n}\n\n/** What the weather means for gameplay, as numbers for whoever has eyes, ears or feet in the world (perception, footsteps, heat). */\nexport function deriveEffects(input: EffectsInput): WeatherEffects {\n const { values, nightFactor } = input\n const weatherSight = Math.min(1, Math.max(0, values.visibility / DRY_VISIBILITY_METERS))\n const heatIndex = heatIndexC(input.temperatureC, input.humidityPercent)\n return {\n visibility: values.visibility,\n sightScale: weatherSight * (1 - NIGHT_SIGHT_LOSS * nightFactor),\n hearingScale: values.hearingScale,\n footstepScale: values.footstepScale,\n heatIndexC: heatIndex,\n heatStress: heatStress(heatIndex),\n lightningReveal: input.lightningReveal,\n dryingScale: values.dryingScale,\n debrisPush: values.debrisPush,\n }\n}\n","const FNV_OFFSET = 0x811c9dc5\nconst FNV_PRIME = 0x01000193\nconst MIX = 0x9e3779b1\n\n/** Deterministic mulberry32 generator; returns floats in [0, 1). */\nexport function createRng(seed: number): () => number {\n let state = seed >>> 0\n return () => {\n state = (state + 0x6d2b79f5) >>> 0\n let mixed = Math.imul(state ^ (state >>> 15), state | 1)\n mixed ^= mixed + Math.imul(mixed ^ (mixed >>> 7), mixed | 61)\n return ((mixed ^ (mixed >>> 14)) >>> 0) / 4294967296\n }\n}\n\n/** A seed for the stream called `name`: the same seed gives each name its own sequence, so lightning never shifts the schedule. */\nexport function streamSeed(seed: number, name: string): number {\n let hash = FNV_OFFSET\n for (let index = 0; index < name.length; index++) hash = Math.imul(hash ^ name.charCodeAt(index), FNV_PRIME) >>> 0\n return Math.imul((seed >>> 0) ^ hash, MIX) >>> 0\n}\n\nexport function createStream(seed: number, name: string): () => number {\n return createRng(streamSeed(seed, name))\n}\n\nexport function drawBetween(rng: () => number, range: readonly [min: number, max: number]): number {\n return range[0] + rng() * (range[1] - range[0])\n}\n","import { createStream, drawBetween } from './rng'\nimport type { LightningConfig } from './types'\n\n/** Speed of sound in air, m/s: a physical constant, so it is not configurable. */\nexport const SPEED_OF_SOUND = 343\n\n/** Seconds a flash goes on after a strike before the scene is back to normal (Workshop W1068: `k > .4`). */\nexport const FLASH_END_SECONDS = 0.4\n\n/** Seconds from the flash to the thunder for a strike `distance` metres away. */\nexport function thunderDelay(distance: number): number {\n return distance / SPEED_OF_SOUND\n}\n\n/** The Workshop double flash (W1068): full for 0.08 s, 0.2 until 0.16 s, 0.8 until 0.26 s, then dark. `elapsed` is seconds since the strike. */\nexport function flashIntensity(elapsed: number): number {\n if (elapsed < 0 || elapsed >= FLASH_END_SECONDS) return 0\n if (elapsed < 0.08) return 1\n if (elapsed < 0.16) return 0.2\n if (elapsed < 0.26) return 0.8\n return 0\n}\n\nexport type LightningStream = {\n /** Seconds from entering a state with lightning to its first strike. */\n first(config: LightningConfig): number\n /** Seconds from a strike to the next one. */\n interval(config: LightningConfig): number\n /** Where a strike lands: metres and degrees clockwise from north. Draws the distance, then the bearing. */\n place(config: LightningConfig): { readonly distance: number; readonly bearing: number }\n}\n\n/** The lightning's own seeded stream, apart from the scheduler's: the server can send the seed and every client sees the same strikes. */\nexport function createLightningStream(seed: number): LightningStream {\n const rng = createStream(seed, 'lightning')\n return {\n first: (config) => drawBetween(rng, config.first),\n interval: (config) => drawBetween(rng, config.interval),\n place(config) {\n const distance = drawBetween(rng, config.distance)\n return { distance, bearing: rng() * 360 }\n },\n }\n}\n","[\n {\n \"id\": \"temperate\",\n \"sources\": [\"galeria-vale-cinzento:L1393 (changes every 2-6 h, arrives from the west)\", \"design:T6 3.2 kit default: mostly clear and overcast, no extremes\"],\n \"start\": \"clear\",\n \"dwellHours\": [2, 6],\n \"blendMinutes\": 10,\n \"frontDirection\": [1, 0],\n \"temperatureC\": { \"mean\": 16, \"swing\": 6 },\n \"humidityOffset\": 0,\n \"transitions\": {\n \"clear\": { \"overcast\": 4, \"windy\": 1, \"fog\": 1, \"light_rain\": 1 },\n \"overcast\": { \"clear\": 3, \"light_rain\": 3, \"heavy_rain\": 1, \"fog\": 1, \"windy\": 1 },\n \"light_rain\": { \"overcast\": 3, \"clear\": 2, \"heavy_rain\": 2 },\n \"heavy_rain\": { \"light_rain\": 3, \"overcast\": 2, \"storm\": 1 },\n \"storm\": { \"heavy_rain\": 3, \"light_rain\": 2, \"overcast\": 1 },\n \"fog\": { \"clear\": 3, \"overcast\": 2 },\n \"windy\": { \"clear\": 3, \"overcast\": 3, \"light_rain\": 1 }\n }\n },\n {\n \"id\": \"tropical\",\n \"sources\": [\"galeria-vale-cinzento:L1393 (changes every 2-6 h, arrives from the west)\", \"design:T6 3.2 and Q3: extremes rare and enabled\"],\n \"start\": \"clear\",\n \"dwellHours\": [2, 6],\n \"blendMinutes\": 10,\n \"frontDirection\": [1, 0],\n \"temperatureC\": { \"mean\": 29, \"swing\": 3 },\n \"humidityOffset\": 0.1,\n \"transitions\": {\n \"clear\": { \"overcast\": 3, \"light_rain\": 2, \"windy\": 1, \"extreme_heat\": 0.8 },\n \"overcast\": { \"clear\": 3, \"light_rain\": 3, \"heavy_rain\": 2, \"fog\": 0.5 },\n \"light_rain\": { \"overcast\": 3, \"clear\": 2, \"heavy_rain\": 2 },\n \"heavy_rain\": { \"light_rain\": 3, \"overcast\": 2, \"storm\": 1.5 },\n \"storm\": { \"heavy_rain\": 3, \"light_rain\": 2, \"overcast\": 1, \"cyclone\": 0.5 },\n \"fog\": { \"clear\": 3, \"overcast\": 2 },\n \"windy\": { \"clear\": 3, \"overcast\": 2, \"light_rain\": 1, \"storm\": 0.5 },\n \"extreme_heat\": { \"clear\": 4, \"overcast\": 2, \"storm\": 0.8 },\n \"cyclone\": { \"storm\": 3, \"heavy_rain\": 2, \"typhoon\": 0.5 },\n \"typhoon\": { \"cyclone\": 3, \"storm\": 2 }\n }\n }\n]\n","[\n {\n \"id\": \"clear\",\n \"sources\": [\"jogo:L69141 (fog far 220 dry)\", \"showroom.js:L1693-1710 (breeze 3.5, 1.5 m/s)\"],\n \"rain\": 0, \"drops\": 0, \"sheets\": 0,\n \"windX\": 3.5, \"windZ\": 1.5, \"gust\": 5,\n \"fogFar\": 220, \"fogDensity\": 0, \"skyScale\": 1, \"visibility\": 220,\n \"humidity\": 0.5, \"temperatureDeltaC\": 0, \"wetness\": 0,\n \"hearingScale\": 1, \"footstepScale\": 1, \"dryingScale\": 1, \"debrisPush\": 0,\n \"haze\": 0, \"mistPerSecond\": 0, \"ashPerSecond\": 0\n },\n {\n \"id\": \"overcast\",\n \"sources\": [\"cena-de-arte:L413 (wind .35, .12)\", \"cena-de-arte:L36 (FogExp2 .03)\", \"design:T6 sky x0.9\"],\n \"rain\": 0, \"drops\": 0, \"sheets\": 0,\n \"windX\": 0.35, \"windZ\": 0.12, \"gust\": 0.6,\n \"fogFar\": 220, \"fogDensity\": 0.03, \"skyScale\": 0.9, \"visibility\": 200,\n \"humidity\": 0.65, \"temperatureDeltaC\": -1, \"wetness\": 0,\n \"hearingScale\": 1, \"footstepScale\": 1, \"dryingScale\": 1, \"debrisPush\": 0,\n \"haze\": 0, \"mistPerSecond\": 0, \"ashPerSecond\": 0\n },\n {\n \"id\": \"light_rain\",\n \"sources\": [\"oficina-vfx:W1065 (Garoa: 900 drops, slant .3)\", \"jogo:L70416-70427 (rain level .035)\", \"design:T6 2.5 mm/h, wetness .35, visibility 160, hearing x0.8 (Q4)\"],\n \"rain\": 2.5, \"drops\": 900, \"sheets\": 0,\n \"windX\": 0.3, \"windZ\": 0, \"gust\": 0.6,\n \"fogFar\": 180, \"fogDensity\": 0, \"skyScale\": 0.9, \"visibility\": 160,\n \"humidity\": 0.85, \"temperatureDeltaC\": -2, \"wetness\": 0.35,\n \"hearingScale\": 0.8, \"footstepScale\": 0.85, \"dryingScale\": 1, \"debrisPush\": 0,\n \"haze\": 0, \"mistPerSecond\": 0, \"ashPerSecond\": 0\n },\n {\n \"id\": \"heavy_rain\",\n \"sources\": [\"oficina-vfx:W1065 (Chuva: 2200 drops, slant 1.2)\", \"cena-de-arte:L1314 (rain gain .06)\", \"jogo:L69141 (fog far 140, sky x0.8)\", \"galeria-vale-cinzento:L1393 (vision 120 m, quieter steps)\", \"design:T6 15 mm/h, hearing x0.6 (Q4)\"],\n \"rain\": 15, \"drops\": 2200, \"sheets\": 0,\n \"windX\": 1.2, \"windZ\": 0, \"gust\": 2,\n \"fogFar\": 140, \"fogDensity\": 0, \"skyScale\": 0.8, \"visibility\": 120,\n \"humidity\": 0.9, \"temperatureDeltaC\": -4, \"wetness\": 0.8,\n \"hearingScale\": 0.6, \"footstepScale\": 0.7, \"dryingScale\": 1, \"debrisPush\": 0,\n \"haze\": 0, \"mistPerSecond\": 0, \"ashPerSecond\": 0\n },\n {\n \"id\": \"storm\",\n \"sources\": [\"oficina-vfx:W1065-1069 (Temporal: 4000 drops, slant 4.5, lightning 3.5-6 s, first at 2-4 s, sheets)\", \"showroom.js (60 mm/h)\", \"design:T6 hearing x0.5 (Q4)\"],\n \"rain\": 60, \"drops\": 4000, \"sheets\": 1,\n \"windX\": 4.5, \"windZ\": 0, \"gust\": 6,\n \"fogFar\": 110, \"fogDensity\": 0, \"skyScale\": 0.7, \"visibility\": 100,\n \"humidity\": 0.92, \"temperatureDeltaC\": -5, \"wetness\": 1,\n \"hearingScale\": 0.5, \"footstepScale\": 0.6, \"dryingScale\": 1, \"debrisPush\": 0,\n \"haze\": 0, \"mistPerSecond\": 0, \"ashPerSecond\": 0,\n \"lightning\": { \"interval\": [3.5, 6], \"first\": [2, 4], \"distance\": [200, 4000], \"reveal\": 0.2 }\n },\n {\n \"id\": \"fog\",\n \"sources\": [\"oficina-vfx:W1073-1080 (density .075, ground mist 5/s, ash 14/s)\", \"jogo:L69101 (zone fog far 60)\"],\n \"rain\": 0, \"drops\": 0, \"sheets\": 0,\n \"windX\": 0.3, \"windZ\": 0.1, \"gust\": 0.5,\n \"fogFar\": 60, \"fogDensity\": 0.075, \"skyScale\": 0.85, \"visibility\": 40,\n \"humidity\": 0.95, \"temperatureDeltaC\": -3, \"wetness\": 0.15,\n \"hearingScale\": 1, \"footstepScale\": 1, \"dryingScale\": 1, \"debrisPush\": 0,\n \"haze\": 0, \"mistPerSecond\": 5, \"ashPerSecond\": 14\n },\n {\n \"id\": \"windy\",\n \"sources\": [\"cena-de-arte:L1315 (wind loop)\", \"design:T6 Q3 8 m/s, gust 12\"],\n \"rain\": 0, \"drops\": 0, \"sheets\": 0,\n \"windX\": 8, \"windZ\": 0, \"gust\": 12,\n \"fogFar\": 220, \"fogDensity\": 0, \"skyScale\": 1, \"visibility\": 220,\n \"humidity\": 0.45, \"temperatureDeltaC\": -1, \"wetness\": 0,\n \"hearingScale\": 0.85, \"footstepScale\": 0.9, \"dryingScale\": 1, \"debrisPush\": 0,\n \"haze\": 0, \"mistPerSecond\": 0, \"ashPerSecond\": 0\n },\n {\n \"id\": \"cyclone\",\n \"sources\": [\"design:T6 Q3 physics preset, no team source: tropical cyclone 33 m/s sustained, gust x1.3, storm rain, debris push\"],\n \"rain\": 60, \"drops\": 4000, \"sheets\": 1,\n \"windX\": 33, \"windZ\": 0, \"gust\": 42.9,\n \"fogFar\": 80, \"fogDensity\": 0, \"skyScale\": 0.6, \"visibility\": 60,\n \"humidity\": 0.9, \"temperatureDeltaC\": 0, \"wetness\": 1,\n \"hearingScale\": 0.3, \"footstepScale\": 0.5, \"dryingScale\": 1, \"debrisPush\": 8,\n \"haze\": 0, \"mistPerSecond\": 0, \"ashPerSecond\": 0,\n \"lightning\": { \"interval\": [3.5, 6], \"first\": [2, 4], \"distance\": [200, 4000], \"reveal\": 0.2 }\n },\n {\n \"id\": \"typhoon\",\n \"sources\": [\"design:T6 Q3 physics preset, no team source: typhoon 50 m/s sustained, gust x1.3, storm rain, debris push\"],\n \"rain\": 60, \"drops\": 4000, \"sheets\": 1,\n \"windX\": 50, \"windZ\": 0, \"gust\": 65,\n \"fogFar\": 60, \"fogDensity\": 0, \"skyScale\": 0.5, \"visibility\": 40,\n \"humidity\": 0.9, \"temperatureDeltaC\": 0, \"wetness\": 1,\n \"hearingScale\": 0.25, \"footstepScale\": 0.4, \"dryingScale\": 1, \"debrisPush\": 18,\n \"haze\": 0, \"mistPerSecond\": 0, \"ashPerSecond\": 0,\n \"lightning\": { \"interval\": [3.5, 6], \"first\": [2, 4], \"distance\": [200, 4000], \"reveal\": 0.2 }\n },\n {\n \"id\": \"extreme_heat\",\n \"sources\": [\"design:T6 Q3 no team source: 38 C at the tropical noon (+6 over the climate), 60 % humidity, haze, drying x2\"],\n \"rain\": 0, \"drops\": 0, \"sheets\": 0,\n \"windX\": 0.8, \"windZ\": 0.3, \"gust\": 1.5,\n \"fogFar\": 220, \"fogDensity\": 0, \"skyScale\": 1, \"visibility\": 200,\n \"humidity\": 0.6, \"temperatureDeltaC\": 6, \"wetness\": 0,\n \"hearingScale\": 1, \"footstepScale\": 1, \"dryingScale\": 2, \"debrisPush\": 0,\n \"haze\": 1, \"mistPerSecond\": 0, \"ashPerSecond\": 0\n }\n]\n","import climatesJson from './data/climates.json'\nimport statesJson from './data/states.json'\nimport type { Climate, DeepPartial, WeatherState } from './types'\n\nexport const WEATHER_STATES: readonly WeatherState[] = statesJson as unknown as readonly WeatherState[]\nexport const CLIMATES: readonly Climate[] = climatesJson as unknown as readonly Climate[]\n\nexport type RegistryOptions = {\n /** Added to the shipped states or, when the id exists, replacing it. */\n readonly states?: readonly WeatherState[]\n readonly climates?: readonly Climate[]\n /** Merged into a shipped or added entry by id; a patch for an id that does not exist is ignored. */\n readonly patch?: {\n readonly states?: Readonly<Record<string, DeepPartial<WeatherState>>>\n readonly climates?: Readonly<Record<string, DeepPartial<Climate>>>\n }\n readonly remove?: { readonly states?: readonly string[]; readonly climates?: readonly string[] }\n}\n\nexport type WeatherRegistry = {\n state(id: string): WeatherState | undefined\n climate(id: string): Climate | undefined\n stateIds(): string[]\n climateIds(): string[]\n registerState(state: WeatherState): void\n registerClimate(climate: Climate): void\n patchState(id: string, patch: DeepPartial<WeatherState>): void\n patchClimate(id: string, patch: DeepPartial<Climate>): void\n removeState(id: string): void\n removeClimate(id: string): void\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction mergeRecords(base: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...base }\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined) continue\n const current = merged[key]\n merged[key] = isPlainObject(current) && isPlainObject(value) ? mergeRecords(current, value) : value\n }\n return merged\n}\n\nfunction deepMerge<TValue>(base: TValue, patch: DeepPartial<TValue>): TValue {\n return mergeRecords(base as Record<string, unknown>, patch as Record<string, unknown>) as TValue\n}\n\ntype Entry = { readonly id: string }\n\nfunction createTable<TEntry extends Entry>(shipped: readonly TEntry[]): Map<string, TEntry> {\n return new Map(shipped.map((entry) => [entry.id, entry]))\n}\n\nfunction patchEntry<TEntry extends Entry>(table: Map<string, TEntry>, id: string, patch: DeepPartial<TEntry>): void {\n const entry = table.get(id)\n if (entry) table.set(id, deepMerge(entry, patch))\n}\n\n/** The shipped states and climates with yours added, patched or removed; change it later with the same verbs. */\nexport function createWeatherRegistry(options: RegistryOptions = {}): WeatherRegistry {\n const states = createTable(WEATHER_STATES)\n const climates = createTable(CLIMATES)\n for (const state of options.states ?? []) states.set(state.id, state)\n for (const climate of options.climates ?? []) climates.set(climate.id, climate)\n for (const [id, patch] of Object.entries(options.patch?.states ?? {})) patchEntry(states, id, patch)\n for (const [id, patch] of Object.entries(options.patch?.climates ?? {})) patchEntry(climates, id, patch)\n for (const id of options.remove?.states ?? []) states.delete(id)\n for (const id of options.remove?.climates ?? []) climates.delete(id)\n return {\n state: (id) => states.get(id),\n climate: (id) => climates.get(id),\n stateIds: () => [...states.keys()],\n climateIds: () => [...climates.keys()],\n registerState: (state) => void states.set(state.id, state),\n registerClimate: (climate) => void climates.set(climate.id, climate),\n patchState: (id, patch) => patchEntry(states, id, patch),\n patchClimate: (id, patch) => patchEntry(climates, id, patch),\n removeState: (id) => void states.delete(id),\n removeClimate: (id) => void climates.delete(id),\n }\n}\n","import { createStream, drawBetween } from './rng'\nimport type { Climate } from './types'\n\nconst SECONDS_PER_HOUR = 3600\n/** A state lasts at least this long, so a climate patched to a zero dwell cannot stall the clock. */\nconst MIN_DWELL_SECONDS = 1\n\nexport type Scheduler = {\n /** Game seconds at which the current state ends. */\n nextChangeAt(): number\n /** Picks the state after `current`, then draws how long it lasts from `at`. Returns the new state's id. */\n next(current: string, at: number): string\n /** Draws a new duration from `at` (after a forced change). */\n restart(at: number): void\n}\n\n/**\n * A seeded Markov chain in game time. Draw order, fixed so the timeline repeats: at start one dwell; at each change the next state first\n * (one draw, over the weights in the climate's order), then its dwell. States `isKnown` rejects are left out of the weights.\n */\nexport function createScheduler(climate: Climate, seed: number, isKnown: (id: string) => boolean): Scheduler {\n const rng = createStream(seed, 'schedule')\n let changeAt = 0\n const drawDwell = (from: number): number => from + Math.max(MIN_DWELL_SECONDS, drawBetween(rng, climate.dwellHours) * SECONDS_PER_HOUR)\n changeAt = drawDwell(0)\n return {\n nextChangeAt: () => changeAt,\n next(current, at) {\n const options = Object.entries(climate.transitions[current] ?? {}).filter(([id, weight]) => weight > 0 && isKnown(id))\n const total = options.reduce((sum, [, weight]) => sum + weight, 0)\n let chosen = current\n if (total > 0) {\n let pick = rng() * total\n chosen = options[options.length - 1]![0]\n for (const [id, weight] of options) {\n if (pick < weight) {\n chosen = id\n break\n }\n pick -= weight\n }\n }\n changeAt = drawDwell(at)\n return chosen\n },\n restart(at) {\n changeAt = drawDwell(at)\n },\n }\n}\n","export const DEFAULT_WET_RISE_SECONDS = 120\n/** The Workshop sheet: the water goes back to the colour of the ground 3 minutes after the rain stops. */\nexport const DEFAULT_DRY_OUT_SECONDS = 180\n\nexport type WetnessOptions = {\n /** Seconds of rain to wet the ground fully. */\n readonly riseSeconds?: number\n /** Seconds from the end of the rain until the ground is dry. */\n readonly dryOutSeconds?: number\n}\n\nexport type Wetness = {\n value(): number\n /** Moves `seconds` toward `target` (the wetness the weather asks for); `dryingScale` speeds the dry-out up. Returns the new value. */\n update(seconds: number, target: number, dryingScale?: number): number\n reset(value?: number): void\n}\n\n/** Ground wetness: rises while the weather asks for more, then falls in a straight line that ends `dryOutSeconds` after the start of the fall. */\nexport function createWetness(options: WetnessOptions = {}): Wetness {\n const riseSeconds = options.riseSeconds ?? DEFAULT_WET_RISE_SECONDS\n const dryOutSeconds = options.dryOutSeconds ?? DEFAULT_DRY_OUT_SECONDS\n let current = 0\n let peak = 0\n let isDrying = false\n return {\n value: () => current,\n update(seconds, target, dryingScale = 1) {\n if (target > current) {\n isDrying = false\n current = Math.min(target, current + seconds / riseSeconds)\n } else if (target < current) {\n if (!isDrying) {\n isDrying = true\n peak = current\n }\n current = Math.max(target, current - (peak * seconds * dryingScale) / dryOutSeconds)\n } else isDrying = false\n return current\n },\n reset(value = 0) {\n current = value\n peak = value\n isDrying = false\n },\n }\n}\n","import { blendValues, valuesOf } from './blend'\nimport { DEFAULT_TIME_SCALE, daylight, dayFractionAt } from './daylight'\nimport { deriveEffects } from './effects'\nimport { createLightningStream, flashIntensity, thunderDelay } from './lightning'\nimport { createWeatherRegistry } from './registry'\nimport type { RegistryOptions, WeatherRegistry } from './registry'\nimport { createScheduler } from './scheduler'\nimport type { Climate, StateValues, Strike, WeatherEventMap, WeatherSample, WeatherState } from './types'\nimport { createWetness } from './wetness'\nimport type { WetnessOptions } from './wetness'\n\nexport const DEFAULT_START_HOUR = 8\nconst SECONDS_PER_MINUTE = 60\nconst WETNESS_STEP_SECONDS = 1\n\nexport type WeatherOptions = RegistryOptions & {\n /** Seed of the schedule and of the lightning (each has its own stream). Default 1. */\n readonly seed?: number\n /** Climate id. Default `temperate`. */\n readonly climate?: string\n /** First state; default the climate's `start`, which is `clear`: the normal world comes first. */\n readonly start?: string\n /** Game seconds per real second. Default 72 (the 20-minute day). */\n readonly timeScale?: number\n /** Hour of the day the world starts at. Default 8. */\n readonly startHour?: number\n /** `false` keeps the state until `force` changes it. Default true. */\n readonly autoSchedule?: boolean\n readonly wetness?: WetnessOptions\n /** An existing registry, instead of building one from the registry options. */\n readonly registry?: WeatherRegistry\n}\n\nexport type WeatherTimeline = {\n readonly seed: number\n readonly climate: string\n readonly start: string\n readonly hours: number\n readonly sampleMinutes: number\n readonly transitions: ReadonlyArray<{ readonly gameSeconds: number; readonly from: string; readonly to: string }>\n readonly strikes: readonly Strike[]\n readonly samples: ReadonlyArray<Readonly<Record<string, number | string>>>\n}\n\nexport type Weather = {\n readonly registry: WeatherRegistry\n /** Moves the world forward by `dt` real seconds (game time moves `timeScale` times as fast). Events fire in order as they are crossed. */\n advance(dt: number): void\n sample(): WeatherSample\n /** The state being blended to, or settled in. */\n state(): string\n /** Changes state now, blending over `blendSeconds` real seconds (0 is at once); the new state then lasts a normal dwell. */\n force(id: string, blendSeconds?: number): void\n setTimeScale(timeScale: number): void\n setAutoSchedule(isOn: boolean): void\n /** What a fresh world with these options does over `hours` game hours; the live world is not touched. */\n timeline(hours: number, sampleMinutes?: number): WeatherTimeline\n on<TEvent extends keyof WeatherEventMap>(event: TEvent, listener: (payload: WeatherEventMap[TEvent]) => void): () => void\n}\n\ntype Blend = { startedAt: number; duration: number }\n\nconst clamp01 = (value: number): number => Math.min(1, Math.max(0, value))\n\nfunction requireClimate(registry: WeatherRegistry, id: string): Climate {\n const climate = registry.climate(id)\n if (!climate) throw new Error(`game-weather: unknown climate \"${id}\"`)\n return climate\n}\n\nfunction requireState(registry: WeatherRegistry, id: string): WeatherState {\n const state = registry.state(id)\n if (!state) throw new Error(`game-weather: unknown state \"${id}\"`)\n return state\n}\n\nexport function createWeather(options: WeatherOptions = {}): Weather {\n const registry = options.registry ?? createWeatherRegistry(options)\n const seed = options.seed ?? 1\n const climateId = options.climate ?? 'temperate'\n const startClimate = requireClimate(registry, climateId)\n const startState = requireState(registry, options.start ?? startClimate.start)\n const startHour = options.startHour ?? DEFAULT_START_HOUR\n const scheduler = createScheduler(startClimate, seed, (id) => registry.state(id) !== undefined)\n const lightning = createLightningStream(seed)\n const wetness = createWetness(options.wetness)\n const listeners = new Map<keyof WeatherEventMap, Set<(payload: never) => void>>()\n\n let timeScale = options.timeScale ?? DEFAULT_TIME_SCALE\n let isAutoSchedule = options.autoSchedule ?? true\n let gameSeconds = 0\n let simSeconds = 0\n let target = startState\n let targetValues = valuesOf(startState)\n let fromValues: StateValues = targetValues\n let previousId: string | undefined\n let blend: Blend = { startedAt: 0, duration: 0 }\n let nextStrikeAt: number | undefined\n let strikeCount = 0\n let lastStrike: Strike | undefined\n const pendingThunder: Strike[] = []\n\n const climate = (): Climate => registry.climate(climateId) ?? startClimate\n\n const emit = <TEvent extends keyof WeatherEventMap>(event: TEvent, payload: WeatherEventMap[TEvent]): void => {\n for (const listener of [...(listeners.get(event) ?? [])]) (listener as (value: WeatherEventMap[TEvent]) => void)(payload)\n }\n\n const progressAt = (at: number): number => (blend.duration <= 0 ? 1 : clamp01((at - blend.startedAt) / blend.duration))\n const valuesAt = (at: number): StateValues => blendValues(fromValues, targetValues, progressAt(at))\n\n const scheduleFirstStrike = (atSim: number): void => {\n nextStrikeAt = target.lightning ? atSim + lightning.first(target.lightning) : undefined\n }\n\n const enter = (id: string, atGame: number, atSim: number, blendGameSeconds: number): void => {\n const next = requireState(registry, id)\n fromValues = valuesAt(atGame)\n previousId = target.id\n target = next\n targetValues = valuesOf(next)\n blend = { startedAt: atGame, duration: blendGameSeconds }\n scheduleFirstStrike(atSim)\n emit('transition', { from: previousId, to: id, gameSeconds: atGame })\n }\n\n const strikeAt = (atSim: number, startGame: number, startSim: number): void => {\n const config = target.lightning!\n const place = lightning.place(config)\n const strike: Strike = {\n id: strikeCount++,\n time: atSim,\n gameSeconds: startGame + (atSim - startSim) * timeScale,\n distance: place.distance,\n bearing: place.bearing,\n thunderDelay: thunderDelay(place.distance),\n }\n lastStrike = strike\n pendingThunder.push(strike)\n pendingThunder.sort((a, b) => a.time + a.thunderDelay - (b.time + b.thunderDelay))\n nextStrikeAt = atSim + lightning.interval(config)\n emit('strike', strike)\n }\n\n scheduleFirstStrike(0)\n\n const runEvents = (startGame: number, startSim: number, endSim: number): void => {\n for (;;) {\n const changeSim = isAutoSchedule ? Math.max(startSim, startSim + (scheduler.nextChangeAt() - startGame) / timeScale) : Infinity\n const strikeSim = nextStrikeAt ?? Infinity\n const thunder = pendingThunder[0]\n const thunderSim = thunder ? thunder.time + thunder.thunderDelay : Infinity\n const earliest = Math.min(changeSim, strikeSim, thunderSim)\n if (earliest > endSim) return\n if (earliest === changeSim) {\n const atGame = Math.max(startGame, scheduler.nextChangeAt())\n const next = scheduler.next(target.id, atGame)\n enter(next, atGame, changeSim, climate().blendMinutes * SECONDS_PER_MINUTE)\n } else if (earliest === strikeSim) strikeAt(strikeSim, startGame, startSim)\n else emit('thunder', pendingThunder.shift()!)\n }\n }\n\n const wet = (startGame: number, endGame: number, seconds: number): void => {\n const steps = Math.max(1, Math.ceil(seconds / WETNESS_STEP_SECONDS))\n for (let step = 1; step <= steps; step++) {\n const values = valuesAt(startGame + ((endGame - startGame) * step) / steps)\n wetness.update(seconds / steps, values.wetness, values.dryingScale)\n }\n }\n\n const api: Weather = {\n registry,\n advance(dt) {\n if (!(dt > 0)) return\n const startGame = gameSeconds\n const startSim = simSeconds\n const endGame = startGame + dt * timeScale\n runEvents(startGame, startSim, startSim + dt)\n gameSeconds = endGame\n simSeconds = startSim + dt\n wet(startGame, endGame, dt)\n },\n sample() {\n const values = valuesAt(gameSeconds)\n const dayFraction = dayFractionAt(startHour, gameSeconds)\n const { sun, nightFactor } = daylight(dayFraction)\n const current = climate()\n const temperatureC = current.temperatureC.mean + current.temperatureC.swing * (2 * sun - 1) + values.temperatureDeltaC\n const humidityPercent = clamp01(values.humidity + current.humidityOffset) * 100\n const progress = progressAt(gameSeconds)\n return {\n ...values,\n gameSeconds,\n dayFraction,\n hour: dayFraction * 24,\n nightFactor,\n sun,\n state: target.id,\n previous: progress < 1 ? previousId : undefined,\n blend: progress,\n front: { direction: current.frontDirection, progress },\n windSpeed: Math.sqrt(values.windX * values.windX + values.windZ * values.windZ),\n temperatureC,\n humidityPercent,\n groundWetness: wetness.value(),\n flash: lastStrike ? flashIntensity(simSeconds - lastStrike.time) : 0,\n effects: deriveEffects({ values, nightFactor, temperatureC, humidityPercent, lightningReveal: target.lightning?.reveal ?? 0 }),\n }\n },\n state: () => target.id,\n force(id, blendSeconds = 0) {\n enter(id, gameSeconds, simSeconds, blendSeconds * timeScale)\n scheduler.restart(gameSeconds)\n },\n setTimeScale(next) {\n if (!(next > 0)) throw new Error('game-weather: timeScale must be above 0')\n timeScale = next\n },\n setAutoSchedule(isOn) {\n if (isOn && !isAutoSchedule) scheduler.restart(gameSeconds)\n isAutoSchedule = isOn\n },\n timeline: (hours, sampleMinutes) => createTimeline({ ...options, registry, hours, ...(sampleMinutes === undefined ? {} : { sampleMinutes }) }),\n on(event, listener) {\n const set = listeners.get(event) ?? new Set()\n set.add(listener as (payload: never) => void)\n listeners.set(event, set)\n return () => void set.delete(listener as (payload: never) => void)\n },\n }\n return api\n}\n\nexport type TimelineOptions = WeatherOptions & {\n readonly hours: number\n /** Game minutes between samples. Default 10. */\n readonly sampleMinutes?: number\n}\n\nconst TIMELINE_DECIMALS = 6\nconst round = (value: number): number => Math.round(value * 10 ** TIMELINE_DECIMALS) / 10 ** TIMELINE_DECIMALS + 0\n\n/**\n * What a fresh world does over `hours` game hours, stepping one game minute at a time: the state changes, the strikes (time, distance, bearing) and\n * a sample every `sampleMinutes`. Plain JSON, rounded to 6 decimals, so it can be a golden.\n */\nexport function createTimeline(options: TimelineOptions): WeatherTimeline {\n const weather = createWeather(options)\n const timeScale = options.timeScale ?? DEFAULT_TIME_SCALE\n const sampleMinutes = options.sampleMinutes ?? 10\n const transitions: Array<{ gameSeconds: number; from: string; to: string }> = []\n const strikes: Strike[] = []\n weather.on('transition', (event) => transitions.push({ gameSeconds: round(event.gameSeconds), from: event.from, to: event.to }))\n weather.on('strike', (strike) =>\n strikes.push({ id: strike.id, time: round(strike.time), gameSeconds: round(strike.gameSeconds), distance: round(strike.distance), bearing: round(strike.bearing), thunderDelay: round(strike.thunderDelay) }),\n )\n const samples: Array<Record<string, number | string>> = []\n const record = (minute: number): void => {\n const sample = weather.sample()\n samples.push({\n at: minute * SECONDS_PER_MINUTE,\n hour: round(sample.hour),\n state: sample.state,\n blend: round(sample.blend),\n rain: round(sample.rain),\n windSpeed: round(sample.windSpeed),\n gust: round(sample.gust),\n visibility: round(sample.effects.visibility),\n sightScale: round(sample.effects.sightScale),\n hearingScale: round(sample.effects.hearingScale),\n temperatureC: round(sample.temperatureC),\n heatIndexC: round(sample.effects.heatIndexC),\n heatStress: round(sample.effects.heatStress),\n wetness: round(sample.groundWetness),\n nightFactor: round(sample.nightFactor),\n })\n }\n record(0)\n const minutes = Math.round(options.hours * 60)\n for (let minute = 1; minute <= minutes; minute++) {\n weather.advance(SECONDS_PER_MINUTE / timeScale)\n if (minute % sampleMinutes === 0) record(minute)\n }\n return {\n seed: options.seed ?? 1,\n climate: options.climate ?? 'temperate',\n start: options.start ?? weather.registry.climate(options.climate ?? 'temperate')!.start,\n hours: options.hours,\n sampleMinutes,\n transitions,\n strikes,\n samples,\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 { blastShove } from './blast-shove'\nimport type { BlastShoveOptions, BlastSource } from './blast-shove'\nimport type { Vec3 } from './destruction'\n\n/** From this speed (m/s) a blast takes a standing body off its feet. Below it the body is only pushed. */\nexport const KNOCK_DOWN_SPEED = 2.5\n/** The angle of a body lying where it fell (the same tilt a dead body takes). */\nexport const KNOCKED_DOWN_ANGLE = 1.35\n/** Seconds the body lies after it lands: this plus the landing speed times `STAND_UP_PER_SPEED`. */\nconst LIE_SECONDS = 1.6\nconst STAND_UP_PER_SPEED = 0.25\nconst FALL_SECONDS = 0.25\nconst RISE_SECONDS = 0.6\n/** m/s squared that slows a body sliding on the ground. */\nconst GROUND_FRICTION = 7\nconst GRAVITY = 9.81\n/** Height of the chest above the feet: where the blast takes the body. */\nexport const KNOCK_CHEST = 1.1\nconst CHEST = KNOCK_CHEST\n\n/** What a blast is doing to one body: its velocity (m/s), whether it is in the air, and how far down it is (0 on its feet, 1 lying). */\nexport interface Knock {\n velocity: [number, number, number]\n airborne: boolean\n /** 0..1 */\n lying: number\n /** Seconds left to lie on the ground after it stopped. */\n lieLeft: number\n /** Knocked off its feet by this blast. */\n down: boolean\n}\n\n/** The mass of a body in kg from its height (m) and its build (1 = average): 70 kg at 1.75 m. */\nexport const bodyMass = (height: number, build = 1): number => 70 * (height / 1.75) ** 2 * build\n\n/**\n * What a blast does to a body standing at `feet`: the shove of `game-contracts` taken at the chest, a hop when it goes up, and a fall when it is strong enough. Undefined when the blast does not\n * reach it. The numbers are the host's (`ModuleHost.blastShove`), the same ones that throw its loose items.\n */\nexport function knockOf(blast: BlastSource, feet: Vec3, mass: number, options?: BlastShoveOptions): Knock | undefined {\n const hit = blastShove(blast, { position: [feet[0], feet[1] + CHEST, feet[2]], mass }, options)\n if (!hit) return undefined\n const [dx, dy, dz] = hit.direction\n return { velocity: [dx * hit.speed, Math.max(0, dy * hit.speed), dz * hit.speed], airborne: dy > 0, lying: 0, lieLeft: 0, down: hit.speed >= KNOCK_DOWN_SPEED }\n}\n\n/**\n * Moves a body one step: it slides on the ground and slows, falls when it was knocked down, lies a while and gets up (a dead body stays down). Moves `position` in place; returns true when the\n * body is on its feet and still, and the knock can be dropped.\n */\nexport function stepKnock(knock: Knock, position: { x: number; y: number; z: number }, dt: number, groundAt: (x: number, z: number) => number, alive: boolean): boolean {\n const v = knock.velocity\n position.x += v[0] * dt\n position.z += v[2] * dt\n const ground = groundAt(position.x, position.z)\n if (knock.airborne || position.y > ground + 1e-4) {\n v[1] -= GRAVITY * dt\n position.y += v[1] * dt\n if (position.y <= ground) {\n position.y = ground\n v[1] = 0\n knock.airborne = false\n }\n } else position.y = ground\n const horizontal = Math.hypot(v[0], v[2])\n if (!knock.airborne && horizontal > 0) {\n const slowed = Math.max(0, horizontal - GROUND_FRICTION * dt)\n v[0] *= slowed / horizontal\n v[2] *= slowed / horizontal\n }\n const moving = horizontal > 0.05 || knock.airborne\n if (knock.down) {\n if (moving) knock.lying = Math.min(1, knock.lying + dt / FALL_SECONDS)\n else {\n knock.lying = 1\n if (alive) {\n knock.lieLeft = knock.lieLeft > 0 ? knock.lieLeft : LIE_SECONDS + Math.hypot(v[0], v[2]) * STAND_UP_PER_SPEED\n knock.lieLeft -= dt\n if (knock.lieLeft <= 0) knock.down = false\n }\n }\n } else if (knock.lying > 0) knock.lying = Math.max(0, knock.lying - dt / RISE_SECONDS)\n return !moving && !knock.down && knock.lying === 0\n}\n","export interface CostOptions {\n /** Reads of `state()` and of `describe()` to time. Default 10 000. */\n reads?: number\n /** Ceiling, in milliseconds, for all the reads of one method. Default 50 (5 microseconds per read). */\n ceilingMs?: number\n /** Ceiling of heap growth, in bytes, over all the reads of one method (only where `process.memoryUsage` exists). Default 1 MiB. */\n allocationBytes?: number\n}\n\nconst DEFAULT_COST_READS = 10_000\nconst DEFAULT_COST_CEILING_MS = 50\nconst DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024\n\nconst heapUsedBytes = (): number | undefined => {\n const nodeProcess = (globalThis as { process?: { memoryUsage?: () => { heapUsed: number } } }).process\n return nodeProcess?.memoryUsage?.().heapUsed\n}\n\ntype CheckReadCostParams = { readonly name: string; readonly read: () => unknown; readonly options: CostOptions; readonly problems: string[] }\n\nexport const checkReadCost = ({ name, read, options, problems }: CheckReadCostParams): void => {\n const reads = options.reads ?? DEFAULT_COST_READS\n const ceilingMs = options.ceilingMs ?? DEFAULT_COST_CEILING_MS\n const allocationBytes = options.allocationBytes ?? DEFAULT_COST_ALLOCATION_BYTES\n if (read() !== read()) problems.push(`${name}() returns a new object on each read: cache it between changes (cost contract)`)\n // a first pass lets the engine compile and optimise the loop: that code and its feedback are heap too, and would read as the method allocating\n for (let i = 0; i < reads; i++) read()\n const heapBefore = heapUsedBytes()\n const started = performance.now()\n for (let i = 0; i < reads; i++) read()\n const elapsedMs = performance.now() - started\n const heapAfter = heapUsedBytes()\n if (elapsedMs > ceilingMs) problems.push(`${name}() took ${elapsedMs.toFixed(1)} ms for ${reads} reads, over the ${ceilingMs} ms ceiling (cost contract)`)\n if (heapBefore !== undefined && heapAfter !== undefined && heapAfter - heapBefore > allocationBytes)\n problems.push(`${name}() grew the heap by ${heapAfter - heapBefore} bytes over ${reads} reads, over the ${allocationBytes} byte ceiling (cost contract)`)\n}\n","import { checkReadCost } from './impact-cost'\nimport type { CostOptions } from './impact-cost'\nimport 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 */\n/** World-space ray for `DestructibleImpl.surfaceRay`. */\nexport interface SurfaceRay {\n readonly origin: Vec3\n readonly direction: Vec3\n readonly maxDistance: number\n ignored?(part: string): boolean\n}\n\nexport interface SurfaceRayHit {\n readonly part: string\n readonly point: Vec3\n readonly normal: Vec3\n readonly distance: number\n /** Thickness the ray crosses in this part, metres. */\n readonly pathLen: number\n}\n\nexport interface DestructibleImpl {\n readonly id: string\n readonly name?: string\n /**\n * Parts, joints and structure in the impl's local space, for raycast, inspector and log.\n * COST CONTRACT (D8): called per frame by the renderer, so it is O(1), allocation-free and cached between changes: the same object comes\n * back until a hit, tool, fire or pose change alters it. `checkDestructible(impl, { cost: true })` measures it.\n */\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 /**\n * The real surface a ray meets, for an impl whose drawn shape is not its boxes (a car body): the boxes of `describe()` only say where to look, and a shot that crosses\n * them without touching the drawn surface goes on. `ignored` answers for the parts the round already went through. Null when the ray meets nothing.\n */\n surfaceRay?(ray: SurfaceRay): SurfaceRayHit | null\n /** Advance fire, melting, collapse and other timed behaviour. */\n update?(dt: number): DestructionEvent[]\n /**\n * COST CONTRACT (D8): the host reads it per part per frame, so it is O(1), allocation-free and cached between changes (see `describe()`).\n * The cached object is shared with every reader: treat it as read-only, copy before changing it.\n */\n state(): Readonly<ObjectState>\n reset?(): void\n /** The world took the object out (`DestructionWorld.remove`): the impl lets go of what it joined (shared queues, listeners). */\n dispose?(): 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 /** Opt-in cost measurement (D8): `true` for the defaults, or the limits to use. */\n cost?: boolean | CostOptions\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). With `options.cost` it also times `state()` and `describe()` (the cost contract of D8).\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 if (options.cost) {\n const cost = options.cost === true ? {} : options.cost\n try {\n checkReadCost({ name: 'state', read: () => impl.state(), options: cost, problems })\n checkReadCost({ name: 'describe', read: () => impl.describe(), options: cost, problems })\n } catch (e) {\n problems.push(`cost measurement threw: ${String(e)}`)\n }\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","/**\n * The seal of a closed container (a food tin, a drink can, a jar, a bottle with a cap): what opens it and whether a breach spoils what is inside.\n * Plain data and one pure check, shared by the item rules (`resolveUse` of game-items) and any host that lets the player open and eat (the test room).\n */\nexport interface SealSpec {\n /** Ids of the tools that open it (items or weapons: `can_opener`, `knife`); any one of them is enough. */\n readonly openWith?: readonly string[]\n /** It opens by hand (a pull tab, a ring pull, a screw cap): no tool needed. */\n readonly byHand?: boolean\n /** A hole through the container or a leak spoils what is inside: it can no longer be eaten or drunk. */\n readonly spoilsWhenBreached?: boolean\n}\n\n/** The state of one container: opened, holed through (a shot, a blade) or leaking. */\nexport interface SealState {\n readonly opened?: boolean\n readonly breached?: boolean\n readonly leaked?: boolean\n}\n\n/** Why a sealed container cannot be eaten or drunk now. */\nexport type SealBlock = 'sealed' | 'breached' | 'leaked'\n\n/** Whether it can be opened with the tools at hand, and with which one (undefined when it opens by hand). */\nexport const sealOpener = (seal: SealSpec, tools: Iterable<string>): { readonly ok: boolean; readonly tool?: string } => {\n if (seal.byHand || !seal.openWith || seal.openWith.length === 0) return { ok: true }\n const have = new Set(tools)\n const tool = seal.openWith.find((id) => have.has(id))\n return tool === undefined ? { ok: false } : { ok: true, tool }\n}\n\n/** What stops eating or drinking it now, or undefined when nothing does. A spoiled container (breached, leaked) is blocked even once open. */\nexport const sealBlock = (seal: SealSpec, state: SealState): SealBlock | undefined => {\n if (seal.spoilsWhenBreached && state.breached) return 'breached'\n if (seal.spoilsWhenBreached && state.leaked) return 'leaked'\n if (!state.opened) return 'sealed'\n return undefined\n}\n","/** How a hand turns something round in strokes (a screw cap, a tap handle, a knob): what `twistStroke` answers for one moment of the turning. */\nexport interface TwistStroke {\n /** Radians the wrist is turned from the middle of its stroke, positive the way the part turns. */\n readonly angle: number\n /** Radians the part has turned in all, from 0 to `strokes * reach`. */\n readonly turned: number\n /** How closed the fingers are, 0 open to 1 closed: closed on the part while it turns, open while the wrist turns back. */\n readonly grip: number\n /** 0..1, how far the hand rises off the part while the wrist turns back (1 is `TWIST_LIFT` metres). */\n readonly lift: number\n}\n\nexport interface TwistOptions {\n /** Strokes of the wrist. Default 3. */\n readonly strokes?: number\n /** Radians the part turns in one stroke (a wrist turns about a quarter round with its hold). Default 1.6. */\n readonly reach?: number\n}\n\n/** Metres a hand rises off the part between two strokes. */\nexport const TWIST_LIFT = 0.012\n/** Share of each stroke spent turning the part; the rest turns the wrist back open. */\nconst TURNING = 0.7\n\nconst smooth = (k: number): number => k * k * (3 - 2 * k)\n\n/**\n * The hand turning a part round at `share` (0..1) of the whole turning: in each stroke the wrist turns with the part, then lets go, rises a little and turns back.\n * Pure and the same for every host, so first and third person turn the same hand the same way.\n */\nexport const twistStroke = (share: number, options: TwistOptions = {}): TwistStroke => {\n const strokes = Math.max(1, Math.round(options.strokes ?? 3))\n const reach = options.reach ?? 1.6\n const along = Math.min(1, Math.max(0, share)) * strokes\n const index = Math.min(strokes - 1, Math.floor(along))\n const phase = along - index\n if (phase <= TURNING) {\n const k = smooth(phase / TURNING)\n return { angle: reach * (k - 0.5), turned: reach * (index + k), grip: 1, lift: 0 }\n }\n const k = (phase - TURNING) / (1 - TURNING)\n return { angle: reach * (0.5 - smooth(k)), turned: reach * (index + 1), grip: 0.35, lift: Math.sin(k * Math.PI) }\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 * Seconds of real time the event fires before `t`, not stretched with the clip: a recorded sound whose hit comes that long after its start\n * (the click of a magazine seating) starts early so the hit lands on `t`. The player passes the lead it could apply as `data.lead`.\n */\n lead?: number\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\n/**\n * The clip with every event that has a `lead` moved that many seconds earlier, the lead it got in `data.lead`. A led event never goes before 0\n * nor before the event written ahead of it (a fast clip keeps its order: the lead shrinks). Run after `scaleClip`: the lead is real time, the\n * clip's stretch does not touch it.\n */\nexport const leadClipEvents = (clip: AnimationClipSpec): AnimationClipSpec => {\n const events = clip.events ?? []\n if (!events.some((event) => (event.lead ?? 0) > 0)) return clip\n let floor = 0\n const led = events.map((event) => {\n const lead = Math.max(0, Math.min(event.t - floor, event.lead ?? 0))\n const t = event.t - lead\n floor = Math.max(floor, t)\n if (event.lead === undefined) return event\n const moved: AnimationEventSpec = { ...event, t, data: lead > 0 ? { ...event.data, lead } : { ...event.data } }\n delete moved.lead\n return moved\n })\n return { ...clip, events: led }\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 = leadClipEvents(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","export const FRAME_PHASES = ['input', 'fixedSimulate', 'lateSimulate', 'present', 'render', 'idle'] as const\nexport type FramePhase = (typeof FRAME_PHASES)[number]\n\n/**\n * Presentation work a system leaves for later frames, run in small units within a shared time budget.\n * Never simulation: nothing that consumes a seeded generator or decides an outcome may wait in a queue.\n */\nexport interface DeferredQueue {\n /** Stable name for stats and logs (see `DEFERRED_QUEUE_ID`). */\n readonly id: string\n /**\n * Units waiting. 0 means idle; the budget skips the queue without calling `run`. `frame` is the budget's frame counter: a queue may\n * report 0 for the rest of a frame (the crease queue does in the frame a panel was torn). Same value for repeated calls in one frame.\n */\n pending(frame?: number): number\n /**\n * Runs units in order until `clock() >= deadline`. Contract: when `pending() > 0` it runs **at least one** unit, even with a deadline\n * already past; it never runs a unit after it saw the deadline pass. Returns how many units it ran.\n */\n run(deadline: number, clock: () => number): number\n /** Runs everything now (synchronous step, clear, dispose). */\n flush(): void\n /**\n * Called by `budget.register`: from now on the owner stops draining on its own (no MessageChannel, no drain inside `update`).\n * Never called by a getter; a queue nobody registered keeps today's internal draining.\n */\n attach?(): void\n /** Called by the unregister function and by `budget.dispose`: the owner goes back to its internal draining. */\n detach?(): void\n}\n\n/** Where a module registers a deferred queue with the host's budget. */\nexport interface DeferredQueueOptions {\n /** Higher runs first. See DEFERRED_PRIORITY. */\n readonly priority: number\n /** `present` runs before the render (the work must show this frame); `idle` runs after it. Default `idle`. */\n readonly phase?: 'present' | 'idle'\n /** Starvation guard: the queue runs at least one unit every this many frames, even with the budget spent. Default 4. */\n readonly minEveryFrames?: number\n /** Milliseconds of every frame kept for this queue before the others share the rest (when it has work). Default 0. */\n readonly reserveMs?: number\n}\n\n/** The ids of the kit's queues: what `DeferredQueue.id` says and what `DEFERRED_PRIORITY` is keyed by. */\nexport const DEFERRED_QUEUE_ID = {\n DESTRUCTION_EVENTS: 'destruction.events',\n BREACH_PAINT: 'floorplan.breach-paint',\n AUDIO_VOICES: 'audio.voices',\n VEHICLE_CREASE: 'vehicles.crease',\n} as const\n\n/** Default priorities and reserves of the kit's queues; a host may override any of them. */\nexport const DEFERRED_PRIORITY = {\n [DEFERRED_QUEUE_ID.DESTRUCTION_EVENTS]: { priority: 300, phase: 'present' },\n [DEFERRED_QUEUE_ID.BREACH_PAINT]: { priority: 250, phase: 'present' },\n [DEFERRED_QUEUE_ID.AUDIO_VOICES]: { priority: 200, phase: 'idle', reserveMs: 1 },\n [DEFERRED_QUEUE_ID.VEHICLE_CREASE]: { priority: 100, phase: 'idle' },\n} as const satisfies Readonly<Record<string, DeferredQueueOptions>>\n\nexport interface DeferredQueueReport {\n readonly ok: boolean\n readonly problems: readonly string[]\n}\n\n/**\n * Checks the `DeferredQueue` contract. `fill` leaves the queue with at least two units waiting (the same call is made on a fresh queue).\n * It cannot prove the queue only carries presentation work: that is a rule of review.\n */\nexport function checkDeferredQueue(queue: DeferredQueue, fill: () => void): DeferredQueueReport {\n const problems: string[] = []\n fill()\n const waiting = queue.pending(1)\n if (waiting < 2) problems.push(`fill() must leave at least 2 units waiting (pending is ${waiting})`)\n if (queue.pending(1) !== waiting) problems.push('pending(frame) changed between two calls in the same frame')\n const ran = queue.run(0, () => 1)\n if (ran !== 1) problems.push(`run with a deadline already past must run exactly one unit (ran ${ran})`)\n if (waiting >= 2 && queue.pending(2) !== waiting - 1) problems.push(`one unit ran but pending went from ${waiting} to ${queue.pending(2)}`)\n try {\n queue.attach?.()\n queue.attach?.()\n queue.detach?.()\n queue.detach?.()\n } catch (error) {\n problems.push(`attach/detach must be idempotent: ${error instanceof Error ? error.message : String(error)}`)\n }\n queue.flush()\n if (queue.pending(3) !== 0) problems.push(`flush must leave nothing pending (pending is ${queue.pending(3)})`)\n if (queue.run(0, () => 1) !== 0) problems.push('run on an empty queue must run no unit')\n return { ok: problems.length === 0, problems }\n}\n","const WARNED_KEY = Symbol.for('@adatechnology/game-contracts.warned')\n\ntype WarnedHolder = { [WARNED_KEY]?: Set<string> }\n\nconst warnedKeys = (): Set<string> => {\n const holder = globalThis as WarnedHolder\n return (holder[WARNED_KEY] ??= new Set<string>())\n}\n\ndeclare const process: { readonly env: { readonly NODE_ENV?: string } }\n\n// The literal `process.env.NODE_ENV` is what bundlers replace; where there is no `process` the reference throws and counts as development.\nconst isProduction = (): boolean => {\n try {\n return process.env.NODE_ENV === 'production'\n } catch {\n return false\n }\n}\n\nconst isQuiet = (): boolean => (globalThis as { __GAME_KIT_QUIET__?: unknown }).__GAME_KIT_QUIET__ === true\n\n/**\n * Warns on the console the first time `key` is seen, and only outside production and when `globalThis.__GAME_KIT_QUIET__` is not `true`: for a silent fallback that keeps working (an adapter that made its own effects\n * world or audio engine because the host lent none). The seen keys live on `globalThis`, so several bundles of the kit on one page warn once together.\n */\nexport const warnOnce = (key: string, message: string): void => {\n if (isQuiet() || isProduction()) return\n const seen = warnedKeys()\n if (seen.has(key)) return\n seen.add(key)\n console.warn(`[game-kit] ${message}`)\n}\n\n/** Forgets what `warnOnce` has seen (tests that count warnings). */\nexport const resetWarnOnce = (): void => warnedKeys().clear()\n","import { warnOnce } from './warn'\n\ntype 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 (and warned about once, in development). */\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 else warnOnce(`registry.patch.${id}`, `patch for \"${id}\" ignored: no entry has that id`)\n }\n}\n\n/** Takes the ids out; an id that does not exist is ignored (and warned about once, in development). */\nexport const removeIds = (entries: Map<string, unknown>, ids: readonly string[] | undefined): void => {\n for (const id of ids ?? []) {\n if (!entries.delete(id)) warnOnce(`registry.remove.${id}`, `remove of \"${id}\" ignored: no entry has that id`)\n }\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","/**\n * The sounds of a body (footsteps, jumps, landings, blows, hurt), as ids every package agrees on: the rule that knows what the body did\n * (`game-player`, a host's own player) names the sound, and the bank that knows how it sounds (`game-audio`'s `BODY_FOLEY_BANK`) answers it.\n * A game swaps the sound of an id (a sample instead of the synthesis) without touching the rule.\n */\n\n/** What the feet touch, by how it sounds (not by what it is made of: a wooden floor and a plywood board both sound `wood`). */\nexport const SURFACE_IDS = ['concrete', 'asphalt', 'wood', 'tile', 'dirt', 'grass', 'gravel', 'sand', 'metal', 'water', 'glass', 'carpet'] as const\nexport type SurfaceId = (typeof SURFACE_IDS)[number]\n\n/** The surface a body stands on when nobody says (the test room's floor is a concrete slab). */\nexport const DEFAULT_SURFACE: SurfaceId = 'concrete'\n\n/** How the feet move: a run hits harder and shorter; a crouched step is the walk, quieter. */\nexport const GAIT_IDS = ['walk', 'run'] as const\nexport type GaitId = (typeof GAIT_IDS)[number]\n\n/** How hard a landing is: `light` for a jump on the spot or a step down (up to about 5 m/s), `heavy` for a drop of more than a metre and a half. */\nexport const LANDING_IDS = ['light', 'heavy'] as const\nexport type LandingId = (typeof LANDING_IDS)[number]\n\n/** The fall speed (m/s) from which a landing is `heavy`. */\nexport const HEAVY_LANDING_SPEED = 5.5\n\nexport type FootstepSoundId = `footstep_${SurfaceId}_${GaitId}`\nexport type LandSoundId = `land_${SurfaceId}_${LandingId}`\n\n/**\n * How a blow is struck, by the body part or the tool that does it. `blade` stabs or slashes, `blunt` is a club, a bat, a pipe or a hammer, `chop` an\n * axe or a machete driven down.\n */\nexport const BLOW_IDS = ['punch', 'kick', 'shove', 'blade', 'blunt', 'chop'] as const\nexport type BlowId = (typeof BLOW_IDS)[number]\n\n/** What the blow meets: a body (flesh and bone, the head apart), something else (the material answers with its own impact), or nothing. */\nexport type BlowTarget = 'body' | 'head' | 'object' | 'air'\n\n/** The sounds of the body that are not a footstep or a landing. */\nexport const BODY_SOUND_IDS = [\n 'jump',\n 'cloth_rustle',\n 'body_fall',\n 'fall_damage',\n 'player_hurt',\n 'player_breath_run',\n 'player_death',\n 'bite',\n 'grab',\n 'punch_swing',\n 'punch_hit_body',\n 'punch_hit_face',\n 'punch_hit_object',\n 'kick_swing',\n 'kick_hit',\n 'kick_hit_object',\n 'shove_two_hands',\n 'knife_swing',\n 'knife_stab',\n 'knife_slash',\n 'knife_hit_object',\n 'blunt_swing',\n 'blunt_hit',\n 'blunt_hit_object',\n 'axe_swing',\n 'axe_chop',\n 'axe_hit_object',\n] as const\nexport type BodySoundId = (typeof BODY_SOUND_IDS)[number]\n\n/** Every sound of a body: what a host plays for the player, a survivor or a zombie. */\nexport type BodyFoleyId = FootstepSoundId | LandSoundId | BodySoundId\n\nexport const footstepSoundId = (surface: SurfaceId, gait: GaitId): FootstepSoundId => `footstep_${surface}_${gait}`\nexport const landSoundId = (surface: SurfaceId, fallSpeed: number): LandSoundId => `land_${surface}_${fallSpeed >= HEAVY_LANDING_SPEED ? 'heavy' : 'light'}`\n\n/** Every footstep and landing id, in the order of the surfaces. */\nexport const FOOTSTEP_SOUND_IDS: readonly FootstepSoundId[] = SURFACE_IDS.flatMap((surface) => GAIT_IDS.map((gait) => footstepSoundId(surface, gait)))\nexport const LAND_SOUND_IDS: readonly LandSoundId[] = SURFACE_IDS.flatMap((surface) => LANDING_IDS.map((landing): LandSoundId => `land_${surface}_${landing}`))\nexport const BODY_FOLEY_IDS: readonly BodyFoleyId[] = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS]\n\nconst SWING: Readonly<Record<BlowId, BodySoundId | null>> = {\n punch: 'punch_swing',\n kick: 'kick_swing',\n // two hands pushed out do not whistle: the shove is one sound, at the contact\n shove: null,\n blade: 'knife_swing',\n blunt: 'blunt_swing',\n chop: 'axe_swing',\n}\n\n/**\n * Seconds from the start of a swing's sound to its loudest point (the limb or the tool at full speed, just before the contact). A host that knows when the\n * blow lands starts the swing this long before; one that does not plays the swing now and the hit this long after.\n */\nexport const SWING_LEAD_SECONDS: Readonly<Record<BlowId, number>> = { punch: 0.07, kick: 0.09, shove: 0, blade: 0.05, blunt: 0.1, chop: 0.11 }\n\n/** The swing through the air, played as the blow starts (`null`: the blow has none). */\nexport const swingSoundId = (blow: BlowId): BodySoundId | null => SWING[blow]\n\nconst HIT: Readonly<Record<BlowId, Readonly<Record<Exclude<BlowTarget, 'air'>, BodySoundId>>>> = {\n punch: { body: 'punch_hit_body', head: 'punch_hit_face', object: 'punch_hit_object' },\n kick: { body: 'kick_hit', head: 'kick_hit', object: 'kick_hit_object' },\n shove: { body: 'shove_two_hands', head: 'shove_two_hands', object: 'shove_two_hands' },\n blade: { body: 'knife_stab', head: 'knife_stab', object: 'knife_hit_object' },\n blunt: { body: 'blunt_hit', head: 'blunt_hit', object: 'blunt_hit_object' },\n chop: { body: 'axe_chop', head: 'axe_chop', object: 'axe_hit_object' },\n}\n\n/**\n * The body's side of a blow that lands: the knuckles, the shoe, the blade or the club meeting what it hit. On an object the material's own impact\n * plays too (the wood, the metal); this is the part that says it was a punch and not a shot.\n */\nexport const hitSoundId = (blow: BlowId, target: Exclude<BlowTarget, 'air'>): BodySoundId => HIT[blow][target]\n\n/**\n * The surface a material sounds like underfoot, by the material ids of `game-destruction` and `game-terrain` (unknown ids: `fallback`). Data, so\n * a game with its own materials passes its own table to `surfaceOfMaterial`.\n */\nexport const MATERIAL_SURFACES: Readonly<Record<string, SurfaceId>> = {\n concrete: 'concrete',\n mortar: 'concrete',\n stone: 'concrete',\n brick: 'concrete',\n plaster: 'concrete',\n asphalt: 'asphalt',\n wood_soft: 'wood',\n wood_hard: 'wood',\n green_wood: 'wood',\n plywood: 'wood',\n mdf: 'wood',\n ceramic: 'tile',\n plastic: 'tile',\n soil: 'dirt',\n wax: 'dirt',\n flesh_fruit: 'dirt',\n turf: 'grass',\n foliage: 'grass',\n cactus: 'grass',\n gravel: 'gravel',\n sand: 'sand',\n steel: 'metal',\n cast_iron: 'metal',\n aluminium: 'metal',\n copper: 'metal',\n water: 'water',\n glass: 'glass',\n tempered_glass: 'glass',\n fabric: 'carpet',\n foam: 'carpet',\n leather: 'carpet',\n rubber: 'carpet',\n paper: 'carpet',\n}\n\nexport const isSurfaceId = (value: unknown): value is SurfaceId => typeof value === 'string' && (SURFACE_IDS as readonly string[]).includes(value)\n\n/** The surface of a material id; a surface id passes through. */\nexport function surfaceOfMaterial(material: string | undefined, table: Readonly<Record<string, SurfaceId>> = MATERIAL_SURFACES, fallback: SurfaceId = DEFAULT_SURFACE): SurfaceId {\n if (material === undefined) return fallback\n if (isSurfaceId(material)) return material\n return table[material] ?? fallback\n}\n","/**\n * @adatechnology/game-contracts: the contracts every game module (vehicles, furniture, plants, houses, weapons) depends on, and nothing else.\n * Types plus tiny pure checkers, no runtime dependencies. The default destruction world, the renderers and the test room are optional.\n */\nexport type {\n ChainDelay,\n Crack,\n DamageStage,\n DamageType,\n DebrisKind,\n Dent,\n DestructibleSpec,\n BlastOptions,\n BlastReport,\n ConfinementOptions,\n ExplosionChain,\n FragmentHitOptions,\n ShelterOptions,\n DestructionEvent,\n DestructionEventType,\n FluidKind,\n Fragment,\n FragmentMark,\n FractureStyle,\n BallisticExtras,\n Hit,\n HitIgnition,\n HitResult,\n Hole,\n JointKind,\n JointSpec,\n JointState,\n MaterialId,\n ObjectState,\n PartShape,\n PartSpec,\n PartBreach,\n PartState,\n PartStatus,\n PartUnits,\n Quat,\n RayHit,\n Reinforcement,\n TensionRebar,\n TunedMassDamper,\n WindExposure,\n WindState,\n PileSpec,\n SoilLayer,\n SoilProfile,\n SoilState,\n StructureRole,\n SurfaceLayer,\n ToolKind,\n Vec3,\n} from './destruction'\nexport type { BusyOptions, BusySink, BusyTask } from './busy'\nexport { blastShove, DEFAULT_BLAST_SHOVE } from './blast-shove'\nexport { bodyMass, KNOCK_CHEST, KNOCK_DOWN_SPEED, KNOCKED_DOWN_ANGLE, knockOf, stepKnock } from './blast-knock'\nexport type { Knock } from './blast-knock'\nexport type { BlastShove, BlastShoveOptions, BlastSource } from './blast-shove'\nexport { IMPACT_CONTRACT, checkDestructible, checkHit } from './impact'\nexport type { CheckOptions, DestructibleImpl, ImpactContract, SurfaceRay, SurfaceRayHit } from './impact'\nexport type { CostOptions } from './impact-cost'\nexport { MISSING_BITS } from './body'\nexport type { BodyRegion, BodySlot, SegmentKind } from './body'\nexport { checkSpec } from './spec'\nexport type { CheckSpecOptions, SpecReport } from './spec'\nexport { shotToHit, shotExtras, useToHit } from './weapons'\nexport { DEFAULT_IGNITION_RULES, fillFactor, ignitionChance, resolveIgnitionRules, sparkTargetFactor } from './ignition'\nexport type { IgnitionRules } from './ignition'\nexport type { Shot, Use } from './weapons'\nexport type { ContainerSpec, ItemUseDef, LiquidId } from './fluids'\nexport { sealBlock, sealOpener } from './seal'\nexport type { SealBlock, SealSpec, SealState } from './seal'\nexport type { HandPose, HandTargetSource, HandTargets } from './hands'\nexport { TWIST_LIFT, twistStroke } from './twist'\nexport type { TwistOptions, TwistStroke } from './twist'\nexport type { EffectHandle, EffectRequest, EffectSink, Enclosure, EnclosureBox } from './effects'\nexport type { WarmUpProbes } from './warm-up'\nexport { checkAnimation } from './animation'\nexport type { AnimationClipSpec, AnimationEventSpec, AnimationKey, AnimationPose, ItemAnimationContract } from './animation'\nexport { STAGE_EVENT, TOOL_CLIP, checkToolClips, createToolClipPlayer, leadClipEvents, sampleClip, scaleClip, toolAnimationStep } from './tool-animation'\nexport type { ToolAnimationStep, ToolAnimator, ToolClipId, ToolClipPlayer, ToolClipPlayerOptions, ToolClipSet, ToolRig } from './tool-animation'\nexport { DEFERRED_PRIORITY, DEFERRED_QUEUE_ID, FRAME_PHASES, checkDeferredQueue } from './frame'\nexport type { DeferredQueue, DeferredQueueOptions, DeferredQueueReport, FramePhase } from './frame'\nexport type { FrameOptions, AreaPlacement, GroundLayout, GroundZone, HostEvent, HostGraphics, HostTool, HostToolAttachment, ModuleArea, ModuleHost, ModuleView, ModuleItemFluid, ModuleItemSeal, ModuleScenario, PlayerState, ItemSeat, ItemSounds, TestableItem, TestableModule, Thumbnail, ToolCommand, ToolCommandResult, HostReach } from './module'\nexport { applyPatches, deepMerge, mergeById, removeIds } from './registry'\nexport type { DeepPartial } from './registry'\nimport { resetWarnOnce as resetWarnOnceForTests } from './warn'\n\nexport { warnOnce } from './warn'\n/** @deprecated Import it from `@adatechnology/game-contracts/testing`. */\nexport const resetWarnOnce = resetWarnOnceForTests\nexport { checkBench } from './bench'\nexport type {\n ActionControl,\n BenchChange,\n BenchDeclaration,\n BenchReport,\n BenchSheet,\n BooleanControl,\n ChoiceControl,\n ChoiceOption,\n ControlPatch,\n ControlValue,\n ModuleBench,\n ModuleControl,\n ModuleLayer,\n ModuleMetric,\n NumberControl,\n} from './bench'\nexport type { AudioListener, AudioSink, LoopHandle, SoundHandle, SoundRequest } from './audio'\nexport { DEFAULT_ENVIRONMENT, createEnvironmentBus } from './environment'\nexport type { EnvironmentBus, EnvironmentKey, EnvironmentState } from './environment'\nexport { checkCategorized, defineCategories, resolveEntry } from './categories'\nexport type { CategorizedOptions, CategorizedReport, CategoryDef, CategoryOptions, CategoryRegistry, ContractChecker, ContractContext } from './categories'\nexport { KIT_ORIGIN_KEY, kitOriginOf, markKitOrigin, unmarkedDrawings } from './origin'\nexport type { KitOrigin, UnmarkedDrawing } from './origin'\nexport { footprintBlocker } from './footprint'\nexport type { Footprint, FootprintObject } from './footprint'\nexport {\n BLOW_IDS,\n BODY_FOLEY_IDS,\n BODY_SOUND_IDS,\n DEFAULT_SURFACE,\n FOOTSTEP_SOUND_IDS,\n GAIT_IDS,\n HEAVY_LANDING_SPEED,\n LANDING_IDS,\n LAND_SOUND_IDS,\n MATERIAL_SURFACES,\n SURFACE_IDS,\n SWING_LEAD_SECONDS,\n footstepSoundId,\n hitSoundId,\n isSurfaceId,\n landSoundId,\n surfaceOfMaterial,\n swingSoundId,\n} from './foley'\nexport type { BlowId, BlowTarget, BodyFoleyId, BodySoundId, FootstepSoundId, GaitId, LandSoundId, LandingId, SurfaceId } from './foley'\n","/**\n * The manifest the converter (`scripts/sfx-convert.mjs`) writes next to the converted files: every sound id with its takes per layer, the file\n * facts the criteria check (rate, channels, peak, length) and where each file came from. `sampleBankFromManifest` turns it into a `SoundBank`\n * of `SampleSound`s, with the bus and the numbers of each sound given by the caller.\n */\nimport type { BusId, Falloff, SoundBank } from './recipe'\nimport type { SampleCrossover, SampleRef, SampleSound } from './sample'\n\n/** Licences the SDK accepts (the files are redistributed on npm): public domain or attribution only. */\nexport const SDK_LICENSES = ['CC0', 'CC-BY'] as const\nexport type SdkLicense = (typeof SDK_LICENSES)[number]\n\nexport type SfxFileEntry = {\n /** Path from the manifest's folder: `armas/pistol_9mm_close_01.ogg`. */\n readonly file: string\n readonly sampleRate: number\n readonly channels: number\n /** dBFS. */\n readonly peak: number\n readonly seconds: number\n readonly source: string\n readonly author: string\n readonly license: SdkLicense\n}\n\n/** The layers a file name can end in: `<id>_<layer>_<nn>` (`pistol_9mm_close_01`); a name without one is a `close` take. */\nexport const SFX_LAYERS = ['close', 'far', 'tail_in', 'tail_out'] as const\nexport type SfxLayer = (typeof SFX_LAYERS)[number]\n\nexport type SfxManifestSound = {\n readonly id: string\n readonly category: string\n readonly layers: Partial<Readonly<Record<SfxLayer, readonly SfxFileEntry[]>>>\n}\n\nexport type SfxManifest = {\n readonly version: 1\n readonly format: { readonly codec: 'vorbis'; readonly sampleRate: number; readonly channels: number; readonly peakDb: number }\n readonly sounds: readonly SfxManifestSound[]\n}\n\n/** The numbers of one sound that the files do not say (bus, curve, pitch...). */\nexport type SampleDefaults = {\n readonly bus: BusId\n readonly gain?: number\n readonly pitch?: number\n readonly crossover?: SampleCrossover\n readonly tailGain?: number\n readonly falloff?: Falloff\n readonly cutoffK?: number\n readonly group?: string\n readonly priority?: number\n readonly fallback?: string\n}\n\nexport type SampleBankOptions = {\n readonly id: string\n /** Prefix of every url (where the manifest's folder is served: `/sfx/`). Default ''. */\n readonly baseUrl?: string\n /** Numbers per sound id, or one function; a sound with neither is left out (it has no bus). */\n readonly sounds?: Readonly<Record<string, SampleDefaults>>\n readonly defaults?: (sound: SfxManifestSound) => SampleDefaults | undefined\n}\n\n/** The strength suffixes of a sound id (`kick_hit_heavy`): takes of the tiers of `kick_hit`, from these intensities. */\nexport const SFX_TIERS: Readonly<Record<string, number>> = { light: 0, medium: 0.34, heavy: 0.67 }\n\n/** Splits a file name (no extension) into its sound id, layer and take number. */\nexport function parseSfxName(name: string): { readonly id: string; readonly layer: SfxLayer; readonly take: number } {\n const match = /^(.*?)(?:_(close|far|tail_in|tail_out))?(?:_(\\d+))?$/.exec(name)\n const id = match?.[1] || name\n const layer = (match?.[2] as SfxLayer | undefined) ?? 'close'\n const take = match?.[3] ? Number(match[3]) : 1\n return { id, layer, take }\n}\n\n/** The base id and tier of a sound id: `kick_hit_heavy` → `kick_hit`, `heavy`. */\nexport function splitTier(id: string): { readonly base: string; readonly tier?: string } {\n const match = /^(.*)_(light|medium|heavy)$/.exec(id)\n return match ? { base: match[1] as string, tier: match[2] as string } : { base: id }\n}\n\nconst refsOf = (entries: readonly SfxFileEntry[] | undefined, baseUrl: string): SampleRef[] => (entries ?? []).map((entry) => ({ url: `${baseUrl}${entry.file}` }))\n\n/** A bank of sample sounds from a manifest. */\nexport function sampleBankFromManifest(manifest: SfxManifest, options: SampleBankOptions): SoundBank {\n const baseUrl = options.baseUrl ?? ''\n const samples: SampleSound[] = []\n // `<id>_light`, `_medium`, `_heavy` are the tiers of `<id>`, gathered under it\n const tiersOf = new Map<string, SfxManifestSound[]>()\n for (const sound of manifest.sounds) {\n const { base, tier } = splitTier(sound.id)\n if (tier) tiersOf.set(base, [...(tiersOf.get(base) ?? []), sound])\n }\n const bases = new Map<string, SfxManifestSound>()\n for (const sound of manifest.sounds) {\n if (!splitTier(sound.id).tier) bases.set(sound.id, sound)\n }\n for (const [base, tiers] of tiersOf) {\n if (!bases.has(base)) bases.set(base, { id: base, category: tiers[0]?.category ?? '', layers: { close: tiers[0]?.layers.close ?? [] } })\n }\n for (const sound of bases.values()) {\n const numbers = options.sounds?.[sound.id] ?? options.defaults?.(sound)\n const close = refsOf(sound.layers.close, baseUrl)\n if (!numbers || close.length === 0) continue\n const tiers = (tiersOf.get(sound.id) ?? [])\n .map((tier) => {\n const name = splitTier(tier.id).tier as string\n const tierFar = refsOf(tier.layers.far, baseUrl)\n return { name, from: SFX_TIERS[name] ?? 0, close: refsOf(tier.layers.close, baseUrl), ...(tierFar.length > 0 ? { far: tierFar } : {}) }\n })\n .sort((a, b) => a.from - b.from)\n const far = refsOf(sound.layers.far, baseUrl)\n const indoor = refsOf(sound.layers.tail_in, baseUrl)\n const outdoor = refsOf(sound.layers.tail_out, baseUrl)\n const files = [sound, ...(tiersOf.get(sound.id) ?? [])].flatMap((entry) => Object.values(entry.layers).flatMap((entries) => entries ?? []))\n samples.push({\n id: sound.id,\n bus: numbers.bus,\n close,\n ...(far.length > 0 ? { far } : {}),\n ...(tiers.length > 0 ? { tiers } : {}),\n ...(numbers.crossover ? { crossover: numbers.crossover } : {}),\n ...(indoor.length > 0 || outdoor.length > 0\n ? { tail: { ...(indoor.length > 0 ? { indoor } : {}), ...(outdoor.length > 0 ? { outdoor } : {}), ...(numbers.tailGain === undefined ? {} : { gain: numbers.tailGain }) } }\n : {}),\n ...(numbers.pitch === undefined ? {} : { pitch: numbers.pitch }),\n ...(numbers.gain === undefined ? {} : { gain: numbers.gain }),\n ...(numbers.falloff ? { falloff: numbers.falloff } : {}),\n ...(numbers.cutoffK === undefined ? {} : { cutoffK: numbers.cutoffK }),\n ...(numbers.group ? { group: numbers.group } : {}),\n ...(numbers.priority === undefined ? {} : { priority: numbers.priority }),\n ...(numbers.fallback ? { fallback: numbers.fallback } : {}),\n sources: files.map((file) => `${file.file}: ${file.source} ${file.author} ${file.license}`),\n })\n }\n return { id: options.id, samples }\n}\n","/**\n * The kit's own recorded sounds: the files in `assets/sfx/` (CC0 / CC-BY, credited in `assets/sfx/CREDITOS.md`) and the numbers each one plays\n * with. `kitSampleBank` turns the converter's manifest into a bank; `KIT_SAMPLE_ALIASES` lets a recipe id the kit already plays (`shut_wood`,\n * `rattle`...) be answered by the takes of a file id (`door_wood_shut`), so a sound that was synthesized becomes the recording with no change where\n * it is played: the recipe of the same id plays while the files load, and wherever they cannot be fetched (`file://`). `loadKitSamples` fetches the\n * manifest from where the folder is served and registers the bank on an engine.\n */\nimport { sampleBankFromManifest } from './manifest'\nimport type { SampleDefaults, SfxManifest, SfxManifestSound } from './manifest'\nimport type { BusId, SoundBank } from './recipe'\n\n/** The bus of a manifest category (the folder the file sits in). */\nexport const KIT_CATEGORY_BUS: Readonly<Record<string, BusId>> = {\n armas: 'weapons',\n impactos: 'world',\n 'explosoes-fogo': 'world',\n personagem: 'world',\n veiculos: 'world',\n 'itens-portas': 'world',\n ambiente: 'ambience',\n}\n\n/** Voice sounds go on the voices bus whatever folder they are in. */\nconst VOICE_IDS = /^(player_(hurt|death|effort|breath)|scream|zombie_)/\n\n/**\n * Recipe id → file id. The recipe keeps playing until the takes of the file id load. Hand-worked leaves (doors, drawers) of `game-destruction-three`\n * and the hand sounds of items.\n */\nexport const KIT_SAMPLE_ALIASES: Readonly<Record<string, string>> = {\n // a house door: the latch giving, the hinge, the leaf meeting the frame, a locked one shaken; a drawer on its runners\n latch: 'metal_latch',\n creak_wood: 'door_creak',\n shut_wood: 'door_wood_shut',\n rattle: 'door_locked_rattle',\n // a drawer stays on its recipe: the recorded runs (1 to 1.9 s) outlast a drawer's pull\n // an item taken in the hand (game-destruction-three grab_* keys)\n grab_hard: 'item_pickup_generic',\n grab_soft: 'item_pickup_leather',\n}\n\n/** The numbers of a sound the files do not say: the bus of its folder, a little pitch spread. */\nexport function kitSampleDefaults(sound: SfxManifestSound): SampleDefaults | undefined {\n const bus = VOICE_IDS.test(sound.id) ? 'voices' : KIT_CATEGORY_BUS[sound.category]\n if (!bus) return undefined\n return { bus, pitch: sound.category === 'ambiente' || /_loop/.test(sound.id) ? 0 : 0.04 }\n}\n\nexport type KitSampleBankOptions = {\n /** Where the `assets/sfx/` folder is served (`./sfx/`). */\n readonly baseUrl?: string\n /** Recipe id → file id (default `KIT_SAMPLE_ALIASES`); `{}` adds none. */\n readonly aliases?: Readonly<Record<string, string>>\n readonly id?: string\n}\n\n/** The manifest with each alias as one more sound, holding the takes of its file id. */\nexport function withAliases(manifest: SfxManifest, aliases: Readonly<Record<string, string>>): SfxManifest {\n const byId = new Map(manifest.sounds.map((sound) => [sound.id, sound]))\n const extra: SfxManifestSound[] = []\n for (const [alias, target] of Object.entries(aliases)) {\n const sound = byId.get(target)\n if (sound && !byId.has(alias)) extra.push({ ...sound, id: alias })\n }\n return { ...manifest, sounds: [...manifest.sounds, ...extra] }\n}\n\n/** The bank of the kit's files. */\nexport function kitSampleBank(manifest: SfxManifest, options: KitSampleBankOptions = {}): SoundBank {\n return sampleBankFromManifest(withAliases(manifest, options.aliases ?? KIT_SAMPLE_ALIASES), {\n id: options.id ?? 'kit_samples',\n baseUrl: options.baseUrl ?? '',\n defaults: kitSampleDefaults,\n })\n}\n\nexport type LoadKitSamplesOptions = KitSampleBankOptions & {\n /** Gets the manifest (default `fetch` of `<baseUrl>manifest.json`). */\n readonly fetchManifest?: (url: string) => Promise<SfxManifest>\n /** Ids to load right away instead of on their first play (the sounds of the first seconds, so they are not the recipe). */\n readonly preload?: readonly string[]\n}\n\ntype Registers = { register(bank: SoundBank): void; loadSamples?(ids?: readonly string[]): Promise<unknown> }\n\n/** True when `url`, read against the page, is a `file://` one: `fetch` refuses that scheme and the browser logs an error for each try. */\nconst isFileUrl = (url: string): boolean => {\n const page = (globalThis as { location?: { href?: string } }).location?.href\n try {\n return new URL(url, page).protocol === 'file:'\n } catch {\n return false\n }\n}\n\nconst fetchJson = async (url: string): Promise<SfxManifest> => {\n // a page opened from disk keeps the recipes without asking: the refused fetch would only put an error in the console\n if (isFileUrl(url)) throw new Error(`${url}: file:// is not fetched`)\n const response = await fetch(url)\n if (!response.ok) throw new Error(`${url}: ${response.status}`)\n return (await response.json()) as SfxManifest\n}\n\n/**\n * Fetches the manifest and registers the bank on the engine. Resolves to the bank, or `null` when the manifest cannot be had (a page opened from\n * `file://`, the folder not served): the recipes keep playing, so that is not an error.\n */\nexport async function loadKitSamples(engine: Registers, options: LoadKitSamplesOptions = {}): Promise<SoundBank | null> {\n const baseUrl = options.baseUrl ?? ''\n let manifest: SfxManifest\n try {\n manifest = await (options.fetchManifest ?? fetchJson)(`${baseUrl}manifest.json`)\n } catch {\n return null\n }\n const bank = kitSampleBank(manifest, options)\n engine.register(bank)\n if (options.preload && engine.loadSamples) void engine.loadSamples(options.preload).catch(() => undefined)\n return bank\n}\n","// The art scene's synthesis primitives (cena L1322-L1328) as recipe layers, shared by every subject bank that ports a `sfx.*` of the page: the\n// materials in `destruction-three`, the voices and wounds in `characters-three`. Each helper writes the layer the team routine builds, with its\n// fields in the order the routine draws its random numbers, so a recipe built from them leaves the trace of the team code.\nimport { DISTANCE_CURVES } from '../falloff'\nimport type { BusId, Envelope, Layer, ParamRef, Range, SoundRecipe } from '../recipe'\n\nconst ATTACK = 0.002\n\n/** A length in seconds: a number, a `[min, max]` range or a named number. */\nexport type SceneLength = number | readonly [number, number] | ParamRef\n\n// the layer is `attack + d` long (the art scene's gain rises over `a` and falls over `d`)\nfunction lengthOf(d: SceneLength, attack: number): Range {\n if (typeof d === 'number') return attack + d\n if (Array.isArray(d)) return [attack + d[0], attack + d[1]] as const\n return { ...(d as ParamRef), add: ((d as ParamRef).add ?? 0) + attack }\n}\n\nconst envelope = (attack: number): Envelope => [\n [0, 0.0001, 'set', 'fixed'],\n [attack, 1, 'exp'],\n ['1d', 0.0001, 'exp', 'fixed'],\n]\n\nexport type SceneNoise = {\n readonly type?: BiquadFilterType\n readonly f: Range\n /** The filter slides here over `d` (needs a fixed `d`). */\n readonly f2?: number\n readonly q?: number\n readonly a?: number\n readonly d: SceneLength\n readonly vol: Range\n readonly wet?: number\n readonly delay?: Range\n}\n\n/** `sNoise` (cena L1323): noise from a random point, a filter (default lowpass, Q 0.7), the gain rising over `a` and falling over `d`. */\nexport function sceneNoise(o: SceneNoise): Layer {\n const a = o.a ?? ATTACK\n const over = typeof o.d === 'number' ? o.d : 0\n return {\n filters: {\n mode: 'serial',\n bank: [{ type: o.type ?? 'lowpass', frequency: o.f2 === undefined ? { set: o.f } : { set: o.f, to: o.f2, curve: 'exp', over }, q: o.q ?? 0.7 }],\n },\n duration: lengthOf(o.d, a),\n gain: o.vol,\n ...(o.delay === undefined ? {} : { at: o.delay }),\n ...(o.wet === undefined ? {} : { wet: o.wet }),\n source: { kind: 'noise', offset: 'random', offsetScale: 1.5, loop: true },\n envelope: envelope(a),\n }\n}\n\nexport type SceneTone = { readonly wave?: OscillatorType; readonly f: Range; readonly f2?: number; readonly d: SceneLength; readonly vol: Range; readonly wet?: number; readonly delay?: Range }\n\n/** `sTone` (cena L1324): an oscillator sliding from `f` to `f2` (back to `f` when none), the gain rising over 0.002 s and falling over `d`. */\nexport function sceneTone(o: SceneTone): Layer {\n return {\n source: { kind: 'osc', wave: o.wave ?? 'sine', frequency: { from: o.f, to: o.f2 ?? 'from', curve: 'exp', over: 'duration' } },\n duration: lengthOf(o.d, ATTACK),\n gain: o.vol,\n ...(o.delay === undefined ? {} : { at: o.delay }),\n ...(o.wet === undefined ? {} : { wet: o.wet }),\n envelope: envelope(ATTACK),\n }\n}\n\nexport type SceneVoice = {\n /** Start and end frequency of the sweep (the page ramps linearly from `f0` to `f1` over the length). */\n readonly f0: Range\n readonly f1: Range\n /** Length in seconds. */\n readonly d: SceneLength\n /** Centre of the first and second formant (Q 4 and 6) and of the breath band (Q 1.1). */\n readonly fA: Range\n readonly fB: Range\n /** Level of the breath noise. */\n readonly noise: Range\n readonly vol: Range\n readonly wave?: OscillatorType\n}\n\n/**\n * `voice` (cena L1325): an oscillator swept from `f0` to `f1` with a vibrato of 4 to 10 Hz (depth 0.08 of `f0`) through two formant bands plus a band of\n * breath noise, summed and boosted 3.2 times, the gain rising over 0.1 s, held to 0.6 of the length and falling; reverb send 0.45. It is the one\n * layer the page builds with `voice(...)`, so a scream is two of them.\n */\nexport function sceneVoice(o: SceneVoice): Layer {\n return {\n duration: lengthOf(o.d, 0),\n gain: o.vol,\n wet: 0.45,\n source: {\n kind: 'formant_voice',\n wave: o.wave ?? 'sawtooth',\n frequency: { from: o.f0, to: o.f1, curve: 'linear', over: 'duration' },\n vibrato: { rate: [4, 10], depth: 0.08 },\n formants: [\n { frequency: o.fA, q: 4 },\n { frequency: o.fB, q: 6 },\n ],\n breath: { frequency: o.fB, q: 1.1, gain: o.noise },\n mix: 3.2,\n },\n envelope: [\n [0, 0.0001, 'set', 'fixed'],\n [0.1, 1, 'exp'],\n ['0.6d', 1, 'set'],\n ['1d', 0.0001, 'exp', 'fixed'],\n ],\n }\n}\n\n/** The layer played `count` times (a team `for` loop whose body draws numbers): each pass is its own layer, so the draws stay in the loop's order. */\nexport const sceneMany = (count: number, make: () => Layer): Layer[] => Array.from({ length: count }, make)\n\nexport type SceneRecipeOptions = {\n /** Named numbers drawn first (`PlayOptions.params` can override them). */\n readonly vars?: SoundRecipe['vars']\n readonly bus?: BusId\n readonly group?: string\n}\n\n/** A recipe in the art scene's mix: positional with the art scene's curve (`vol / (1 + d² × 0.008)`) and pan; `line` is the team code it comes from (`cena:<line>`). */\nexport function sceneRecipe(id: string, layers: readonly Layer[], line: string, options: SceneRecipeOptions = {}): SoundRecipe {\n return {\n id,\n bus: options.bus ?? 'world',\n pan: true,\n layers,\n falloff: DISTANCE_CURVES.art_scene,\n ...(options.vars ? { vars: options.vars } : {}),\n ...(options.group ? { group: options.group } : {}),\n sources: [`cena:${line}`],\n }\n}\n","// Foley of the hand: items picked up, opened, drunk and eaten, and the doors, lids and drawers of furniture and vehicles. Synthesised stand-ins under the\n// ids of the sound list (`sons/checklist-sons.md` 5.7 and the car doors of 5.6), so a recorded take filed under the same id replaces the recipe without\n// touching the code that plays it. Every recipe draws its numbers from ranges (pitch about ±4 %, timing and level), so no two plays are the same, and\n// falls off faster than a gunshot and loses its highs with distance: a latch is a near sound.\nimport type { Falloff, Layer, SoundBank, SoundRecipe } from '../recipe'\nimport { sceneMany as many, sceneNoise as noise, sceneTone as tone } from './scene'\n\n/** Small sounds of the hand: `vol / (1 + d² × 0.02)`, lowpass cutoffs `/ (1 + d × 0.04)`, silent below 0.004 (a latch at 10 m is a third, at 50 m gone). */\nexport const FOLEY_FALLOFF: Falloff = {\n model: 'inverse_square',\n k: 0.02,\n cutoffK: 0.04,\n minGain: 0.004,\n}\n\n/** ±`pct` around `f`: the range a recipe draws a frequency from. */\nconst vary = (f: number, pct = 0.04): readonly [number, number] => [Math.round(f * (1 - pct)), Math.round(f * (1 + pct))]\n\nconst foley = (id: string, layers: readonly Layer[], note: string): SoundRecipe => ({\n id,\n bus: 'world',\n pan: true,\n layers,\n falloff: FOLEY_FALLOFF,\n sources: [`sdk: ${note}`],\n})\n\n/** The click of a latch or a catch: a short band of noise and a tick of metal. */\nconst click = (f: number, vol: number, delay: readonly [number, number] | number = 0): Layer[] => [\n noise({\n type: 'bandpass',\n f: vary(f, 0.08),\n q: 4,\n d: [0.018, 0.03],\n vol: [vol * 0.85, vol * 1.1],\n delay,\n }),\n tone({\n f: vary(f * 0.8, 0.06),\n f2: Math.round(f * 0.62),\n d: [0.015, 0.025],\n vol: vol * 0.18,\n delay,\n }),\n]\n\n/** A leaf meeting its frame: the body (a falling tone), the board or panel (a band of noise) and the latch catching right after. */\nconst meet = (body: number, panel: number, vol: number, latch: number): Layer[] => [\n tone({\n f: vary(body),\n f2: Math.round(body * 0.5),\n d: [0.12, 0.16],\n vol: [vol * 0.9, vol * 1.1],\n }),\n noise({\n type: 'bandpass',\n f: vary(panel, 0.06),\n q: 1.2,\n d: [0.08, 0.12],\n vol: [vol * 0.9, vol * 1.1],\n wet: 0.25,\n }),\n ...click(latch, vol * 0.45, [0.012, 0.028]),\n]\n\n/** The doors and lids of furniture and vehicles, by what the leaf is (the room picks one from the item, else from the leaf's material). */\nexport const LEAF_FOLEY: readonly SoundRecipe[] = [\n // a wooden house door or wardrobe door: latch, a short creak, the leaf on the frame (the hard slam adds the frame rattling)\n foley('door_wood_open', click(2400, 0.4), 'the latch of a wooden door giving'),\n foley(\n 'door_creak',\n [\n tone({ f: [190, 240], f2: 310, d: [0.4, 0.55], vol: [0.04, 0.06] }),\n noise({\n type: 'bandpass',\n f: [480, 560],\n q: 6,\n a: 0.05,\n d: [0.35, 0.5],\n vol: [0.1, 0.14],\n }),\n ],\n 'a wooden hinge turning',\n ),\n foley('door_wood_close', meet(120, 600, 0.55, 2400), 'a wooden leaf meeting its frame'),\n foley(\n 'door_wood_slam',\n [\n ...meet(110, 560, 0.85, 2300),\n ...many(3, () =>\n noise({\n type: 'bandpass',\n f: [900, 1500],\n q: 3,\n d: 0.03,\n vol: [0.08, 0.14],\n delay: [0.05, 0.18],\n }),\n ),\n ],\n 'a wooden door slammed: the frame and the hinges rattle after the hit',\n ),\n // a metal door or gate: a hollow panel that rings a little\n foley('door_metal_open_close', [...meet(160, 900, 0.55, 2000), tone({ f: vary(1250), f2: 1150, d: [0.1, 0.16], vol: 0.06, wet: 0.3 })], 'a metal door or gate meeting its frame'),\n foley(\n 'door_creak_metal',\n [\n tone({ f: [620, 760], f2: 900, d: [0.3, 0.4], vol: [0.03, 0.045] }),\n noise({\n type: 'bandpass',\n f: [1300, 1500],\n q: 8,\n a: 0.04,\n d: [0.25, 0.35],\n vol: [0.07, 0.09],\n }),\n ],\n 'a metal hinge turning',\n ),\n // the handle of a locked door shaken: the latch knocking in its keeper, five to seven times in half a second\n foley(\n 'door_locked_rattle',\n [\n ...many(6, () =>\n noise({\n type: 'bandpass',\n f: [1600, 2600],\n q: 3,\n d: [0.025, 0.04],\n vol: [0.22, 0.34],\n delay: [0, 0.45],\n }),\n ),\n ...many(3, () =>\n tone({\n f: [170, 210],\n f2: 120,\n d: 0.05,\n vol: [0.12, 0.18],\n delay: [0.02, 0.4],\n }),\n ),\n ],\n 'the handle of a locked door shaken: latch knocks and the leaf thumping in its frame',\n ),\n // a fridge: the rubber seal lets go with a soft suck, the bottles in the door clink; closing it is a soft thump and the seal pulling shut\n foley(\n 'fridge_open',\n [\n noise({\n type: 'lowpass',\n f: [320, 380],\n a: 0.03,\n d: [0.14, 0.2],\n vol: [0.32, 0.4],\n }),\n noise({\n type: 'bandpass',\n f: [800, 950],\n f2: 1400,\n q: 2,\n a: 0.02,\n d: 0.12,\n vol: [0.08, 0.12],\n }),\n ...many(2, () =>\n tone({\n f: [2600, 4200],\n d: [0.04, 0.08],\n vol: [0.015, 0.03],\n delay: [0.08, 0.22],\n }),\n ),\n ],\n 'a fridge door: the seal letting go and the bottles in the door',\n ),\n foley(\n 'fridge_close',\n [\n tone({ f: vary(85), f2: 45, d: [0.1, 0.13], vol: [0.38, 0.46] }),\n noise({\n type: 'lowpass',\n f: [520, 620],\n d: [0.08, 0.12],\n vol: [0.3, 0.36],\n }),\n noise({\n type: 'lowpass',\n f: [260, 320],\n a: 0.02,\n d: 0.1,\n vol: [0.12, 0.18],\n delay: [0.04, 0.07],\n }),\n ...many(2, () =>\n tone({\n f: [2600, 4200],\n d: [0.04, 0.07],\n vol: [0.015, 0.03],\n delay: [0.03, 0.12],\n }),\n ),\n ],\n 'a fridge door shut: a soft thump, the seal pulling and the bottles',\n ),\n // a microwave: the spring catch of the door, plastic on plastic\n foley(\n 'microwave_open',\n [\n ...click(1800, 0.34),\n tone({\n f: [3000, 3400],\n f2: 2800,\n d: 0.05,\n vol: 0.025,\n delay: [0.01, 0.02],\n }),\n ],\n 'the spring catch of a microwave door',\n ),\n foley(\n 'microwave_close',\n [\n noise({\n type: 'bandpass',\n f: [1200, 1450],\n q: 2.5,\n d: [0.04, 0.06],\n vol: [0.36, 0.44],\n }),\n tone({ f: vary(180), f2: 120, d: 0.05, vol: [0.16, 0.22] }),\n ...click(2200, 0.2, [0.008, 0.016]),\n ],\n 'a microwave door clacking shut',\n ),\n foley('microwave_beep', [tone({ wave: 'square', f: 2000, d: 0.12, vol: 0.05 })], 'the beep of a microwave'),\n // drawers and sliding leaves: the runners, then the stop\n foley(\n 'drawer_open',\n [\n noise({\n type: 'bandpass',\n f: [620, 780],\n q: 2,\n a: 0.04,\n d: [0.28, 0.36],\n vol: [0.18, 0.24],\n }),\n noise({ type: 'bandpass', f: [2000, 2400], q: 5, d: 0.28, vol: 0.04 }),\n ...click(1400, 0.16, [0.28, 0.34]),\n ],\n 'a drawer on its runners and its stop',\n ),\n foley(\n 'drawer_close',\n [\n noise({\n type: 'bandpass',\n f: [620, 780],\n q: 2,\n a: 0.03,\n d: [0.2, 0.28],\n vol: [0.18, 0.24],\n }),\n tone({\n f: vary(150),\n f2: 90,\n d: 0.08,\n vol: [0.22, 0.3],\n delay: [0.2, 0.26],\n }),\n ],\n 'a drawer pushed home',\n ),\n]\n\n/** What the hand does with an item: picking it up by what it is made of, throwing it, and the uses (opening, drinking, eating, first aid). */\nexport const ITEM_FOLEY: readonly SoundRecipe[] = [\n // the hand closing on the item: a touch of skin and cloth, then the item's own small sound\n foley(\n 'item_pickup_metal',\n [\n noise({ type: 'bandpass', f: [700, 900], q: 1, d: 0.06, vol: 0.18 }),\n tone({\n f: [1900, 2600],\n f2: 1700,\n d: [0.05, 0.08],\n vol: [0.06, 0.09],\n delay: [0.01, 0.03],\n }),\n ],\n 'picking up a metal thing',\n ),\n foley(\n 'item_pickup_plastic',\n [\n noise({ type: 'bandpass', f: [700, 900], q: 1, d: 0.06, vol: 0.18 }),\n noise({\n type: 'bandpass',\n f: [1500, 1900],\n q: 3,\n d: 0.03,\n vol: [0.15, 0.21],\n delay: [0.01, 0.03],\n }),\n ],\n 'picking up a plastic thing',\n ),\n foley(\n 'item_pickup_glass',\n [\n noise({ type: 'bandpass', f: [700, 900], q: 1, d: 0.06, vol: 0.15 }),\n tone({\n f: [3200, 4200],\n d: [0.05, 0.08],\n vol: [0.038, 0.06],\n delay: [0.01, 0.03],\n }),\n ],\n 'picking up a glass thing',\n ),\n foley(\n 'item_pickup_paper',\n [\n noise({\n type: 'highpass',\n f: [2600, 3400],\n a: 0.01,\n d: [0.1, 0.14],\n vol: [0.12, 0.18],\n }),\n noise({ type: 'bandpass', f: [800, 1000], q: 1, d: 0.05, vol: 0.12 }),\n ],\n 'picking up paper or cardboard',\n ),\n foley(\n 'item_pickup_fabric',\n [\n noise({\n type: 'bandpass',\n f: [900, 1300],\n q: 0.8,\n a: 0.02,\n d: [0.12, 0.16],\n vol: [0.15, 0.21],\n }),\n ],\n 'picking up cloth',\n ),\n foley(\n 'item_pickup_wood',\n [\n noise({ type: 'bandpass', f: [700, 900], q: 1, d: 0.06, vol: 0.18 }),\n noise({\n type: 'bandpass',\n f: [900, 1100],\n q: 2,\n d: 0.04,\n vol: [0.15, 0.21],\n delay: [0.01, 0.03],\n }),\n ],\n 'picking up a wooden thing',\n ),\n foley(\n 'item_pickup_food',\n [\n noise({ type: 'bandpass', f: [700, 900], q: 1, d: 0.06, vol: 0.18 }),\n noise({\n type: 'lowpass',\n f: [500, 650],\n d: 0.05,\n vol: 0.12,\n delay: [0.01, 0.03],\n }),\n ],\n 'picking up food',\n ),\n foley(\n 'item_throw',\n [\n noise({\n type: 'bandpass',\n f: [450, 650],\n f2: 1400,\n q: 1.5,\n a: 0.05,\n d: 0.18,\n vol: [0.1, 0.14],\n }),\n ],\n 'the arm throwing something: air',\n ),\n // a can: the tab cracks, the gas hisses out; drinking is gulps; crushing it crumples the sheet\n foley(\n 'can_open',\n [\n ...click(3200, 0.36),\n noise({\n type: 'highpass',\n f: [3600, 4400],\n a: 0.005,\n d: [0.22, 0.3],\n vol: [0.16, 0.22],\n delay: [0.012, 0.02],\n }),\n ...many(5, () => tone({ f: [4000, 7000], d: 0.02, vol: 0.01, delay: [0.05, 0.4] })),\n ],\n 'the tab of a can and the gas hissing out',\n ),\n foley(\n 'can_crush',\n [\n noise({\n type: 'bandpass',\n f: [1600, 2200],\n q: 1.2,\n d: [0.18, 0.25],\n vol: [0.3, 0.38],\n }),\n ...many(6, () =>\n tone({\n f: [1200, 3400],\n f2: 900,\n d: 0.03,\n vol: [0.03, 0.06],\n delay: [0, 0.2],\n }),\n ),\n ],\n 'a can crushed in the hand',\n ),\n foley(\n 'can_puncture_spray',\n [\n noise({\n type: 'highpass',\n f: [2400, 3000],\n a: 0.005,\n d: [0.5, 0.7],\n vol: [0.2, 0.26],\n }),\n noise({ type: 'bandpass', f: [600, 800], q: 2, d: 0.4, vol: 0.06 }),\n ],\n 'a can holed: the jet hissing out',\n ),\n foley(\n 'bottle_open',\n [\n ...many(5, () =>\n noise({\n type: 'bandpass',\n f: [2200, 3000],\n q: 4,\n d: 0.008,\n vol: [0.1, 0.16],\n delay: [0, 0.18],\n }),\n ),\n noise({\n type: 'highpass',\n f: 3800,\n a: 0.005,\n d: 0.12,\n vol: [0.06, 0.1],\n delay: [0.18, 0.22],\n }),\n ],\n 'the screw cap of a bottle turning off',\n ),\n foley(\n 'drink_gulp',\n [\n noise({\n type: 'lowpass',\n f: [280, 360],\n a: 0.02,\n d: [0.1, 0.14],\n vol: [0.24, 0.3],\n }),\n tone({ f: [140, 170], f2: 85, d: [0.08, 0.11], vol: [0.12, 0.16] }),\n ],\n 'a gulp',\n ),\n foley(\n 'eat_crunch',\n [\n ...many(4, () =>\n noise({\n type: 'bandpass',\n f: [1800, 3200],\n q: 1.5,\n d: [0.02, 0.04],\n vol: [0.12, 0.2],\n delay: [0, 0.12],\n }),\n ),\n ],\n 'a bite of something crunchy',\n ),\n foley(\n 'eat_chew',\n [\n noise({\n type: 'lowpass',\n f: [450, 600],\n a: 0.03,\n d: [0.1, 0.14],\n vol: [0.12, 0.16],\n }),\n noise({\n type: 'bandpass',\n f: [1100, 1400],\n q: 2,\n d: 0.04,\n vol: 0.04,\n delay: [0.03, 0.06],\n }),\n ],\n 'chewing',\n ),\n foley(\n 'bandage_wrap',\n [\n noise({\n type: 'bandpass',\n f: [1400, 1800],\n f2: 2400,\n q: 1,\n a: 0.04,\n d: [0.3, 0.4],\n vol: [0.12, 0.16],\n }),\n ],\n 'a bandage unrolled round a limb',\n ),\n foley(\n 'pills_shake',\n [\n ...many(8, () =>\n tone({\n f: [2400, 3600],\n d: 0.015,\n vol: [0.03, 0.05],\n delay: [0, 0.25],\n }),\n ),\n noise({ type: 'bandpass', f: [1500, 1900], q: 2, d: 0.25, vol: 0.05 }),\n ],\n 'pills shaken in their bottle',\n ),\n foley(\n 'medkit_use',\n [\n ...click(1700, 0.24),\n noise({\n type: 'bandpass',\n f: [1000, 1400],\n q: 1,\n a: 0.03,\n d: [0.25, 0.32],\n vol: [0.08, 0.12],\n delay: [0.05, 0.08],\n }),\n ],\n 'a first aid kit opened and rummaged',\n ),\n foley('flashlight_click', click(2600, 0.3), 'the switch of a flashlight'),\n]\n\n/** The foley bank: register it in the engine (`engine.register(FOLEY_BANK)`). */\nexport const FOLEY_BANK: SoundBank = {\n id: 'foley',\n sounds: [...LEAF_FOLEY, ...ITEM_FOLEY],\n}\n\n/** What a picked-up item sounds like, by its heaviest material class. */\nexport const PICKUP_CLASSES = ['metal', 'plastic', 'glass', 'paper', 'fabric', 'wood', 'food'] as const\nexport type PickupClass = (typeof PICKUP_CLASSES)[number]\n\n/** The sound set of a leaf worked by hand: the ids it plays at the latch, while it turns, and as it meets the frame (gently, and slammed). */\nexport type LeafFoley = {\n readonly open: string\n readonly turn?: string\n readonly close: string\n readonly slam?: string\n readonly locked: string\n}\n\n/** The leaf sound sets the kit ships, by name; an item names one (`TestableItem.sounds.leaves`), else the room picks `wood_door` or `metal_door` by the leaf's material. */\nexport const LEAF_FOLEY_SETS: Readonly<Record<string, LeafFoley>> = {\n wood_door: {\n open: 'door_wood_open',\n turn: 'door_creak',\n close: 'door_wood_close',\n slam: 'door_wood_slam',\n locked: 'door_locked_rattle',\n },\n metal_door: {\n open: 'door_wood_open',\n turn: 'door_creak_metal',\n close: 'door_metal_open_close',\n locked: 'door_locked_rattle',\n },\n fridge: {\n open: 'fridge_open',\n close: 'fridge_close',\n locked: 'door_locked_rattle',\n },\n microwave: {\n open: 'microwave_open',\n close: 'microwave_close',\n locked: 'door_locked_rattle',\n },\n drawer: {\n open: 'drawer_open',\n close: 'drawer_close',\n locked: 'door_locked_rattle',\n },\n}\n","// The sounds of a body: footsteps by surface and gait, landings, the swing and the hit of each blow, hurt, bite and grab. Synthesised in the meantime\n// (the kit has no recorded take of these yet): each id is a set of four takes, with the numbers of each take apart, so the same action never\n// sounds twice the same. A recorded take replaces a variant by its id (`register` a bank with the same recipe id) without touching who plays it.\n//\n// How each one is built (what the ear picks first):\n// - a footstep is the heel (a low thump, how hollow the floor is) and then the roll to the toe (the surface's own texture: the grit of concrete,\n// the crunch of gravel, the ring of a metal sheet, the splash of water); a run hits harder, brighter and shorter, heel and toe almost together;\n// - a landing is the whole body's weight on the surface: a deeper thump, the surface texture longer, the clothes after;\n// - a swing is air: a band of noise sweeping up while the limb or the tool speeds up, loudest just before the contact, so it is started early\n// (`SWING_LEAD_SECONDS` of game-contracts) and the contact sound follows it;\n// - a hit on a body is skin (a slap), mass (a thud) and, for a blade, a wet tear; on an object it is only the striking part (knuckles, sole, steel)\n// and the material's own impact plays beside it.\nimport {\n BODY_SOUND_IDS,\n FOOTSTEP_SOUND_IDS,\n GAIT_IDS,\n LAND_SOUND_IDS,\n LANDING_IDS,\n SURFACE_IDS,\n SWING_LEAD_SECONDS,\n footstepSoundId,\n} from '@adatechnology/game-contracts'\nimport type { BlowId, BodySoundId, GaitId, LandingId, SurfaceId } from '@adatechnology/game-contracts'\n\nimport type { Falloff, Layer, SoundBank, SoundRecipe, SoundSet } from '../recipe'\nimport { sceneNoise, sceneTone, sceneVoice } from './scene'\n\n/** Takes per id. */\nexport const BODY_FOLEY_TAKES = 4\n\n/** How each take differs from the first: frequency factor, timing factor and level factor (fixed numbers, so takes are really apart). */\nconst TAKES: readonly { readonly f: number; readonly t: number; readonly v: number }[] = [\n { f: 1, t: 1, v: 1 },\n { f: 1.08, t: 0.9, v: 0.92 },\n { f: 0.93, t: 1.12, v: 1.05 },\n { f: 1.03, t: 1.05, v: 0.88 },\n]\n\n\n/** Footsteps fade fast with distance and lose their top: a step 10 m away is a quarter of the level, 30 m a few percent, and duller. */\nexport const FOOTSTEP_FALLOFF: Falloff = { model: 'inverse_square', k: 0.03, cutoffK: 0.03, minGain: 0.004 }\n/** Blows and body sounds carry further than a step. */\nexport const BODY_FALLOFF: Falloff = { model: 'inverse_square', k: 0.012, cutoffK: 0.015, minGain: 0.004 }\n\ntype Take = (typeof TAKES)[number]\n\n/** What a surface sounds like under a foot: the heel's thump and the texture of the roll. */\ntype SurfaceVoice = {\n /** The heel: thump pitch (Hz), its length (s) and level. */\n readonly heel: { readonly f: number; readonly d: number; readonly vol: number }\n /** The roll to the toe and the scuff: a filtered noise. */\n readonly roll: { readonly type: BiquadFilterType; readonly f: number; readonly q: number; readonly d: number; readonly vol: number }\n /** Grit, crunch or drips: short bursts spread after the heel. */\n readonly grains?: { readonly count: number; readonly f: number; readonly q: number; readonly spread: number; readonly vol: number }\n /** A ring of the floor itself (a hollow board, a metal sheet). */\n readonly ring?: readonly { readonly f: number; readonly d: number; readonly vol: number }[]\n /** Water: a sweep of the splash band. */\n readonly splash?: { readonly f: number; readonly f2: number; readonly d: number; readonly vol: number }\n}\n\nconst SURFACES: Readonly<Record<SurfaceId, SurfaceVoice>> = {\n concrete: { heel: { f: 95, d: 0.05, vol: 0.55 }, roll: { type: 'bandpass', f: 2300, q: 1.1, d: 0.05, vol: 0.38 }, grains: { count: 2, f: 4200, q: 2, spread: 0.04, vol: 0.08 } },\n asphalt: { heel: { f: 90, d: 0.05, vol: 0.5 }, roll: { type: 'bandpass', f: 1700, q: 0.9, d: 0.06, vol: 0.36 }, grains: { count: 4, f: 3600, q: 2.5, spread: 0.06, vol: 0.1 } },\n wood: { heel: { f: 130, d: 0.08, vol: 0.55 }, roll: { type: 'lowpass', f: 1300, q: 0.8, d: 0.06, vol: 0.3 }, ring: [{ f: 210, d: 0.1, vol: 0.18 }, { f: 470, d: 0.06, vol: 0.06 }] },\n tile: { heel: { f: 110, d: 0.04, vol: 0.45 }, roll: { type: 'bandpass', f: 3600, q: 2, d: 0.03, vol: 0.42 }, ring: [{ f: 1900, d: 0.03, vol: 0.05 }] },\n dirt: { heel: { f: 75, d: 0.06, vol: 0.45 }, roll: { type: 'lowpass', f: 750, q: 0.7, d: 0.09, vol: 0.4 }, grains: { count: 3, f: 1800, q: 1.5, spread: 0.07, vol: 0.06 } },\n grass: { heel: { f: 70, d: 0.05, vol: 0.32 }, roll: { type: 'highpass', f: 2600, q: 0.7, d: 0.13, vol: 0.28 }, grains: { count: 4, f: 5200, q: 1.2, spread: 0.1, vol: 0.07 } },\n gravel: { heel: { f: 80, d: 0.05, vol: 0.4 }, roll: { type: 'lowpass', f: 950, q: 0.8, d: 0.1, vol: 0.3 }, grains: { count: 8, f: 2600, q: 3, spread: 0.13, vol: 0.2 } },\n sand: { heel: { f: 65, d: 0.06, vol: 0.3 }, roll: { type: 'bandpass', f: 1500, q: 0.6, d: 0.16, vol: 0.3 }, grains: { count: 5, f: 4800, q: 1.5, spread: 0.12, vol: 0.05 } },\n metal: { heel: { f: 120, d: 0.05, vol: 0.5 }, roll: { type: 'bandpass', f: 2800, q: 1.5, d: 0.03, vol: 0.25 }, ring: [{ f: 420, d: 0.25, vol: 0.16 }, { f: 1130, d: 0.18, vol: 0.09 }, { f: 2470, d: 0.1, vol: 0.04 }] },\n water: { heel: { f: 70, d: 0.04, vol: 0.25 }, roll: { type: 'bandpass', f: 3000, q: 1.2, d: 0.1, vol: 0.18 }, splash: { f: 1100, f2: 380, d: 0.22, vol: 0.55 }, grains: { count: 4, f: 2400, q: 6, spread: 0.18, vol: 0.12 } },\n glass: { heel: { f: 90, d: 0.04, vol: 0.4 }, roll: { type: 'highpass', f: 4500, q: 0.8, d: 0.06, vol: 0.2 }, grains: { count: 9, f: 6200, q: 5, spread: 0.12, vol: 0.22 }, ring: [{ f: 3900, d: 0.04, vol: 0.04 }, { f: 5300, d: 0.03, vol: 0.03 }] },\n carpet: { heel: { f: 70, d: 0.05, vol: 0.32 }, roll: { type: 'lowpass', f: 420, q: 0.7, d: 0.07, vol: 0.3 } },\n}\n\nconst GAITS: Readonly<Record<GaitId, { readonly vol: number; readonly bright: number; readonly toe: number; readonly short: number }>> = {\n walk: { vol: 1, bright: 1, toe: 0.075, short: 1 },\n run: { vol: 1.35, bright: 1.25, toe: 0.035, short: 0.75 },\n}\n\nconst grainLayers = (g: NonNullable<SurfaceVoice['grains']>, take: Take, start: number, scale: number): Layer[] =>\n Array.from({ length: g.count }, (_, i) =>\n sceneNoise({ type: 'bandpass', f: [g.f * take.f * 0.85, g.f * take.f * 1.15], q: g.q, d: [0.008, 0.02], vol: g.vol * scale * take.v, delay: start + ((i + 0.5) / g.count) * g.spread * take.t }),\n )\n\nconst ringLayers = (rings: NonNullable<SurfaceVoice['ring']>, take: Take, scale: number, at = 0): Layer[] =>\n rings.map((r) => sceneTone({ f: r.f * take.f, d: r.d * take.t, vol: r.vol * scale * take.v, delay: at }))\n\n/** The layers of one foot on a surface: heel, roll to the toe, then grains, ring or splash. `weight` scales the heel (a landing is the whole body). */\nfunction footLayers(surface: SurfaceId, take: Take, gait: (typeof GAITS)[GaitId], weight = 1): Layer[] {\n const s = SURFACES[surface]\n const toe = gait.toe * take.t\n return [\n sceneTone({ f: s.heel.f * take.f * weight ** -0.3, f2: s.heel.f * take.f * 0.6, d: s.heel.d * gait.short * take.t * weight ** 0.5, vol: s.heel.vol * gait.vol * take.v * weight ** 0.5 }),\n sceneNoise({ type: 'lowpass', f: 500 * gait.bright, d: 0.02 * take.t, vol: 0.18 * gait.vol * take.v * weight ** 0.5 }),\n sceneNoise({ type: s.roll.type, f: [s.roll.f * take.f * gait.bright * 0.92, s.roll.f * take.f * gait.bright * 1.08], q: s.roll.q, d: s.roll.d * gait.short * take.t * weight ** 0.3, vol: s.roll.vol * gait.vol * take.v, delay: toe }),\n ...(s.grains ? grainLayers(s.grains, take, toe * 0.5, gait.vol * weight ** 0.5) : []),\n ...(s.ring ? ringLayers(s.ring, take, gait.vol * weight ** 0.5) : []),\n ...(s.splash ? [sceneNoise({ type: 'bandpass', f: s.splash.f * take.f, f2: s.splash.f2 * take.f, q: 1.4, d: s.splash.d * take.t * weight ** 0.4, vol: s.splash.vol * gait.vol * take.v * weight ** 0.5 })] : []),\n ]\n}\n\n/** The layers at `level` of their gain (a scene layer's gain is a number or a range). */\nconst quieter = (level: number, layers: readonly Layer[]): Layer[] =>\n layers.map((layer) => {\n const gain = layer.gain ?? 1\n if (typeof gain === 'number') return { ...layer, gain: gain * level }\n if (Array.isArray(gain)) return { ...layer, gain: [gain[0] * level, gain[1] * level] as const }\n return layer\n })\n\nconst recipe = (id: string, layers: readonly Layer[], falloff: Falloff, bus: SoundRecipe['bus'] = 'world'): SoundRecipe => ({ id, bus, pan: true, layers, falloff, sources: ['sdk'] })\n\nconst takeId = (id: string, index: number): string => `${id}_${String(index + 1).padStart(2, '0')}`\n\n/** The four takes of an id and the set that names them. */\nfunction takes(id: string, layersOf: (take: Take, index: number) => Layer[], falloff: Falloff, rate: readonly [number, number] = [0.96, 1.04], bus?: SoundRecipe['bus']): { sounds: SoundRecipe[]; set: SoundSet } {\n const sounds = TAKES.map((take, index) => recipe(takeId(id, index), layersOf(take, index), falloff, bus))\n return { sounds, set: { id, variants: sounds.map((sound) => sound.id), rate } }\n}\n\nconst footstep = (surface: SurfaceId, gait: GaitId) => takes(footstepSoundId(surface, gait), (take) => footLayers(surface, take, GAITS[gait]), FOOTSTEP_FALLOFF)\n\nconst LANDINGS: Readonly<Record<LandingId, { readonly weight: number; readonly cloth: number }>> = { light: { weight: 1.4, cloth: 0.12 }, heavy: { weight: 2.4, cloth: 0.22 } }\n\n/** A landing's layers are the feet's at the body's weight; this brings the whole of it under a blow on a body (C6). */\nconst LANDING_LEVEL = 0.65\n\nconst landing = (surface: SurfaceId, landing: LandingId) =>\n takes(\n `land_${surface}_${landing}`,\n (take) => quieter(LANDING_LEVEL, [\n // both feet, a hair apart, then the knees taking the weight (a low body thump) and the clothes settling\n ...footLayers(surface, take, GAITS.run, LANDINGS[landing].weight),\n ...footLayers(surface, { ...take, v: take.v * 0.6 }, GAITS.run, LANDINGS[landing].weight * 0.6).map((layer) => ({ ...layer, at: (typeof layer.at === 'number' ? layer.at : 0) + 0.018 * take.t })),\n sceneTone({ f: 60 * take.f, f2: 38, d: 0.14 * LANDINGS[landing].weight * take.t, vol: 0.22 * LANDINGS[landing].weight ** 0.7 * take.v, delay: 0.02 }),\n sceneNoise({ type: 'bandpass', f: [1800, 2600], q: 0.8, d: 0.18, vol: LANDINGS[landing].cloth * take.v, delay: 0.04 }),\n ]),\n FOOTSTEP_FALLOFF,\n )\n\nconst whoosh = (blow: BlowId, from: number, to: number, d: number, vol: number, q = 1.6): ((take: Take) => Layer[]) => (take) => [\n // the air: a band sweeping up as the limb speeds up; it peaks `SWING_LEAD_SECONDS` in, the moment before the contact, then dies away\n sceneNoise({ type: 'bandpass', f: from * take.f, f2: to * take.f, q, a: SWING_LEAD_SECONDS[blow], d: d * take.t, vol: vol * take.v }),\n sceneNoise({ type: 'lowpass', f: 600 * take.f, a: SWING_LEAD_SECONDS[blow], d: d * take.t * 0.8, vol: vol * 0.35 * take.v }),\n]\n\n/** Skin, mass and (optionally) bone: what a blow on a body is made of. */\nconst fleshHit = (o: { slap: number; slapF: number; thud: number; thudF: number; thudD: number; crack?: number; wet?: number }): ((take: Take) => Layer[]) => (take) => [\n sceneNoise({ type: 'bandpass', f: o.slapF * take.f, q: 1.2, d: 0.03 * take.t, vol: o.slap * take.v }),\n sceneNoise({ type: 'highpass', f: 3500 * take.f, d: 0.012, vol: o.slap * 0.4 * take.v }),\n sceneTone({ f: o.thudF * take.f, f2: o.thudF * 0.5, d: o.thudD * take.t, vol: o.thud * take.v }),\n sceneNoise({ type: 'lowpass', f: 600 * take.f, d: o.thudD * 1.2 * take.t, vol: o.thud * 0.5 * take.v }),\n ...(o.crack ? [sceneNoise({ type: 'highpass', f: 2500, d: 0.015, vol: o.crack * take.v, delay: 0.006 }), sceneTone({ wave: 'square', f: 900 * take.f, f2: 300, d: 0.02, vol: o.crack * 0.15 * take.v, delay: 0.006 })] : []),\n ...(o.wet ? [sceneNoise({ type: 'bandpass', f: 1400 * take.f, f2: 500, q: 2.5, d: 0.12 * take.t, vol: o.wet * take.v, delay: 0.01 })] : []),\n]\n\n/** What only the striking part sounds like on a hard thing (the material's impact plays beside it). */\nconst knock = (o: { clickF: number; click: number; tones: readonly (readonly [number, number, number])[] }): ((take: Take) => Layer[]) => (take) => [\n sceneNoise({ type: 'bandpass', f: o.clickF * take.f, q: 1.5, d: 0.018, vol: o.click * take.v }),\n ...o.tones.map(([f, d, vol]) => sceneTone({ f: f * take.f, d: d * take.t, vol: vol * take.v })),\n]\n\nconst voice = (o: { f0: number; f1: number; d: number; fA: number; fB: number; noise: number; vol: number }): ((take: Take) => Layer[]) => (take) => [\n sceneVoice({ f0: o.f0 * take.f, f1: o.f1 * take.f, d: o.d * take.t, fA: o.fA * take.f, fB: o.fB * take.f, noise: o.noise, vol: o.vol * take.v }),\n]\n\nconst BODY: Readonly<Record<BodySoundId, (take: Take) => Layer[]>> = {\n jump: (take) => [\n sceneNoise({ type: 'bandpass', f: 900 * take.f, f2: 1700 * take.f, q: 1, a: 0.03, d: 0.12, vol: 0.18 * take.v }),\n sceneNoise({ type: 'bandpass', f: 2200 * take.f, q: 0.8, d: 0.04, vol: 0.12 * take.v }),\n sceneTone({ f: 85 * take.f, f2: 60, d: 0.04, vol: 0.2 * take.v }),\n ],\n cloth_rustle: (take) => [\n { ...sceneNoise({ type: 'bandpass', f: 2400 * take.f, q: 0.8, a: 0.06, d: 0.22 * take.t, vol: 0.12 * take.v }), modulators: [{ target: 'gain', wave: 'triangle', rate: [9, 14], depth: 0.5 }] },\n ],\n body_fall: (take) => [\n sceneTone({ f: 58 * take.f, f2: 35, d: 0.28 * take.t, vol: 0.9 * take.v }),\n sceneNoise({ type: 'lowpass', f: 480 * take.f, d: 0.3 * take.t, vol: 0.55 * take.v }),\n sceneNoise({ type: 'bandpass', f: 1300 * take.f, q: 1, d: 0.05, vol: 0.3 * take.v }),\n // the limbs a moment later, and the clothes\n sceneTone({ f: 75 * take.f, f2: 45, d: 0.12, vol: 0.4 * take.v, delay: 0.13 * take.t }),\n sceneNoise({ type: 'bandpass', f: 2200, q: 0.8, d: 0.2, vol: 0.12 * take.v, delay: 0.05 }),\n ],\n fall_damage: (take) => [\n ...BODY.body_fall(take),\n sceneNoise({ type: 'highpass', f: 2800, d: 0.02, vol: 0.35 * take.v, delay: 0.01 }),\n ...voice({ f0: 170, f1: 110, d: 0.32, fA: 650, fB: 1150, noise: 0.25, vol: 0.32 })(take).map((layer) => ({ ...layer, at: 0.08 })),\n ],\n player_hurt: voice({ f0: 150, f1: 105, d: 0.26, fA: 620, fB: 1100, noise: 0.3, vol: 0.35 }),\n player_breath_run: (take) => [\n sceneNoise({ type: 'bandpass', f: 1200 * take.f, q: 1, a: 0.08, d: 0.22 * take.t, vol: 0.25 * take.v }),\n sceneNoise({ type: 'bandpass', f: 900 * take.f, q: 1, a: 0.06, d: 0.26 * take.t, vol: 0.3 * take.v, delay: 0.3 * take.t }),\n ],\n player_death: voice({ f0: 135, f1: 68, d: 1, fA: 560, fB: 980, noise: 0.35, vol: 0.4 }),\n bite: (take) => [\n sceneNoise({ type: 'highpass', f: 2600, d: 0.015, vol: 0.4 * take.v }),\n sceneNoise({ type: 'bandpass', f: 1600 * take.f, f2: 450, q: 2.2, d: 0.16 * take.t, vol: 0.55 * take.v, delay: 0.01 }),\n sceneNoise({ type: 'bandpass', f: 3200 * take.f, f2: 1800, q: 1.2, d: 0.22 * take.t, vol: 0.25 * take.v, delay: 0.09 }),\n sceneTone({ f: 120 * take.f, f2: 70, d: 0.06, vol: 0.3 * take.v }),\n ],\n grab: (take) => [\n sceneNoise({ type: 'bandpass', f: 1900 * take.f, q: 0.9, a: 0.004, d: 0.13 * take.t, vol: 0.32 * take.v }),\n sceneTone({ f: 95 * take.f, f2: 60, d: 0.07, vol: 0.3 * take.v }),\n sceneNoise({ type: 'lowpass', f: 700, d: 0.08, vol: 0.2 * take.v }),\n ],\n punch_swing: whoosh('punch', 450, 1500, 0.16, 0.9),\n punch_hit_body: fleshHit({ slap: 0.55, slapF: 1500, thud: 0.6, thudF: 115, thudD: 0.09 }),\n punch_hit_face: fleshHit({ slap: 0.75, slapF: 2300, thud: 0.45, thudF: 150, thudD: 0.07, crack: 0.25 }),\n punch_hit_object: knock({ clickF: 1800, click: 0.3, tones: [[210, 0.04, 0.25]] }),\n kick_swing: whoosh('kick', 280, 950, 0.22, 1, 1.3),\n kick_hit: fleshHit({ slap: 0.45, slapF: 1100, thud: 0.85, thudF: 82, thudD: 0.14 }),\n kick_hit_object: knock({ clickF: 1000, click: 0.3, tones: [[92, 0.08, 0.5]] }),\n shove_two_hands: (take) => [\n sceneNoise({ type: 'bandpass', f: 950 * take.f, q: 0.9, a: 0.01, d: 0.13 * take.t, vol: 0.38 * take.v }),\n sceneTone({ f: 100 * take.f, f2: 60, d: 0.1 * take.t, vol: 0.45 * take.v }),\n sceneNoise({ type: 'bandpass', f: 2300, q: 0.8, d: 0.16, vol: 0.12 * take.v, delay: 0.03 }),\n ],\n knife_swing: whoosh('blade', 1900, 4600, 0.12, 1.1, 2.2),\n knife_stab: fleshHit({ slap: 0.2, slapF: 1200, thud: 0.4, thudF: 160, thudD: 0.06, wet: 0.5 }),\n knife_slash: (take) => [\n sceneNoise({ type: 'highpass', f: 3200 * take.f, f2: 1800, d: 0.13 * take.t, vol: 0.35 * take.v }),\n sceneNoise({ type: 'bandpass', f: 1300 * take.f, f2: 600, q: 2.5, d: 0.12 * take.t, vol: 0.35 * take.v, delay: 0.02 }),\n ],\n knife_hit_object: knock({ clickF: 3200, click: 0.25, tones: [[2800, 0.08, 0.07], [4150, 0.06, 0.05]] }),\n blunt_swing: whoosh('blunt', 220, 760, 0.28, 1.1, 1.5),\n blunt_hit: fleshHit({ slap: 0.35, slapF: 900, thud: 1, thudF: 70, thudD: 0.18, crack: 0.3 }),\n blunt_hit_object: knock({ clickF: 1400, click: 0.35, tones: [[300, 0.12, 0.25], [760, 0.08, 0.12]] }),\n axe_swing: whoosh('chop', 190, 620, 0.3, 1.2, 1.4),\n axe_chop: fleshHit({ slap: 0.3, slapF: 1400, thud: 0.75, thudF: 140, thudD: 0.1, crack: 0.35, wet: 0.45 }),\n axe_hit_object: knock({ clickF: 2200, click: 0.4, tones: [[1500, 0.15, 0.1], [2300, 0.1, 0.06], [140, 0.1, 0.35]] }),\n}\n\nconst BODY_BUS: Partial<Record<BodySoundId, SoundRecipe['bus']>> = { player_hurt: 'voices', player_death: 'voices', player_breath_run: 'voices', fall_damage: 'voices' }\n\nconst built = [\n ...SURFACE_IDS.flatMap((surface) => GAIT_IDS.map((gait) => footstep(surface, gait))),\n ...SURFACE_IDS.flatMap((surface) => LANDING_IDS.map((l) => landing(surface, l))),\n ...BODY_SOUND_IDS.map((id) => takes(id, BODY[id], BODY_FALLOFF, id.endsWith('_swing') ? [0.94, 1.06] : [0.96, 1.04], BODY_BUS[id])),\n]\n\n/**\n * Every body sound of `game-contracts`' `BODY_FOLEY_IDS` as a set of four takes (`footstep_concrete_walk` plays `footstep_concrete_walk_01`..`_04`,\n * never the same twice in a row, at ±4% of rate). Register it in the engine the body plays on.\n */\nexport const BODY_FOLEY_BANK: SoundBank = {\n id: 'body_foley',\n sounds: built.flatMap((entry) => entry.sounds),\n sets: built.map((entry) => entry.set),\n}\n\n/** The ids the bank answers (the sets), in the contract's order. */\nexport const BODY_FOLEY_SET_IDS: readonly string[] = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS]\n","import type { AudioListener, AudioSink, LoopHandle, SoundHandle, SoundRequest } from '@adatechnology/game-contracts'\n\nimport type { SoundBank } from '../recipe'\nimport type { AudioEngine, VoiceHandle } from './engine'\n\n/** An `AudioSink` (what a host lends its modules) that carries the engine behind it, so a module that wants more than `play` can use the engine. */\nexport type EngineSink = AudioSink & { readonly engine: AudioEngine }\n\n/** Lends an engine as the host's `audio`: sounds by key, loops, the listener, and banks registered into the same engine. */\nexport function createAudioSink(engine: AudioEngine): EngineSink {\n const playing = new Map<number, VoiceHandle>()\n return {\n engine,\n play(request: SoundRequest): SoundHandle | null {\n const handle = engine.play(request.sound, {\n ...(request.position ? { position: request.position } : {}),\n ...(request.volume === undefined ? {} : { volume: request.volume }),\n ...(request.params ? { params: request.params } : {}),\n ...(request.seed === undefined ? {} : { seed: request.seed }),\n ...(request.delay === undefined ? {} : { delay: request.delay }),\n ...(request.urgent ? { urgent: true } : {}),\n })\n if (!handle) return null\n playing.set(handle.id, handle)\n return { id: handle.id }\n },\n stop(handle) {\n playing.get(handle.id)?.stop()\n playing.delete(handle.id)\n },\n loop(sound, options): LoopHandle {\n const loop = engine.loop(sound)\n // the loop's place comes with the first set; `params.cutoff` drives the first filter\n let position = options?.position\n return {\n set: (state) => {\n position = state.position ?? position\n const cutoff = state.params?.['cutoff']\n loop.set({\n ...(state.volume === undefined ? {} : { volume: state.volume }),\n ...(state.frequency === undefined ? {} : { frequency: state.frequency }),\n ...(position ? { position } : {}),\n ...(cutoff === undefined ? {} : { cutoff }),\n })\n },\n stop: () => loop.stop(),\n }\n },\n setListener(listener: AudioListener) {\n engine.setListener(listener)\n },\n register(bank: unknown) {\n engine.register(bank as SoundBank)\n },\n }\n}\n","// Rain, wind and thunder as recipes of @adatechnology/game-audio. The loops are the teams' (the art scene's `initAudio`, cena L1314-L1315, and the game's\n// `chuva` loop, jogo L62116); the thunder is the game's `boom` (jogo L68278, a sine from 70 to 28 Hz) with a low rumble under it, the kit's choice.\nimport { sceneNoise, sceneTone } from '@adatechnology/game-audio'\nimport type { LoopRecipe, SoundBank, SoundRecipe } from '@adatechnology/game-audio'\n\n/** Rain of the art scene: noise through a highpass at 600 Hz and a lowpass at 6000 Hz (cena L1314), opened at its volume. */\nexport const RAIN_LOOP: LoopRecipe = {\n id: 'rain_loop',\n bus: 'weather',\n order: 'chain_loop',\n source: { kind: 'noise', loop: true },\n filters: {\n mode: 'serial',\n bank: [\n { type: 'highpass', frequency: { set: 600 } },\n { type: 'lowpass', frequency: { set: 6000 } },\n ],\n },\n sources: ['cena:L1314'],\n}\n\n/** Wind of the art scene: noise through a lowpass at 320 Hz whose gain swings 0.035 at 0.07 Hz (cena L1315). */\nexport const WIND_LOOP: LoopRecipe = {\n id: 'wind_loop',\n bus: 'weather',\n order: 'chain_loop',\n source: { kind: 'noise', loop: true },\n filters: { mode: 'serial', bank: [{ type: 'lowpass', frequency: { set: 320 } }] },\n modulators: [{ target: 'gain', rate: 0.07, depth: 0.035 }],\n sources: ['cena:L1315'],\n}\n\n/** Rain of the game: noise through a highpass at 2500 Hz, created when the volume first passes 0.002 (jogo L62116 `chuva`). */\nexport const RAIN_HISS_GAME: LoopRecipe = {\n id: 'rain_hiss_game',\n bus: 'weather',\n order: 'game_loop',\n source: { kind: 'noise', loop: true },\n filters: { mode: 'serial', bank: [{ type: 'highpass', frequency: { set: 2500 } }] },\n sources: ['jogo:L62116 chuva'],\n}\n\n/**\n * Thunder. The thump is the game's boom (a sine from 70 to 28 Hz over 1.2 s); the rumble is a lowpassed noise that slides from 260 to 50 Hz over 3.2 s\n * with a reverb send, the kit's choice (no team has a thunder). It carries much farther than a blast: `k` is 0.00001, so a strike 1 km away is still at\n * 0.13 and one 4 km away (the farthest the weather places) at 0.009.\n */\nexport const THUNDER: SoundRecipe = {\n id: 'thunder',\n bus: 'weather',\n pan: true,\n layers: [sceneTone({ f: 70, f2: 28, d: 1.2, vol: 1 }), sceneNoise({ f: 260, f2: 50, d: 3.2, vol: 0.9, wet: 0.8 })],\n falloff: { model: 'inverse_square', k: 0.00001, scale: 1.4 },\n sources: ['jogo:L68278-68285 boom', 'sdk: rumble and falloff'],\n}\n\nexport const WEATHER_BANK: SoundBank = { id: 'weather', sounds: [THUNDER], loops: [RAIN_LOOP, WIND_LOOP, RAIN_HISS_GAME] }\nexport const WEATHER_SOUNDS: readonly SoundRecipe[] = WEATHER_BANK.sounds ?? []\nexport const WEATHER_LOOPS: readonly LoopRecipe[] = WEATHER_BANK.loops ?? []\n","import type { AudioSink, LoopHandle, Vec3 } from '@adatechnology/game-contracts'\n\nimport type { Strike, WeatherSample } from '../types'\nimport { WEATHER_BANK } from './recipes'\n\nexport type WeatherSoundStyle = 'art_scene' | 'game'\n\nexport type WeatherSoundOptions = {\n /** Which team's rain: `art_scene` (default, the kit mix) or `game` (a highpass hiss). The wind and the thunder are the same. */\n readonly style?: WeatherSoundStyle\n /**\n * Level of the rain loop. It is `light` at `lightAt` mm/h (the game's rain, 0.035) and `heavy` from `heavyAt` mm/h (the art scene's, 0.06), a straight\n * line between, and 0 when it does not rain.\n */\n readonly rain?: { readonly light?: number; readonly heavy?: number; readonly lightAt?: number; readonly heavyAt?: number }\n /** Level of the wind loop: `base` (the art scene's 0.05) at `referenceSpeed` m/s, in proportion to the speed, at most `max`. */\n readonly wind?: { readonly base?: number; readonly referenceSpeed?: number; readonly max?: number }\n /** Where the listener is, to place the thunder around it (default the origin). */\n readonly listener?: () => Vec3\n /** Level of the thunder (default 1). */\n readonly thunderVolume?: number\n /** `false` keeps the sink's bank as it is (the host registered `WEATHER_BANK` itself). */\n readonly register?: boolean\n}\n\nexport const DEFAULT_RAIN_SOUND = { light: 0.035, heavy: 0.06, lightAt: 2.5, heavyAt: 15 } as const\n/** The art scene opens its wind at 0.05 with the breeze of its day (3.8 m/s, the `clear` state's wind): the level follows the speed from there. */\nexport const DEFAULT_WIND_SOUND = { base: 0.05, referenceSpeed: 3.8, max: 0.12 } as const\n\n/** The level of the rain loop for `rain` mm/h. */\nexport function rainVolume(rain: number, options: WeatherSoundOptions['rain'] = {}): number {\n if (!(rain > 0)) return 0\n const { light, heavy, lightAt, heavyAt } = { ...DEFAULT_RAIN_SOUND, ...options }\n if (heavyAt <= lightAt) return rain >= heavyAt ? heavy : light\n const t = Math.min(1, Math.max(0, (rain - lightAt) / (heavyAt - lightAt)))\n return light + (heavy - light) * t\n}\n\n/** The level of the wind loop for a wind of `speed` m/s. */\nexport function windVolume(speed: number, options: WeatherSoundOptions['wind'] = {}): number {\n const { base, referenceSpeed, max } = { ...DEFAULT_WIND_SOUND, ...options }\n return Math.min(max, (base * Math.max(0, speed)) / referenceSpeed)\n}\n\n/** Where a strike is, around `origin`: `bearing` is degrees clockwise from north, and north is -z. */\nexport function strikePosition(strike: Pick<Strike, 'distance' | 'bearing'>, origin: Vec3 = [0, 0, 0]): Vec3 {\n const angle = (strike.bearing * Math.PI) / 180\n return [origin[0] + Math.sin(angle) * strike.distance, origin[1], origin[2] - Math.cos(angle) * strike.distance]\n}\n\nexport type WeatherSound = {\n /** Sets the level of the loops from the weather of now (opens a loop the first time it is above 0). */\n update(sample: Pick<WeatherSample, 'rain' | 'windSpeed'>): void\n /** Plays a clap of thunder at the strike (call it when the thunder arrives: the weather delays it by `distance / 343`). */\n thunder(strike: Pick<Strike, 'distance' | 'bearing'>): void\n dispose(): void\n}\n\n/** The sound of the weather on the host's `audio`: the rain loop, the wind loop and the thunder. Without a sink (or one without loops) it does nothing. */\nexport function createWeatherSound(sink: AudioSink | undefined, options: WeatherSoundOptions = {}): WeatherSound {\n if (sink && options.register !== false) sink.register?.(WEATHER_BANK)\n const rainSound = options.style === 'game' ? 'rain_hiss_game' : 'rain_loop'\n const loops = new Map<string, LoopHandle>()\n const set = (id: string, volume: number): void => {\n let loop = loops.get(id)\n if (!loop) {\n if (!(volume > 0) || !sink?.loop) return\n loop = sink.loop(id)\n loops.set(id, loop)\n }\n loop.set({ volume })\n }\n return {\n update(sample) {\n set(rainSound, rainVolume(sample.rain, options.rain))\n set('wind_loop', windVolume(sample.windSpeed, options.wind))\n },\n thunder(strike) {\n sink?.play({ sound: 'thunder', position: strikePosition(strike, options.listener?.()), ...(options.thunderVolume === undefined ? {} : { volume: options.thunderVolume }) })\n },\n dispose() {\n for (const loop of loops.values()) loop.stop()\n loops.clear()\n },\n }\n}\n","import type { AudioSink, EnvironmentBus, EnvironmentState } from '@adatechnology/game-contracts'\n\nimport { createWeatherSound } from './audio/sound'\nimport type { WeatherSound, WeatherSoundOptions } from './audio/sound'\nimport type { Strike, WeatherSample } from './types'\nimport type { Weather } from './weather'\n\n/** The shape of the film of `game-fluids-three` (`FilmBridge`) this needs: no import, so the weather does not depend on it. */\nexport type FilmTarget = {\n setWeather(weather: { rain: number; drying?: number }): void\n wash?(maxDryness?: number): number\n}\n/** The interactables world (`game-interactables`): its water takes the rain, the soft volumes the wind. */\nexport type InteractablesTarget = { water: { setRain(mmPerHour: number): void }; setWind(x: number, z: number): void }\n/** The vegetation renderer (`game-vegetation`). `direction` is normalised by the renderer, `strength` and `gust` are 0..1. */\nexport type VegetationTarget = { setWind(wind: { direction?: [number, number]; strength?: number; gust?: number }): void }\n/** The effects world (`game-vfx`'s `VfxWorld`): smoke and dust drift with its wind. */\nexport type VfxTarget = { setWind(wind: readonly [number, number, number]): void }\n\nexport type WeatherTargets = {\n readonly film?: FilmTarget\n readonly interactables?: InteractablesTarget\n readonly vegetation?: VegetationTarget\n readonly vfx?: VfxTarget\n /** The host's environment bus (`ModuleHost.environment`). */\n readonly environment?: Pick<EnvironmentBus, 'set'>\n /** The host's sound (`ModuleHost.audio`): the rain and wind loops, and the thunder. */\n readonly audio?: AudioSink\n}\n\n/** The rules the bridges apply; every number can be replaced. */\nexport type WeatherBridgeRules = {\n /** Rain in mm/h at which the film counts as fully rained on (it stops drying, spreads 1.5 times faster). Default 2.5, the light rain. */\n readonly filmRainFullAt?: number\n /** Rain in mm/h from which the rain washes the fresh stain (Workshop and visor: heavy rain and stronger). Default 15; light rain does not wash. */\n readonly washFrom?: number\n /** Stains less dry than this are washed (visor L13494). Default 0.5. */\n readonly washBelow?: number\n /** Seconds between two washes while it pours. Default 1. */\n readonly washEvery?: number\n /** Wind speed in m/s that is 1 for the vegetation (`strength = min(1, speed / this)`). Default 20. */\n readonly windFull?: number\n}\n\nexport type WeatherBridgeOptions = WeatherBridgeRules & {\n readonly sound?: WeatherSoundOptions | false\n}\n\nexport const DEFAULT_BRIDGE_RULES = { filmRainFullAt: 2.5, washFrom: 15, washBelow: 0.5, washEvery: 1, windFull: 20 } as const\n\n/** The weather as the environment bus holds it. */\nexport function environmentOf(sample: WeatherSample): Partial<EnvironmentState> {\n return {\n rain: sample.rain,\n windX: sample.windX,\n windZ: sample.windZ,\n windSpeed: sample.windSpeed,\n gust: sample.gust,\n fogFar: sample.fogFar,\n visibility: sample.effects.visibility,\n nightFactor: sample.nightFactor,\n temperatureC: sample.temperatureC,\n wetness: sample.groundWetness,\n }\n}\n\nexport type WeatherBridges = {\n /** Follows this weather (the one before is let go) and pushes its values at once. */\n bind(weather: Weather): void\n /** Pushes the weather of now to every target; call it each frame after `weather.advance(dt)`. */\n update(dt: number): void\n dispose(): void\n}\n\n/**\n * Pushes one weather to the packages that react to it, each through a small interface of its own, so the weather depends on none of them and every\n * target is optional:\n * - fluids film: `setWeather({ rain: min(1, rain / filmRainFullAt), drying: dryingScale })`, and `wash()` every `washEvery` seconds while the rain is at\n * `washFrom` or stronger (light rain stops the drying and does not wash);\n * - interactables: `water.setRain(mm/h)` and `setWind(x, z)`;\n * - vegetation: `setWind({ direction: [x, z], strength: min(1, speed / windFull), gust: min(1, gust / windFull) })`;\n * - vfx: `setWind([x, 0, z])`;\n * - the environment bus: every value of `environmentOf`;\n * - sound: the rain and wind loops follow the weather, and each thunder that arrives plays at its bearing.\n */\nexport function createWeatherBridges(targets: WeatherTargets, options: WeatherBridgeOptions = {}): WeatherBridges {\n const rules = { ...DEFAULT_BRIDGE_RULES, ...options }\n const sound: WeatherSound | undefined = options.sound === false || !targets.audio ? undefined : createWeatherSound(targets.audio, options.sound ?? {})\n let weather: Weather | undefined\n let offThunder: (() => void) | undefined\n let sinceWash = 0\n let last: { rain?: number; windX?: number; windZ?: number; filmRain?: number; drying?: number } = {}\n\n const push = (): void => {\n if (!weather) return\n const sample = weather.sample()\n const { film, interactables, vegetation, vfx, environment } = targets\n environment?.set(environmentOf(sample))\n if (film) {\n const rain = Math.min(1, Math.max(0, sample.rain / rules.filmRainFullAt))\n const drying = sample.effects.dryingScale\n if (rain !== last.filmRain || drying !== last.drying) film.setWeather({ rain, drying })\n last.filmRain = rain\n last.drying = drying\n }\n if (interactables && sample.rain !== last.rain) interactables.water.setRain(sample.rain)\n const windChanged = sample.windX !== last.windX || sample.windZ !== last.windZ\n if (windChanged) {\n interactables?.setWind(sample.windX, sample.windZ)\n vfx?.setWind([sample.windX, 0, sample.windZ])\n if (vegetation) {\n const wind = { strength: Math.min(1, sample.windSpeed / rules.windFull), gust: Math.min(1, sample.gust / rules.windFull) }\n vegetation.setWind(sample.windSpeed > 1e-6 ? { direction: [sample.windX, sample.windZ], ...wind } : wind)\n }\n }\n last = { ...last, rain: sample.rain, windX: sample.windX, windZ: sample.windZ }\n sound?.update(sample)\n }\n\n const onThunder = (strike: Strike): void => sound?.thunder(strike)\n\n return {\n bind(next) {\n offThunder?.()\n weather = next\n offThunder = next.on('thunder', onThunder)\n last = {}\n sinceWash = 0\n push()\n },\n update(dt) {\n push()\n if (!weather || !targets.film?.wash) return\n if (weather.sample().rain < rules.washFrom) {\n sinceWash = 0\n return\n }\n sinceWash += dt\n if (sinceWash >= rules.washEvery) {\n sinceWash = 0\n targets.film.wash(rules.washBelow)\n }\n },\n dispose() {\n offThunder?.()\n offThunder = undefined\n weather = undefined\n sound?.dispose()\n },\n }\n}\n","export type WeatherBenchText = {\n readonly group: string\n readonly name: string\n readonly controls: Readonly<Record<'state' | 'transition' | 'time_of_day' | 'auto_schedule' | 'seed', string>>\n readonly hints: Readonly<Record<'transition' | 'time_of_day' | 'auto_schedule' | 'seed', string>>\n readonly options: Readonly<Record<'instant' | 'ten_seconds', string>>\n readonly layers: Readonly<Record<'environment' | 'fog', string>>\n readonly metrics: Readonly<Record<'state' | 'hour' | 'rain' | 'visibility' | 'sight' | 'hearing' | 'wind' | 'heat_index' | 'wetness' | 'strikes' | 'blend' | 'temperature', string>>\n readonly scenarios: Readonly<Record<'storm' | 'blend' | 'determinism' | 'dry_ground', string>>\n /** Names of the states and climates by id; the bench falls back to the id. */\n readonly states: Readonly<Record<string, string>>\n}\n\nconst EN: WeatherBenchText = {\n group: 'Weather',\n name: 'Weather area',\n controls: { state: 'Weather', transition: 'Transition', time_of_day: 'Time of day', auto_schedule: 'Automatic schedule', seed: 'Seed' },\n hints: {\n transition: 'How long the change to the chosen weather takes, in real seconds.',\n time_of_day: 'Hour of the day (the world restarts the clock there and keeps the weather).',\n auto_schedule: 'On: the climate changes the weather by itself and the clock runs 600 times faster than real time. Off: the weather stays until you change it.',\n seed: 'The same seed gives the same schedule and the same lightning.',\n },\n options: { instant: 'At once', ten_seconds: '10 seconds' },\n layers: { environment: 'Write host environment', fog: 'Fog in the scene' },\n metrics: {\n state: 'Weather',\n hour: 'Hour',\n rain: 'Rain',\n visibility: 'Visibility',\n sight: 'Sight scale',\n hearing: 'Hearing scale',\n wind: 'Wind',\n heat_index: 'Heat index',\n wetness: 'Ground wetness',\n strikes: 'Lightning strikes',\n blend: 'Blend',\n temperature: 'Temperature',\n },\n scenarios: {\n storm: 'A storm sees less and is heard less than a clear day, and strikes lightning',\n blend: 'A change blends over its time and settles in the new weather',\n determinism: 'The same seed gives the same day, another seed another one',\n dry_ground: 'Rain wets the ground and it dries after the rain stops',\n },\n states: {\n clear: 'Clear',\n overcast: 'Overcast',\n light_rain: 'Light rain',\n heavy_rain: 'Heavy rain',\n storm: 'Storm',\n fog: 'Fog',\n windy: 'Windy',\n cyclone: 'Cyclone',\n typhoon: 'Typhoon',\n extreme_heat: 'Extreme heat',\n },\n}\n\nconst PT_BR: WeatherBenchText = {\n group: 'Clima',\n name: 'Área de clima',\n controls: { state: 'Clima', transition: 'Transição', time_of_day: 'Hora do dia', auto_schedule: 'Sorteio automático', seed: 'Semente' },\n hints: {\n transition: 'Quanto demora a mudança para o clima escolhido, em segundos reais.',\n time_of_day: 'Hora do dia (o mundo reinicia o relógio nela e mantém o clima).',\n auto_schedule: 'Ligado: o clima do lugar muda sozinho e o relógio corre 600 vezes mais rápido que o real. Desligado: o clima fica até você trocar.',\n seed: 'A mesma semente dá o mesmo sorteio e os mesmos raios.',\n },\n options: { instant: 'De uma vez', ten_seconds: '10 segundos' },\n layers: { environment: 'Escrever no ambiente do host', fog: 'Névoa na cena' },\n metrics: {\n state: 'Clima',\n hour: 'Hora',\n rain: 'Chuva',\n visibility: 'Visibilidade',\n sight: 'Escala de visão',\n hearing: 'Escala de audição',\n wind: 'Vento',\n heat_index: 'Índice de calor',\n wetness: 'Chão molhado',\n strikes: 'Raios',\n blend: 'Mistura',\n temperature: 'Temperatura',\n },\n scenarios: {\n storm: 'Tempestade enxerga e é ouvida menos que um dia limpo, e solta raios',\n blend: 'Uma mudança se mistura no tempo dela e assenta no clima novo',\n determinism: 'A mesma semente dá o mesmo dia, outra semente dá outro',\n dry_ground: 'A chuva molha o chão e ele seca depois que a chuva para',\n },\n states: {\n clear: 'Limpo',\n overcast: 'Nublado',\n light_rain: 'Chuva fraca',\n heavy_rain: 'Chuva forte',\n storm: 'Tempestade',\n fog: 'Neblina',\n windy: 'Ventania',\n cyclone: 'Ciclone',\n typhoon: 'Tufão',\n extreme_heat: 'Calor extremo',\n },\n}\n\nexport const weatherBenchTextOf = (locale: string): WeatherBenchText => (locale === 'en' ? EN : PT_BR)\n","import type { BenchChange, ModuleControl, ModuleHost, ModuleLayer, ModuleMetric, ModuleScenario, TestableModule } from '@adatechnology/game-contracts'\n\nimport { createWeatherBridges, environmentOf } from '../bridges'\nimport type { WeatherBridges, WeatherTargets } from '../bridges'\nimport type { WeatherSoundOptions } from '../audio/sound'\nimport { createWeather } from '../weather'\nimport type { Weather, WeatherOptions } from '../weather'\nimport type { Strike, WeatherSample } from '../types'\nimport { weatherBenchTextOf } from './locale'\nimport type { WeatherBenchText } from './locale'\n\n/** What a renderer of the weather (game-weather-three) gives the bench: its layers, a step per frame and the switch of each layer. */\nexport type WeatherVisual = {\n update(dt: number): void\n setLayer(id: string, on: boolean): void\n dispose(): void\n}\n\n/** The bench hands a visual the weather it shows (always the live one, even after the bench restarts it) and what the room lends. */\nexport type WeatherVisualContext = { readonly host: ModuleHost; readonly locale: string; readonly weather: { sample(): WeatherSample } }\n\n/** The bench declares its layers before the room lends anything, so the factory tells them first (`fog` is the bench's own layer and reaches the visual through `setLayer` too). */\nexport type WeatherVisualFactory = {\n layers(locale: string): readonly ModuleLayer[]\n create(context: WeatherVisualContext): WeatherVisual\n}\n\nexport type WeatherTestModuleOptions = WeatherOptions & {\n locale?: string\n /** Text of a state id in the game's words; without it the bench uses its own names, then the id. */\n translate?: (key: string, locale: string) => string | undefined\n /** The weather to test (default a new one built from the other options). */\n weather?: Weather\n /** Whether the scene fog follows the weather at first (default true; the layer can be switched either way). A room that is not about weather starts it off. */\n fog?: boolean\n /** The packages the weather drives besides the host's environment bus and sound (fluids film, interactables, vegetation, vfx), from the example. */\n targets?: Omit<WeatherTargets, 'environment' | 'audio'>\n /** The rain, wind and thunder on the host's `audio`; `false` keeps the area silent. */\n sound?: WeatherSoundOptions | false\n /** The picture of the weather (rain, fog, sky, lightning, headlights, haze), from `@adatechnology/game-weather-three`. With it the bench leaves the scene fog to the visual. */\n visual?: WeatherVisualFactory\n}\n\n/** What the scenarios use of the context the test room passes. */\nexport interface WeatherScenarioLike {\n expect(condition: boolean, label: string, data?: Record<string, unknown>): void\n}\n\nexport type WeatherTestModule = TestableModule<WeatherScenarioLike> & {\n /** The weather in use (replaced when the bench changes the seed or the hour). */\n readonly weather: Weather\n}\n\nconst FAST_SCALE = 600\nconst NORMAL_SCALE = 72\nconst BLEND_SECONDS = 10\nconst HOURS_PER_DAY = 24\nconst FOG_NEAR = 1\n\ntype FogLike = new (color: number, near: number, far: number) => { far: number }\ntype SceneLike = { fog?: unknown }\ntype ThreeLike = { Fog?: FogLike }\n\n/**\n * The weather area of a test room, written against the generic contracts only: it drives a `createWeather` world, writes the result into the host's\n * environment bus (`host.environment`) every frame and shows the numbers a game reads from it. It also sets the scene fog from the weather's render fog.\n * Pure: no three.js beyond the host's own `Fog`, and the sky, rain, lightning and sound wait for the packages that draw and play them.\n */\nexport function createWeatherTestModule(options: WeatherTestModuleOptions = {}): WeatherTestModule {\n const locale = options.locale ?? 'pt-BR'\n const text: WeatherBenchText = weatherBenchTextOf(locale)\n const { locale: _locale, translate, weather: given, fog: _fog, targets: _targets, sound: _sound, visual: _visual, ...weatherOptions } = options\n void _locale\n void _fog\n void _targets\n void _sound\n void _visual\n const state = {\n seed: options.seed ?? 1,\n hour: options.startHour ?? 8,\n transition: 0,\n auto: options.autoSchedule ?? false,\n environment: true,\n fog: options.fog ?? true,\n }\n let host: ModuleHost | null = null\n let strikes = 0\n let offStrike: (() => void) | undefined\n let bridges: WeatherBridges | undefined\n let visual: WeatherVisual | undefined\n let weather: Weather = given ?? createWeather({ ...weatherOptions, autoSchedule: state.auto, ...(state.auto ? { timeScale: FAST_SCALE } : {}) })\n\n const watch = (): void => {\n offStrike?.()\n offStrike = weather.on('strike', (_strike: Strike) => void (strikes += 1))\n }\n watch()\n\n const nameOf = (id: string): string => translate?.(`weather.${id}`, locale) ?? text.states[id] ?? id\n\n // the world restarts with the new seed or hour and keeps the weather it was in\n const rebuild = (): void => {\n const current = weather.state()\n weather = createWeather({ ...weatherOptions, seed: state.seed, start: current, startHour: state.hour, autoSchedule: state.auto, timeScale: state.auto ? FAST_SCALE : NORMAL_SCALE })\n strikes = 0\n watch()\n bridges?.bind(weather)\n }\n\n const applyFog = (far: number): void => {\n const scene = host?.scene as SceneLike | undefined\n const Fog = (host?.THREE as ThreeLike | undefined)?.Fog\n if (!scene || typeof Fog !== 'function') return\n const fog = scene.fog as { far: number } | undefined | null\n if (fog && typeof fog.far === 'number') fog.far = far\n else scene.fog = new Fog(0xa8b4c0, FOG_NEAR, far)\n }\n\n const publish = (): void => {\n const sample = weather.sample()\n if (state.environment) host?.environment?.set(environmentOf(sample))\n if (state.fog && !visual) applyFog(sample.fogFar)\n }\n\n const onChange = (change: BenchChange): void => {\n if (change.kind === 'layer') {\n if (change.id === 'environment' || change.id === 'fog') state[change.id] = change.on\n visual?.setLayer(change.id, change.on)\n return\n }\n if (change.kind === 'action') return\n const { id, value } = change\n if (id === 'state' && typeof value === 'string') weather.force(value, state.transition)\n else if (id === 'transition' && typeof value === 'string') state.transition = value === 'instant' ? 0 : BLEND_SECONDS\n else if (id === 'time_of_day' && typeof value === 'number') {\n state.hour = value\n rebuild()\n } else if (id === 'auto_schedule' && typeof value === 'boolean') {\n state.auto = value\n weather.setAutoSchedule(value)\n weather.setTimeScale(value ? FAST_SCALE : NORMAL_SCALE)\n } else if (id === 'seed' && typeof value === 'number') {\n state.seed = value\n rebuild()\n }\n }\n\n const controls = (): ModuleControl[] => [\n { kind: 'choice', id: 'state', name: text.controls.state, options: weather.registry.stateIds().map((id) => ({ id, name: nameOf(id) })), default: weather.state() },\n {\n kind: 'choice',\n id: 'transition',\n name: text.controls.transition,\n hint: text.hints.transition,\n options: [\n { id: 'instant', name: text.options.instant },\n { id: 'ten_seconds', name: text.options.ten_seconds },\n ],\n default: 'instant',\n },\n { kind: 'number', id: 'time_of_day', name: text.controls.time_of_day, hint: text.hints.time_of_day, unit: 'h', min: 0, max: HOURS_PER_DAY - 1, step: 1, default: state.hour },\n { kind: 'boolean', id: 'auto_schedule', name: text.controls.auto_schedule, hint: text.hints.auto_schedule, default: state.auto },\n { kind: 'number', id: 'seed', name: text.controls.seed, hint: text.hints.seed, min: 1, max: 9999, step: 1, default: state.seed },\n ]\n\n const layers = (): ModuleLayer[] => [\n { id: 'environment', name: text.layers.environment },\n { id: 'fog', name: text.layers.fog, default: state.fog },\n ...(options.visual?.layers(locale) ?? []),\n ]\n\n const metrics = (): ModuleMetric[] => [\n { id: 'current_state', name: text.metrics.state, read: () => nameOf(weather.state()) },\n { id: 'hour', name: text.metrics.hour, unit: 'h', digits: 1, read: () => weather.sample().hour },\n { id: 'blend', name: text.metrics.blend, digits: 2, read: () => weather.sample().blend },\n { id: 'rain', name: text.metrics.rain, unit: 'mm/h', digits: 1, read: () => weather.sample().rain },\n { id: 'visibility', name: text.metrics.visibility, unit: 'm', digits: 0, read: () => weather.sample().effects.visibility },\n { id: 'sight', name: text.metrics.sight, digits: 2, read: () => weather.sample().effects.sightScale },\n { id: 'hearing', name: text.metrics.hearing, digits: 2, read: () => weather.sample().effects.hearingScale },\n { id: 'wind', name: text.metrics.wind, unit: 'm/s', digits: 1, read: () => weather.sample().windSpeed },\n { id: 'temperature', name: text.metrics.temperature, unit: '°C', digits: 1, read: () => weather.sample().temperatureC },\n { id: 'heat_index', name: text.metrics.heat_index, unit: '°C', digits: 1, read: () => weather.sample().effects.heatIndexC },\n { id: 'wetness', name: text.metrics.wetness, digits: 2, read: () => weather.sample().groundWetness },\n { id: 'strikes', name: text.metrics.strikes, digits: 0, read: () => strikes },\n ]\n\n // each scenario runs its own worlds, so the bench's live one is not touched\n const fresh = (extra: WeatherOptions = {}): Weather => createWeather({ ...weatherOptions, autoSchedule: false, ...extra })\n const run = (world: Weather, seconds: number): void => {\n for (let elapsed = 0; elapsed < seconds; elapsed += 0.5) world.advance(0.5)\n }\n\n const scenarios = (): Array<ModuleScenario<WeatherScenarioLike>> => [\n {\n id: 'storm',\n name: text.scenarios.storm,\n run: ({ expect }) => {\n const clear = fresh({ seed: 3, start: 'clear' })\n const storm = fresh({ seed: 3, start: 'storm' })\n let hits = 0\n storm.on('strike', () => void (hits += 1))\n run(storm, 120)\n const calm = clear.sample().effects\n const rough = storm.sample().effects\n expect(rough.visibility < calm.visibility, 'a storm sees less far', { storm: rough.visibility, clear: calm.visibility })\n expect(rough.hearingScale < calm.hearingScale, 'rain masks sound', { storm: rough.hearingScale, clear: calm.hearingScale })\n expect(hits > 0, 'a storm strikes lightning', { strikes: hits })\n },\n },\n {\n id: 'blend',\n name: text.scenarios.blend,\n run: ({ expect }) => {\n const world = fresh({ seed: 3, start: 'clear' })\n world.force('heavy_rain', 10)\n run(world, 5)\n const middle = world.sample()\n expect(middle.blend > 0 && middle.blend < 1, 'halfway through the change it is still blending', { blend: middle.blend })\n expect(middle.rain > 0 && middle.rain < 15, 'the rain is between the two weathers', { rain: middle.rain })\n run(world, 6)\n expect(world.sample().blend === 1 && world.sample().state === 'heavy_rain', 'after its time it has settled', { blend: world.sample().blend })\n },\n },\n {\n id: 'determinism',\n name: text.scenarios.determinism,\n run: ({ expect }) => {\n const day = (seed: number): string => JSON.stringify(createWeather({ ...weatherOptions, seed }).timeline(48).samples)\n expect(day(5) === day(5), 'the same seed gives the same two days')\n expect(day(5) !== day(6), 'another seed gives another one')\n },\n },\n {\n id: 'dry_ground',\n name: text.scenarios.dry_ground,\n run: ({ expect }) => {\n const world = fresh({ seed: 3, start: 'heavy_rain' })\n run(world, 60)\n const wet = world.sample().groundWetness\n expect(wet > 0.3, 'rain wets the ground', { wetness: wet })\n world.force('clear', 0)\n run(world, 600)\n expect(world.sample().groundWetness < wet, 'it dries after the rain stops', { wet, after: world.sample().groundWetness })\n },\n },\n ]\n\n const module: WeatherTestModule = {\n id: 'weather_area',\n name: text.name,\n ground: { surface: 'lawn' },\n group: text.group,\n get weather() {\n return weather\n },\n items: () => [],\n controls: () => controls(),\n layers: () => layers(),\n metrics: () => metrics(),\n scenarios: () => scenarios(),\n attach(lent) {\n host = lent\n const off = lent.bench?.onChange(onChange)\n bridges = createWeatherBridges({ ...options.targets, ...(lent.audio ? { audio: lent.audio } : {}) }, { sound: options.sound ?? {} })\n bridges.bind(weather)\n visual = options.visual?.create({ host: lent, locale, weather: { sample: () => weather.sample() } })\n if (visual && !state.fog) visual.setLayer('fog', false)\n publish()\n const offFrame = lent.onFrame((dt) => {\n weather.advance(dt)\n publish()\n bridges?.update(dt)\n visual?.update(dt)\n })\n return () => {\n visual?.dispose()\n visual = undefined\n off?.()\n offFrame()\n bridges?.dispose()\n bridges = undefined\n host = null\n }\n },\n }\n return module\n}\n"],"mappings":"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","names":["landing","landing"]}
|