@adatechnology/game-animals-three 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.global.js +949 -12
- package/dist/index.global.js.map +1 -1
- package/package.json +5 -5
package/dist/index.global.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../../game-contracts/src/blast-shove.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/falloff.ts","../../game-audio/src/data/game-sfx.json","../../game-audio/src/recipes/noise-class.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/data/mix-profiles.json","../../game-audio/src/mix.ts","../../game-audio/src/recipe.ts","../../game-audio/src/data/voice-groups.json","../../game-audio/src/voices.ts","../../game-audio/src/bank.ts","../../game-audio/src/listener.ts","../../game-audio/src/rng.ts","../../game-audio/src/plan.ts","../../game-audio/src/webaudio/noise.ts","../../game-audio/src/webaudio/chain.ts","../../game-audio/src/webaudio/context.ts","../../game-audio/src/loops.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/variation.ts","../../game-audio/src/webaudio/engine.ts","../../game-audio/src/bench/locale.ts","../../game-audio/src/bench/module.ts","../../game-audio/src/recipes/scene.ts","../../game-audio/src/recipes/body-foley.ts","../../game-audio/src/webaudio/sink.ts","../src/audio/recipes.ts","../src/audio/calls.json","../src/audio/sound.ts","../src/context.ts","../src/extras.ts","../../game-animals/src/data/gaits.json","../../game-animals/src/gaits.ts","../../game-animals/src/layout.ts","../../game-animals/src/registry.ts","../../game-animals/src/kit.ts","../src/line-up.ts","../../game-shapes/src/color.ts","../../game-shapes/src/hash.ts","../../game-shapes/src/math.ts","../../game-shapes/src/primitives/three-generators.ts","../../game-shapes/src/primitives/difference.ts","../../game-shapes/src/primitives/earcut.ts","../../game-shapes/src/primitives/extrude.ts","../../game-shapes/src/primitives/index.ts","../../game-shapes/src/random.ts","../../game-shapes/src/registry.ts","../../game-shapes/schema/shape.schema.json","../../game-shapes/src/schema.ts","../../game-shapes/src/generate.ts","../../game-shapes/src/merge.ts","../../game-shapes/src/worker.ts","../../game-shapes-three/src/geometry.ts","../../game-shapes-three/src/namespace.ts","../../game-shapes-three/src/object.ts","../../game-shapes-three/src/merge-by-material.ts","../../game-shapes-three/src/cord.ts","../../game-shapes-three/src/release.ts","../src/profile.ts","../src/textures.ts","../src/visual-state.ts","../src/views.ts","../../game-props3d/src/colors.ts","../../game-props3d/src/atlas.ts","../../game-props3d/src/catalog.ts","../../game-props3d/src/builders/ceiling.ts","../../game-props3d/src/builders/floors.ts","../../game-props3d/src/builders/shared.ts","../../game-props3d/src/builders/furniture.ts","../../game-props3d/src/builders/house-state.ts","../../game-props3d/src/builders/plants.ts","../../game-props3d/src/builders/wall.ts","../../game-props3d/src/builders/yard.ts","../../game-props3d/src/cover.ts","../../game-props3d/src/kit.ts","../../game-props3d/src/locale.ts","../../game-props3d/src/create.ts","../../game-props3d/src/node-skin.ts","../../game-props3d/src/recorder.ts","../../game-props3d/src/merge-by-material.ts","../../game-props3d/src/shape-recorder.ts","../src/animate-bird.ts","../src/animate-quadruped.ts","../src/animate-snake.ts","../src/bird.ts","../src/quadruped.ts","../src/snake.ts","../src/animals-three.ts","../../game-render-three/src/lights/slots.ts","../../game-render-three/src/lights/arbiter.ts","../../game-render-three/src/fog.ts","../../game-render-three/src/lights/scene-state.ts","../../game-render-three/src/lights/budget.ts","../../game-render-three/src/lights/types.ts","../../game-render-three/src/lights/guard.ts","../../game-render-three/src/program-key.ts","../../game-render-three/src/pools/anchors.ts","../../game-render-three/src/probes.ts","../../game-render-three/src/warmup.ts","../../game-render-three/src/pools/pooled.ts","../../game-render-three/src/pools/geometry.ts","../../game-render-three/src/pools/material.ts","../../game-render-three/src/pools/render-pools.ts","../../game-render-three/src/frame-pacer.ts","../../game-render-three/src/static-batcher.ts","../src/test-area/scenarios.ts","../src/test-area/text.ts","../src/test-area/module.ts"],"sourcesContent":["/**\n * @adatechnology/game-animals-three: the three.js drawing of the animals, a literal port of the Bichos do Vale builders (`quad`, `bird`, `snake`) and\n * their animation, with the alive / infected / dead looks for every species, the joints and split views, and a GPU node skin for crowds.\n */\n\nexport { ANIMAL_SOUND_BANK, ANIMAL_SOUNDS, COW_MOO, DOG_BARK, HEN_CLUCK, LAPWING_CALL } from './audio/recipes'\nexport { ANIMAL_CALLS, ANIMAL_CALL_SOURCES, createAnimalSound } from './audio/sound'\nexport type { AnimalCall, AnimalSound, AnimalSoundOptions, AnimalSoundSink } from './audio/sound'\nexport type { AnimalLeg, AnimalPivot, AnimalRig, AnimalWing, AnimalsThreeOptions, CanvasLike, DocumentLike, Three } from './types'\nexport type { AnimateParams } from './animate-types'\nexport type { BirdExtra, BirdParts, ExtraContext, QuadrupedExtra, QuadrupedParts } from './extras'\nexport { BIRD_EXTRAS, QUADRUPED_EXTRAS } from './extras'\nexport { lineUpAnimals } from './line-up'\nexport type { LineUpOptions } from './line-up'\nexport { profileGeometry } from './profile'\nexport { createCoatTexture } from './textures'\nexport { applyVisualState } from './visual-state'\nexport { setJointView, setSplit, updateSplit } from './views'\n\nexport type { AnimalsThree } from './animals-three'\nexport { createAnimalsThree } from './animals-three'\n/** @deprecated import from '@adatechnology/game-animals-three/testing' */\nexport { createAnimalsTestModule, ANIMALS_AREA_ID, ANIMAL_ITEM_PREFIX } from './testing'\n/** @deprecated import from '@adatechnology/game-animals-three/testing' */\nexport type { AnimalsContext } from './testing'\n/** @deprecated import from '@adatechnology/game-animals-three/testing' */\nexport type { AnimalsTestOptions, OutbreakLike } from './testing'\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","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\n/** A use animation a module brings with its item. Poses between keys are interpolated by the player (the test room does it linearly). */\nexport interface AnimationClipSpec {\n /** snake_case: `reload`, `open`, `drink`... Unique within the item. */\n id: string\n /** Seconds. */\n duration: number\n keys: AnimationKey[]\n events?: AnimationEventSpec[]\n}\n\n/** What an item exposes when it brings its own animations. */\nexport interface ItemAnimationContract {\n animations: AnimationClipSpec[]\n}\n\nconst finite3 = (v: unknown): boolean => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === 'number' && Number.isFinite(n))\n\n/**\n * Problems with a clip (empty when it is valid): a snake_case id, a positive finite duration, at least one key, key and event times inside\n * `[0, duration]` and not going backwards, finite vectors, every named part one of `partIds`, event types that are non-empty strings and\n * JSON-serializable event data.\n */\nexport const checkAnimation = (clip: AnimationClipSpec, partIds: Iterable<string>): { ok: boolean; problems: string[] } => {\n const problems: string[] = []\n const parts = new Set(partIds)\n const id = String(clip?.id ?? '')\n if (!/^[a-z][a-z0-9_]*$/.test(id)) problems.push(`clip id \"${id}\" must be snake_case`)\n const label = `clip \"${id}\"`\n if (!(typeof clip?.duration === 'number' && Number.isFinite(clip.duration) && clip.duration > 0)) problems.push(`${label} needs a positive finite duration`)\n const dur = typeof clip?.duration === 'number' && clip.duration > 0 ? clip.duration : Infinity\n if (!Array.isArray(clip?.keys) || clip.keys.length === 0) {\n problems.push(`${label} needs at least one key`)\n return { ok: false, problems }\n }\n let prev = -Infinity\n clip.keys.forEach((k, i) => {\n if (!(typeof k.t === 'number' && Number.isFinite(k.t)) || k.t < 0 || k.t > dur + 1e-9) problems.push(`${label} key ${i} has t=${String(k.t)} outside 0..${dur}`)\n else if (k.t < prev) problems.push(`${label} key ${i} goes back in time (${k.t} < ${prev})`)\n else prev = k.t\n for (const f of ['position', 'rotation'] as const) if (k[f] !== undefined && !finite3(k[f])) problems.push(`${label} key ${i} has an invalid ${f}`)\n for (const [pid, pose] of Object.entries(k.parts ?? {})) {\n if (!parts.has(pid)) problems.push(`${label} key ${i} moves unknown part \"${pid}\"`)\n for (const f of ['position', 'rotation'] as const) if (pose?.[f] !== undefined && !finite3(pose[f])) problems.push(`${label} key ${i} part \"${pid}\" has an invalid ${f}`)\n }\n })\n let prevEv = -Infinity\n ;(clip.events ?? []).forEach((e, i) => {\n if (!(typeof e.t === 'number' && Number.isFinite(e.t)) || e.t < 0 || e.t > dur + 1e-9) problems.push(`${label} event ${i} has t=${String(e.t)} outside 0..${dur}`)\n else if (e.t < prevEv) problems.push(`${label} event ${i} goes back in time`)\n else prevEv = e.t\n if (typeof e.type !== 'string' || e.type === '') problems.push(`${label} event ${i} needs a type`)\n try {\n JSON.stringify(e.data ?? {})\n } catch {\n problems.push(`${label} event ${i} data is not JSON`)\n }\n })\n return { ok: problems.length === 0, problems }\n}\n","import type { AnimationClipSpec, AnimationEventSpec, AnimationPose } from './animation'\nimport { checkAnimation } from './animation'\nimport type { HostEvent } from './module'\n\n/** Clips a tool can bring, one per move of its cycle. A tool brings the ones it has; a missing clip is simply not played. */\nexport const TOOL_CLIP = {\n DRAW: 'draw',\n SHOT: 'shot',\n MISFIRE: 'misfire',\n EMPTY: 'empty',\n CYCLE: 'cycle',\n RELOAD: 'reload',\n FEED: 'feed',\n FAULT: 'fault',\n CLEAR: 'clear',\n SAFETY_ON: 'safety_on',\n SAFETY_OFF: 'safety_off',\n MODE: 'mode',\n /** A throwable with the pin out, held until it is thrown. */\n COOK: 'cook',\n /** The stock folds, unfolds, slides in or out (variants `fold`, `unfold`, `collapse`, `extend`). */\n STOCK: 'stock',\n /** A light or a laser is switched (variants `on`, `off`). */\n TOGGLE: 'toggle',\n} as const\nexport type ToolClipId = (typeof TOOL_CLIP)[keyof typeof TOOL_CLIP]\n\n/** What a renderer does when a rule event arrives. `variant` picks `<clip>_<variant>` when the tool has it, else `<clip>`. */\nexport type ToolAnimationStep =\n | { readonly type: 'play'; readonly clip: ToolClipId; readonly variant?: string; readonly duration?: number; readonly repeat?: number }\n /** Move the clip that is playing to the marker (a clip event `stage` with `data.stage`) the rule just reached. */\n | { readonly type: 'sync'; readonly stage: string }\n /** The rule says the phase is over: the clip jumps to its end if it is still running. */\n | { readonly type: 'finish'; readonly clip: ToolClipId }\n\nconst stockVariant = (event: Extract<HostEvent, { type: 'stock' }>): string => {\n const isFolding = event.kind === undefined ? false : event.kind.includes('fold')\n if (event.state === 'collapsed') return isFolding ? 'fold' : 'collapse'\n return isFolding ? 'unfold' : 'extend'\n}\n\n/** The move a host event asks of the tool in hand, or null when it has no motion. */\nexport const toolAnimationStep = (event: HostEvent): ToolAnimationStep | null => {\n switch (event.type) {\n case 'tool':\n return { type: 'play', clip: TOOL_CLIP.DRAW, ...(event.duration === undefined ? {} : { duration: event.duration }) }\n case 'shot':\n return { type: 'play', clip: TOOL_CLIP.SHOT, repeat: Math.max(1, event.rounds ?? 1) }\n case 'misfire':\n return { type: 'play', clip: TOOL_CLIP.MISFIRE }\n case 'empty':\n return { type: 'play', clip: TOOL_CLIP.EMPTY }\n case 'mode':\n return { type: 'play', clip: TOOL_CLIP.MODE, variant: event.mode }\n case 'fault':\n return { type: 'play', clip: TOOL_CLIP.FAULT, variant: event.fault }\n case 'clear':\n if (event.phase === 'end') return { type: 'finish', clip: TOOL_CLIP.CLEAR }\n return { type: 'play', clip: TOOL_CLIP.CLEAR, variant: event.fault, ...(event.duration === undefined ? {} : { duration: event.duration }) }\n case 'safety':\n return { type: 'play', clip: event.on ? TOOL_CLIP.SAFETY_ON : TOOL_CLIP.SAFETY_OFF }\n case 'cycle':\n if (event.phase === 'end') return { type: 'finish', clip: TOOL_CLIP.CYCLE }\n return { type: 'play', clip: TOOL_CLIP.CYCLE, ...(event.duration === undefined ? {} : { duration: event.duration }) }\n case 'reload':\n if (event.phase === 'end') return { type: 'finish', clip: TOOL_CLIP.RELOAD }\n return { type: 'play', clip: TOOL_CLIP.RELOAD, ...(event.kind === undefined ? {} : { variant: event.kind }), ...(event.duration === undefined ? {} : { duration: event.duration }) }\n case 'stage':\n return { type: 'sync', stage: event.stage }\n case 'feed':\n return { type: 'play', clip: TOOL_CLIP.FEED }\n case 'cook':\n if (event.phase === 'start') return { type: 'play', clip: TOOL_CLIP.COOK, duration: event.fuse }\n return { type: 'finish', clip: TOOL_CLIP.COOK }\n case 'stock':\n return { type: 'play', clip: TOOL_CLIP.STOCK, variant: stockVariant(event), ...(event.duration === undefined ? {} : { duration: event.duration }) }\n case 'toggle':\n return { type: 'play', clip: TOOL_CLIP.TOGGLE, variant: event.on ? 'on' : 'off' }\n default:\n return null\n }\n}\n\n/** Where a renderer puts the poses: every part of the tool by id (the ids the clips move), and the whole tool. Poses are offsets from rest. */\nexport interface ToolRig {\n setPartPose(partId: string, pose: AnimationPose): void\n setRootPose?(pose: AnimationPose): void\n}\n\n/** Clips by id, with the variants as `<clip>_<variant>`. */\nexport type ToolClipSet = Readonly<Record<string, AnimationClipSpec>>\n\n/** A renderer's tool animator: feed it the host events and the frame time. */\nexport interface ToolAnimator {\n handle(event: HostEvent): void\n update(dt: number): void\n /** Ids of the clips playing now. */\n readonly playing: readonly string[]\n dispose(): void\n}\n\nexport const STAGE_EVENT = 'stage'\n\n/** The clip with its times (keys and events) scaled so that it lasts `duration` seconds. */\nexport const scaleClip = (clip: AnimationClipSpec, duration: number): AnimationClipSpec => {\n const factor = clip.duration > 0 ? duration / clip.duration : 1\n return {\n ...clip,\n duration,\n keys: clip.keys.map((key) => ({ ...key, t: key.t * factor })),\n ...(clip.events ? { events: clip.events.map((event) => ({ ...event, t: event.t * factor })) } : {}),\n }\n}\n\nconst ZERO: [number, number, number] = [0, 0, 0]\nconst mix = (from: readonly number[], to: readonly number[], fraction: number): [number, number, number] => [\n (from[0] ?? 0) + ((to[0] ?? 0) - (from[0] ?? 0)) * fraction,\n (from[1] ?? 0) + ((to[1] ?? 0) - (from[1] ?? 0)) * fraction,\n (from[2] ?? 0) + ((to[2] ?? 0) - (from[2] ?? 0)) * fraction,\n]\n\n/** Pose of the whole tool (`partId` undefined) or of one part at `time`, linear between keys; a key that does not name it means rest. */\nexport const sampleClip = (clip: AnimationClipSpec, partId: string | undefined, time: number): { position: [number, number, number]; rotation: [number, number, number] } => {\n const points = clip.keys.map((key) => {\n const pose = partId === undefined ? key : key.parts?.[partId]\n return { t: key.t, position: pose?.position ?? ZERO, rotation: pose?.rotation ?? ZERO }\n })\n const first = points[0]\n if (!first || first.t > 0) points.unshift({ t: 0, position: ZERO, rotation: ZERO })\n const last = points[points.length - 1]\n if (!last) return { position: ZERO, rotation: ZERO }\n if (time >= last.t) return { position: mix(last.position, last.position, 0), rotation: mix(last.rotation, last.rotation, 0) }\n const index = Math.max(0, points.findIndex((point, i) => time >= point.t && time < (points[i + 1]?.t ?? Infinity)))\n const from = points[index]\n const to = points[index + 1]\n if (!from || !to) return { position: ZERO, rotation: ZERO }\n const fraction = to.t > from.t ? (time - from.t) / (to.t - from.t) : 0\n return { position: mix(from.position, to.position, fraction), rotation: mix(from.rotation, to.rotation, fraction) }\n}\n\n/** Problems with a clip set (empty when valid): each key is the clip id (or `<clip>_<variant>`), every clip passes `checkAnimation`, and a `stage` event names its stage. */\nexport const checkToolClips = (clips: ToolClipSet, partIds: Iterable<string>): { ok: boolean; problems: string[] } => {\n const parts = [...partIds]\n const problems: string[] = []\n for (const [key, clip] of Object.entries(clips)) {\n if (clip.id !== key) problems.push(`clip \"${key}\" has the id \"${clip.id}\"`)\n if (!(clip.duration > 0)) problems.push(`clip \"${key}\" has no duration`)\n problems.push(...checkAnimation(clip, parts).problems)\n for (const event of clip.events ?? []) if (event.type === STAGE_EVENT && typeof event.data?.stage !== 'string') problems.push(`clip \"${key}\" has a stage event without data.stage`)\n }\n return { ok: problems.length === 0, problems }\n}\n\ninterface Instance {\n readonly name: string\n readonly clip: AnimationClipSpec\n time: number\n delay: number\n fired: number\n}\n\nexport interface ToolClipPlayerOptions {\n clips: ToolClipSet\n rig: ToolRig\n /** Called for each clip event as its time passes (`eject_casing`, `drop_magazine`, `stage`...). */\n onEvent?(event: AnimationEventSpec, clip: string): void\n /** Seconds between the rounds of a `repeat` (a burst): default 0.1. */\n repeatInterval?(): number\n}\n\n/** The default player of a tool's clips: runs the steps of `toolAnimationStep` against a rig, scaled to the duration the rule gives. */\nexport interface ToolClipPlayer {\n apply(step: ToolAnimationStep): void\n update(dt: number): void\n readonly playing: readonly string[]\n /** The clips being played with the time each has reached and the clip itself (scaled to the duration the rule gave), for a renderer that has to follow what the clip does now (the hands working a part). */\n readonly running: ReadonlyArray<{ readonly name: string; readonly time: number; readonly clip: AnimationClipSpec }>\n}\n\nexport const createToolClipPlayer = (options: ToolClipPlayerOptions): ToolClipPlayer => {\n const { clips, rig, onEvent } = options\n let instances: Instance[] = []\n\n const find = (clip: string, variant?: string): { name: string; clip: AnimationClipSpec } | undefined => {\n const name = variant !== undefined && clips[`${clip}_${variant}`] ? `${clip}_${variant}` : clip\n const found = clips[name]\n return found ? { name, clip: found } : undefined\n }\n\n const render = (instance: Instance): void => {\n const time = Math.min(instance.time, instance.clip.duration)\n const root = sampleClip(instance.clip, undefined, time)\n if (instance.clip.keys.some((key) => key.position || key.rotation)) rig.setRootPose?.(root)\n const touched = new Set(instance.clip.keys.flatMap((key) => Object.keys(key.parts ?? {})))\n for (const id of touched) rig.setPartPose(id, sampleClip(instance.clip, id, time))\n }\n\n const fireEvents = (instance: Instance, upTo: number): void => {\n const events = instance.clip.events ?? []\n while (instance.fired < events.length && (events[instance.fired]?.t ?? Infinity) <= upTo + 1e-9) {\n const event = events[instance.fired]\n instance.fired += 1\n if (event) onEvent?.(event, instance.name)\n }\n }\n\n return {\n apply(step) {\n if (step.type === 'sync') {\n for (const instance of instances) {\n const marker = (instance.clip.events ?? []).find((event) => event.type === STAGE_EVENT && event.data?.stage === step.stage)\n if (marker && instance.time < marker.t) instance.time = marker.t\n }\n return\n }\n if (step.type === 'finish') {\n for (const instance of instances) {\n if (instance.name !== step.clip && !instance.name.startsWith(`${step.clip}_`)) continue\n fireEvents(instance, instance.clip.duration)\n instance.time = instance.clip.duration\n }\n return\n }\n const found = find(step.clip, step.variant)\n if (!found) return\n const clip = step.duration && step.duration > 0 ? scaleClip(found.clip, step.duration) : found.clip\n const interval = options.repeatInterval?.() ?? 0.1\n instances = instances.filter((existing) => existing.name !== found.name || existing.delay > 0)\n for (let round = 0; round < (step.repeat ?? 1); round++) {\n instances.push({ name: found.name, clip, time: 0, delay: round * interval, fired: 0 })\n }\n },\n update(dt) {\n for (const instance of instances) {\n const carried = instance.delay > 0 ? dt - instance.delay : dt\n if (instance.delay > 0) {\n instance.delay = Math.max(0, instance.delay - dt)\n if (instance.delay > 0) continue\n }\n instance.time += carried\n fireEvents(instance, instance.time)\n render(instance)\n }\n instances = instances.filter((instance) => instance.delay > 0 || instance.time < instance.clip.duration)\n },\n get playing() {\n return instances.map((instance) => instance.name)\n },\n get running() {\n return instances.filter((instance) => instance.delay <= 0).map((instance) => ({ name: instance.name, time: instance.time, clip: instance.clip }))\n },\n }\n}\n","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 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, 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, ModuleItemFluid, ModuleItemSeal, ModuleScenario, PlayerState, ItemSeat, 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","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","{\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","{\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","[\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","/**\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\n/**\n * One id that plays one of several recipes (the takes of a footstep, of a punch): `play(id)` draws a variant, never the one it played last time (unless\n * `repeat`), and plays it at a rate drawn from `rate` (1.04 is 4% higher and 4% shorter; see `retunePlan`), so ten footsteps never sound like one.\n */\nexport type SoundSet = {\n readonly id: string\n /** Recipe ids, at least one. */\n readonly variants: readonly string[]\n /** `[min, max]` of the playback rate (pitch and speed together). Default `[1, 1]`. */\n readonly rate?: readonly [min: number, max: number]\n /** Allow the same variant twice in a row (default false). */\n readonly repeat?: boolean\n}\n\nexport type SoundBank = {\n readonly id: string\n readonly sounds?: readonly SoundRecipe[]\n readonly loops?: readonly LoopRecipe[]\n readonly sets?: readonly SoundSet[]\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 { LoopRecipe, SoundBank, SoundRecipe, SoundSet } 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 /** The set an id names (`SoundBank.sets`), if any; a recipe with the same id wins in `play`. */\n set(id: string): SoundSet | undefined\n soundIds(): string[]\n setIds(): 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 sets = new Map<string, SoundSet>()\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 for (const set of bank.sets ?? []) {\n if (set.variants.length === 0) throw new Error(`sound set ${set.id} has no variant`)\n sets.set(set.id, set)\n owners.set(set.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 sets.delete(id)\n owners.delete(id)\n },\n sound: (id) => sounds.get(id),\n loop: (id) => loops.get(id),\n set: (id) => sets.get(id),\n soundIds: () => [...sounds.keys()],\n setIds: () => [...sets.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","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","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","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","/** `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 { 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","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","import { DEFERRED_QUEUE_ID } from '@adatechnology/game-contracts'\nimport type { DeferredQueue } from '@adatechnology/game-contracts'\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 = {\n push(job: Omit<StartJob, 'queuedAt'>): void\n clear(): void\n readonly size: number\n /** The same queue as a `DeferredQueue` for a shared frame budget. Asking for it changes nothing: the queue drains by itself until a budget attaches it. */\n deferredQueue?(): DeferredQueue\n}\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. */\n/** The queue this package makes always has its `deferredQueue`; a third-party `StartQueue` may leave it out. */\nexport type OwnStartQueue = StartQueue & { deferredQueue(): DeferredQueue }\n\nexport function createStartQueue(options: StartQueueOptions = {}): OwnStartQueue {\n const budgetMs = options.budgetMs ?? 2\n const maxLatencyMs = options.maxLatencyMs ?? 250\n let jobs: StartJob[] = []\n let scheduled = false\n // true once a frame budget owns the draining: nothing schedules a task by itself\n let attached = false\n\n const runNext = (): void => {\n const job = jobs.shift() as StartJob\n if (nowMs() - job.queuedAt > maxLatencyMs) job.drop()\n else job.run()\n }\n\n const drain = (): void => {\n scheduled = false\n if (attached) return\n const began = nowMs()\n while (jobs.length > 0) {\n runNext()\n if (nowMs() - began >= budgetMs) break\n }\n if (jobs.length > 0) schedule()\n }\n\n const schedule = (): void => {\n if (scheduled || attached) return\n scheduled = true\n scheduleTask(drain)\n }\n\n const queue: OwnStartQueue = {\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 deferredQueue: () => deferredQueue,\n }\n\n const deferredQueue: DeferredQueue = {\n id: DEFERRED_QUEUE_ID.AUDIO_VOICES,\n pending: () => jobs.length,\n run(deadline, clock) {\n let ran = 0\n while (jobs.length > 0) {\n runNext()\n ran++\n if (clock() >= deadline) break\n }\n return ran\n },\n flush() {\n while (jobs.length > 0) runNext()\n },\n attach() {\n attached = true\n },\n detach() {\n attached = false\n if (jobs.length > 0) schedule()\n },\n }\n return queue\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 { PlannedAutomation, PlannedFilter, PlannedLayer, PlannedModulator, PlannedSource, SoundPlan } from './plan'\nimport type { SoundSet } from './recipe'\n\nconst tuneAutomation = (automation: PlannedAutomation, rate: number): PlannedAutomation => {\n const tuned: { -readonly [K in keyof PlannedAutomation]: PlannedAutomation[K] } = { ...automation }\n if (automation.value !== undefined) tuned.value = automation.value * rate\n if (automation.from !== undefined) tuned.from = automation.from * rate\n if (automation.to !== undefined) tuned.to = automation.to * rate\n if (automation.at !== undefined) tuned.at = automation.at / rate\n return tuned\n}\n\nconst tuneFilter = (filter: PlannedFilter, rate: number): PlannedFilter => ({ ...filter, frequency: tuneAutomation(filter.frequency, rate) })\n\nconst tuneSource = (source: PlannedSource, rate: number): PlannedSource => {\n switch (source.kind) {\n case 'noise':\n return source\n case 'osc':\n return { ...source, frequency: tuneAutomation(source.frequency, rate) }\n case 'formant_voice':\n return {\n ...source,\n frequency: tuneAutomation(source.frequency, rate),\n formants: source.formants.map((formant) => ({ ...formant, frequency: formant.frequency * rate })),\n breath: { ...source.breath, frequency: source.breath.frequency * rate },\n }\n }\n}\n\nconst tuneModulator = (modulator: PlannedModulator, rate: number): PlannedModulator => ({\n ...modulator,\n rate: modulator.rate * rate,\n ...(modulator.target === 'frequency' ? { depth: modulator.depth * rate } : {}),\n})\n\nconst tuneLayer = (layer: PlannedLayer, rate: number): PlannedLayer => ({\n ...layer,\n at: layer.at / rate,\n duration: layer.duration / rate,\n tail: layer.tail / rate,\n source: tuneSource(layer.source, rate),\n ...(layer.filters ? { filters: { ...layer.filters, bank: layer.filters.bank.map((filter) => tuneFilter(filter, rate)) } } : {}),\n modulators: layer.modulators.map((modulator) => tuneModulator(modulator, rate)),\n envelope: layer.envelope.map((point) => ({ ...point, time: point.time / rate })),\n})\n\n/**\n * A plan played faster or slower, as a tape would: every frequency (tones, filters, formants, LFOs) times `rate`, every time divided by it. Noise\n * has no pitch of its own, so its colour moves with its filters. `rate` 1 gives the plan back.\n */\nexport function retunePlan(plan: SoundPlan, rate: number): SoundPlan {\n if (rate === 1 || !(rate > 0)) return plan\n return { ...plan, length: plan.length / rate, layers: plan.layers.map((layer) => tuneLayer(layer, rate)) }\n}\n\n/** Remembers the last variant of each set, so the next draw never repeats it. */\nexport type SetPicker = {\n /** The variant to play and its rate, drawn from `random` (two numbers: the variant, then the rate). */\n pick(set: SoundSet, random: () => number): { readonly id: string; readonly rate: number }\n /** Forget the last variants (a fresh scene). */\n reset(): void\n}\n\nexport function createSetPicker(): SetPicker {\n const last = new Map<string, number>()\n return {\n pick(set, random) {\n const count = set.variants.length\n const previous = last.get(set.id)\n let index: number\n if (count === 1) index = 0\n else if (set.repeat || previous === undefined) index = Math.min(count - 1, Math.floor(random() * count))\n else {\n // one of the others: draw among count − 1 and skip the previous\n index = Math.min(count - 2, Math.floor(random() * (count - 1)))\n if (index >= previous) index += 1\n }\n last.set(set.id, index)\n const [min, max] = set.rate ?? [1, 1]\n const rate = min === max ? min : min + random() * (max - min)\n return { id: set.variants[index] as string, rate }\n },\n reset: () => last.clear(),\n }\n}\n","import type { DeferredQueue, 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'\nimport { createSetPicker, retunePlan } from '../variation'\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 /**\n * The voice start queue as a `DeferredQueue` for a shared frame budget (`budget.register` attaches it and the queue stops scheduling its own tasks), or `null`\n * without `deferredStart`. Asking for it changes nothing.\n */\n deferredQueue?(): DeferredQueue | null\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 // a set id (`SoundBank.sets`) plays one of its variants, never the last one, at a drawn rate; a recipe id plays itself at rate 1\n const picker = createSetPicker()\n const resolve = (id: string, random: () => number, chooser = picker): { readonly id: string; readonly rate: number } => {\n if (registry.sound(id)) return { id, rate: 1 }\n const set = registry.set(id)\n return set ? chooser.pick(set, random) : { id, rate: 1 }\n }\n\n const playWith = (asked: string, playOptions: PlayOptions, streamRandom: () => number, store: NoiseStore): VoiceHandle | null => {\n const seeded = playOptions.seed === undefined ? null : createRng(playOptions.seed)\n const { id, rate } = resolve(asked, seeded ?? streamRandom)\n const random = seeded ?? streamRandom\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, random))\n return plan ? start(retunePlan(plan, rate), 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 // pure: a set is drawn with a picker of its own, so planning never moves what `play` picks next\n const planWith = (asked: string, playOptions: PlayOptions, random: () => number): SoundPlan | null => {\n const { id, rate } = resolve(asked, random, createSetPicker())\n const recipe = registry.sound(id)\n const plan = recipe ? planSound(recipe, inputFor(recipe, playOptions, random)) : null\n return plan ? retunePlan(plan, rate) : 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 deferredQueue: () => startQueue?.deferredQueue() ?? null,\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","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","// 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","// 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 })\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","// The calls of the animals, as recipes of @adatechnology/game-audio: the game's `bichoSom` (jogo L66625), which synthesizes four calls with the same\n// routine (the dog's two filtered clicks, the lapwing's shriek, the hen's cluck, the cow's moo). Linear fall to silence at 70 m, as the page has it; the\n// numbers are drawn and the nodes created in the page's order, so a recipe leaves the trace of the team code (tests/audio.test.ts against\n// tools/golden/fixtures/audio/game-audio.json).\nimport { DISTANCE_CURVES } from '@adatechnology/game-audio'\nimport type { SoundBank, SoundRecipe } from '@adatechnology/game-audio'\n\ntype Call = {\n readonly id: string\n /** The name in the team code. */\n readonly team: string\n readonly count: number\n readonly spacing: number\n readonly seconds: number\n readonly wave: OscillatorType\n readonly frequency: number\n /** The pitch falls to this fraction of itself over each pass. */\n readonly drop: number\n}\n\nconst call = (o: Call): SoundRecipe => ({\n id: o.id,\n bus: 'world',\n order: 'calls',\n duration: o.seconds,\n tail: 0.02,\n // the page makes its gain and filter before it looks at the level\n keepSilent: true,\n layers: [\n {\n repeat: { count: o.count, spacing: o.spacing },\n source: { kind: 'osc', wave: o.wave, frequency: { from: o.frequency, to: o.frequency * o.drop, curve: 'exp', over: 'duration' } },\n filters: { mode: 'serial', bank: [{ type: 'bandpass', frequency: { set: o.frequency * 1.2 }, q: 3 }] },\n envelope: [\n [0, 0.0001, 'set', 'fixed'],\n [0.015, 1, 'exp'],\n ['1d', 0.0001, 'exp', 'fixed'],\n ],\n },\n ],\n falloff: DISTANCE_CURVES.animals,\n sources: [`jogo:L66625-66639 bichoSom('${o.team}')`],\n})\n\n/** `latido`: two square clicks 0.22 s apart, 520 Hz falling to 0.6, 0.12 s each. */\nexport const DOG_BARK = call({ id: 'dog_bark', team: 'latido', count: 2, spacing: 0.22, seconds: 0.12, wave: 'square', frequency: 520, drop: 0.6 })\n/** `queroquero`: three squares 0.14 s apart, 2400 Hz falling to 0.6, 0.09 s each. */\nexport const LAPWING_CALL = call({ id: 'lapwing_call', team: 'queroquero', count: 3, spacing: 0.14, seconds: 0.09, wave: 'square', frequency: 2400, drop: 0.6 })\n/** `cacarejo`: four squares 0.14 s apart, 900 Hz falling to 0.6, 0.09 s each. */\nexport const HEN_CLUCK = call({ id: 'hen_cluck', team: 'cacarejo', count: 4, spacing: 0.14, seconds: 0.09, wave: 'square', frequency: 900, drop: 0.6 })\n/** `mugido`: one sawtooth, 140 Hz falling to 0.8, 1.2 s. */\nexport const COW_MOO = call({ id: 'cow_moo', team: 'mugido', count: 1, spacing: 0.14, seconds: 1.2, wave: 'sawtooth', frequency: 140, drop: 0.8 })\n\nexport const ANIMAL_SOUNDS: readonly SoundRecipe[] = [DOG_BARK, LAPWING_CALL, HEN_CLUCK, COW_MOO]\n\nexport const ANIMAL_SOUND_BANK: SoundBank = { id: 'animals_three', sounds: ANIMAL_SOUNDS }\n","{\n \"sources\": [\n \"jogo:L66677 galinha: cacarejo, 6 s between calls, noise 30 m\",\n \"jogo:L66680 quero-quero: queroquero when the player is near (alarme), 8 s, noise 40 m\",\n \"jogo:L66681 vaca: mugido, 30 s, noise 80 m\",\n \"jogo:L66683 cachorro: latido when a zombie is within 12 m, 3 s, noise 60 m\"\n ],\n \"species\": {\n \"chicken\": { \"sound\": \"hen_cluck\", \"cooldown\": 6, \"noiseMetres\": 30 },\n \"southern_lapwing\": { \"sound\": \"lapwing_call\", \"cooldown\": 8, \"noiseMetres\": 40 },\n \"cow\": { \"sound\": \"cow_moo\", \"cooldown\": 30, \"noiseMetres\": 80 },\n \"dog\": { \"sound\": \"dog_bark\", \"cooldown\": 3, \"noiseMetres\": 60 }\n }\n}\n","import type { SoundRequest, Vec3 } from '@adatechnology/game-contracts'\n\nimport { ANIMAL_SOUND_BANK } from './recipes'\nimport callsData from './calls.json'\n\n/** The call of a species: the recipe, the seconds the page waits before the same animal calls again, and the metres its noise reaches the AI (`emitNoise`). */\nexport type AnimalCall = { readonly sound: string; readonly cooldown: number; readonly noiseMetres: number }\n\ntype CallsData = { readonly sources: readonly string[]; readonly species: Readonly<Record<string, AnimalCall>> }\n\nconst DATA = callsData as unknown as CallsData\n\n/** The calls the page delivered, by species id. The other species (horse, pig, boar, capybara, jaguar, cat, vulture, snake, rat) have none: the page gives them no sound. */\nexport const ANIMAL_CALLS: Readonly<Record<string, AnimalCall>> = DATA.species\nexport const ANIMAL_CALL_SOURCES: readonly string[] = DATA.sources\n\n/** The part of the host's sound an animal needs: `play`, and `register` to put the bank in its engine (`createAudioSink(...)` has both). */\nexport type AnimalSoundSink = {\n play(request: SoundRequest): unknown\n register?(bank: unknown): void\n}\n\nexport type AnimalSoundOptions = {\n /** Replaces a species' call; `null` silences it. */\n readonly calls?: Readonly<Record<string, AnimalCall | null>>\n}\n\nexport type AnimalSound = {\n /** The call of a species, if it has one. */\n callOf(speciesId: string): AnimalCall | undefined\n /**\n * Makes the species call where the animal is. `key` is the animal (default the species): the same animal waits out its cooldown, and the call is\n * refused (false) until then. Returns whether a sound was asked for.\n */\n call(speciesId: string, position: Vec3, options?: { readonly volume?: number; readonly key?: string }): boolean\n /** Counts the cooldowns down: call it each frame. */\n update(dt: number): void\n dispose(): void\n}\n\n/**\n * Plays the calls of the animals on the host's sound: the four of the Bichos do Vale (hen, lapwing, cow, dog), positional with the page's linear 70 m\n * fall, each animal waiting out the page's cooldown. The bank is registered in the host's engine.\n */\nexport function createAnimalSound(sink: AnimalSoundSink, options: AnimalSoundOptions = {}): AnimalSound {\n sink.register?.(ANIMAL_SOUND_BANK)\n const calls: Record<string, AnimalCall | null | undefined> = { ...ANIMAL_CALLS, ...options.calls }\n const waiting = new Map<string, number>()\n const callOf = (speciesId: string): AnimalCall | undefined => calls[speciesId] ?? undefined\n return {\n callOf,\n call(speciesId, position, o = {}) {\n const found = callOf(speciesId)\n if (!found) return false\n const key = o.key ?? speciesId\n if ((waiting.get(key) ?? 0) > 0) return false\n waiting.set(key, found.cooldown)\n sink.play({ sound: found.sound, position, ...(o.volume === undefined ? {} : { volume: o.volume }) })\n return true\n },\n update(dt) {\n for (const [key, seconds] of waiting) {\n if (seconds - dt <= 0) waiting.delete(key)\n else waiting.set(key, seconds - dt)\n }\n },\n dispose: () => waiting.clear(),\n }\n}\n","import type * as THREE_NS from 'three'\n\nimport type { AnimalRig, AnimalsThreeOptions, Three } from './types'\n\nexport type MaterialExtras = Partial<THREE_NS.MeshStandardMaterialParameters>\n\n/** The mulberry32 stream of the page (`rng`): the same numbers for the same seed. */\nexport const createStream = (seed: number): (() => number) => {\n let state = seed\n return () => {\n state |= 0\n state = (state + 0x6d2b79f5) | 0\n let t = Math.imul(state ^ (state >>> 15), 1 | state)\n t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n }\n}\n\n/** The page's `add`: a mesh at a position and rotation under a parent, casting and receiving shadows. */\nexport const addMesh = (\n THREE: Three,\n parent: THREE_NS.Object3D,\n geometry: THREE_NS.BufferGeometry,\n material: THREE_NS.Material,\n x?: number,\n y?: number,\n z?: number,\n rx?: number,\n ry?: number,\n rz?: number,\n): THREE_NS.Mesh => {\n const mesh = new THREE.Mesh(geometry, material)\n mesh.position.set(x || 0, y || 0, z || 0)\n if (rx || ry || rz) mesh.rotation.set(rx || 0, ry || 0, rz || 0)\n mesh.castShadow = true\n mesh.receiveShadow = true\n parent.add(mesh)\n return mesh\n}\n\n/** The part builders of the page's `Animal` (`M`, `ball`, `limb`, `joint`, `pivot`), bound to one rig. */\nexport type RigTools = {\n material(color: number, extras?: MaterialExtras): THREE_NS.MeshStandardMaterial\n ball(parent: THREE_NS.Object3D, radius: number, material: THREE_NS.Material, x?: number, y?: number, z?: number, sx?: number, sy?: number, sz?: number): THREE_NS.Mesh\n /** A cylinder hanging from the origin; `radius1` is the one the page calls the top, but the geometry takes it as the bottom (kept as the page has it). */\n limb(parent: THREE_NS.Object3D, radius1: number, radius2: number, length: number, material: THREE_NS.Material): THREE_NS.Mesh\n joint(parent: THREE_NS.Object3D, radius: number, material: THREE_NS.Material, x?: number, y?: number, z?: number): THREE_NS.Mesh\n pivot(parent: THREE_NS.Object3D, x: number, y: number, z: number, offset?: THREE_NS.Vector3): THREE_NS.Group\n eyes(head: THREE_NS.Object3D, radius: number, x: number, y: number, z: number): void\n}\n\nexport const createRigTools = (THREE: Three, rig: AnimalRig): RigTools => {\n const tools: RigTools = {\n material(color, extras) {\n const material = new THREE.MeshStandardMaterial(Object.assign({ color, roughness: 0.8, metalness: 0, flatShading: false }, extras || {}))\n material.userData.base = material.color.clone()\n rig.materials.push(material)\n return material\n },\n ball(parent, radius, material, x, y, z, sx, sy, sz) {\n const mesh = addMesh(THREE, parent, new THREE.SphereGeometry(radius, 16, 12), material, x, y, z)\n if (sx) mesh.scale.set(sx, sy as number, sz as number)\n return mesh\n },\n limb: (parent, radius1, radius2, length, material) => addMesh(THREE, parent, new THREE.CylinderGeometry(radius2, radius1, length, 14, 1).translate(0, -length / 2, 0), material),\n joint(parent, radius, material, x, y, z) {\n const joint = tools.ball(parent, radius, material, x, y, z)\n joint.userData.joint = 1\n rig.joints.push(joint)\n return joint\n },\n pivot(parent, x, y, z, offset) {\n const group = new THREE.Group()\n group.position.set(x, y, z)\n parent.add(group)\n rig.pivots.push({ group, rest: group.position.clone(), offset: offset || new THREE.Vector3() })\n return group\n },\n eyes(head, radius, x, y, z) {\n const eyeMaterial = tools.material(0x111111, { roughness: 0.25 })\n rig.eyeMaterial = eyeMaterial\n for (const side of [-1, 1]) rig.eyes.push(tools.ball(head, radius, eyeMaterial, side * x, y, z))\n },\n }\n return tools\n}\n\nexport const createRig = (THREE: Three, species: AnimalRig['species'], options: AnimalsThreeOptions & { phase: number }): AnimalRig => {\n const rig: AnimalRig = {\n species,\n root: new THREE.Group(),\n trunk: new THREE.Group(),\n materials: [],\n joints: [],\n pivots: [],\n legs: [],\n tail: [],\n ears: [],\n infectionParts: [],\n eyes: [],\n wings: [],\n segments: [],\n phase: options.phase,\n size: 1,\n segmentLength: 0,\n segmentCount: 0,\n home: new THREE.Vector3(),\n infected: false,\n dead: false,\n neckAngle: 0,\n headAngle: 0,\n triangles: 0,\n }\n rig.root.add(rig.trunk)\n return rig\n}\n","import type * as THREE_NS from 'three'\n\nimport { addMesh } from './context'\nimport type { RigTools } from './context'\nimport type { HeadSpecOf, NeckSpecOf } from './extras-types'\nimport type { AnimalRig, Three } from './types'\n\nexport type ExtraContext = { readonly THREE: Three; readonly rig: AnimalRig; readonly tools: RigTools }\n\nexport type QuadrupedParts = {\n readonly bodyGroup: THREE_NS.Group\n readonly head: THREE_NS.Group\n readonly neck: THREE_NS.Group\n readonly main: THREE_NS.MeshStandardMaterial\n readonly belly: THREE_NS.MeshStandardMaterial\n readonly dark: THREE_NS.MeshStandardMaterial\n readonly L: number\n readonly Rb: number\n readonly bodyY: number\n readonly hd: HeadSpecOf\n readonly nk: NeckSpecOf\n}\n\nexport type BirdParts = {\n readonly bodyGroup: THREE_NS.Group\n readonly head: THREE_NS.Group\n readonly neck: THREE_NS.Group\n readonly main: THREE_NS.MeshStandardMaterial\n readonly dark: THREE_NS.MeshStandardMaterial\n}\n\nexport type QuadrupedExtra = (context: ExtraContext, parts: QuadrupedParts) => void\nexport type BirdExtra = (context: ExtraContext, parts: BirdParts) => void\n\nconst cow: QuadrupedExtra = ({ THREE, tools }, k) => {\n tools.ball(k.bodyGroup, 0.2, k.main, 0, k.Rb * 1.1, k.L * 0.33, 0.95, 1.1, 1.1) // hump\n tools.ball(k.neck, 0.13, k.main, 0, 0.12, 0.08, 0.55, 1.4, 1.05) // dewlap\n for (const side of [-1, 1]) addMesh(THREE, k.head, new THREE.ConeGeometry(0.025, 0.13, 8), tools.material(0xcfc6a8), side * 0.12, k.hd.r * 1.05, k.hd.r * 0.05, 0, 0, side * -0.9)\n}\n\nconst horse: QuadrupedExtra = ({ THREE, rig, tools }, k) => {\n for (let index = 0; index < 9; index++) addMesh(THREE, k.neck, new THREE.BoxGeometry(0.035, 0.16, 0.08), k.dark, 0, 0.06 + index * 0.065, -0.12 + index * 0.004, -0.25, 0, 0) // mane\n addMesh(THREE, k.head, new THREE.BoxGeometry(0.03, 0.12, 0.06), k.dark, 0, k.hd.r * 1.05, k.hd.r * 0.3, 0.6, 0, 0) // forelock\n const last = rig.tail[rig.tail.length - 1] as THREE_NS.Group\n tools.ball(last, 0.08, k.dark, 0, -0.42, 0, 0.7, 4.5, 0.7) // tail hair\n}\n\nconst wildBoar: QuadrupedExtra = ({ THREE, tools }, k) => {\n for (let index = 0; index < 12; index++) addMesh(THREE, k.bodyGroup, new THREE.BoxGeometry(0.03, 0.1, 0.06), k.dark, 0, k.Rb * 1.12 - index * 0.004, k.L * 0.38 - index * 0.06, -0.2, 0, 0) // bristles\n for (const side of [-1, 1])\n addMesh(THREE, k.head, new THREE.ConeGeometry(0.012, 0.09, 6), tools.material(0xe8e0cc), side * 0.07, k.hd.r * -0.25 + 0.03, k.hd.r * 0.25 + k.hd.r * k.hd.s[2] * 0.55 + 0.13, -0.6, 0, side * 0.5) // tusks\n}\n\nconst chicken: BirdExtra = ({ tools }, k) => {\n const red = tools.material(0xc0281e)\n for (let index = 0; index < 4; index++) tools.ball(k.head, 0.014, red, 0, 0.045, -0.012 + index * 0.014, 0.5, 1, 0.8) // comb\n tools.ball(k.head, 0.012, red, 0, -0.04, 0.04, 0.5, 1.3, 0.7) // wattle\n}\n\nconst southernLapwing: BirdExtra = ({ THREE, tools }, k) => {\n const black = tools.material(0x161616)\n tools.ball(k.bodyGroup, 0.05, black, 0, 0.03, 0.08, 1.1, 0.9, 0.6) // breast patch\n addMesh(THREE, k.head, new THREE.BoxGeometry(0.006, 0.006, 0.06).translate(0, 0, -0.03), black, 0, 0.025, -0.01, 0.5, 0, 0) // crest\n tools.ball(k.head, 0.026, black, 0, -0.005, 0.02, 1.05, 0.7, 0.9) // cap\n}\n\n/** The parts the page builds with code for five species (the page's `extra` closures), by species id. */\nexport const QUADRUPED_EXTRAS: Readonly<Record<string, QuadrupedExtra>> = { cow, horse, wild_boar: wildBoar }\nexport const BIRD_EXTRAS: Readonly<Record<string, BirdExtra>> = { chicken, southern_lapwing: southernLapwing }\n","{\n \"walk\": [\n 1.570796327,\n 4.71238898,\n 0,\n 3.141592654\n ],\n \"trot\": [\n 0,\n 3.141592654,\n 3.141592654,\n 0\n ],\n \"gallop\": [\n 2.890265241,\n 3.707079331,\n 0,\n 0.879645943\n ]\n}\n","import gaitsData from './data/gaits.json'\nimport type { AnimalGaitName, GaitPhases } from './types'\n\n/** The phase of each leg per gait, in radians, from the Bichos do Vale page (`GAIT`); the leg order is front right, front left, hind right, hind left. */\nexport const GAIT_PHASES = gaitsData as unknown as Readonly<Record<AnimalGaitName, GaitPhases>>\n","import type { BirdSpec, QuadrupedSpec } from './types'\n\nexport type QuadrupedLayout = {\n /** Height of the front and the hind hips above the ground. */\n readonly frontHipHeight: number\n readonly hindHipHeight: number\n readonly footHeight: number\n /** Height of the body centre. */\n readonly bodyHeight: number\n /** Distance between the shoulders and the hips. */\n readonly span: number\n /** Largest dimension, used to scale the offsets of the split view and the blood pool. */\n readonly size: number\n}\n\n/** The rest-pose geometry of a quadruped: leg lengths and angles decide how high the body stands. */\nexport const quadrupedLayout = (spec: QuadrupedSpec): QuadrupedLayout => {\n const leg = spec.leg\n const frontAngle1 = leg.fa1 ?? 0.06\n const frontAngle2 = leg.fa2 ?? -0.08\n const hindAngle1 = leg.ha1 ?? -0.32\n const hindAngle2 = leg.ha2 ?? 0.58\n const footHeight = leg.hoof ? leg.r * 1.1 : leg.r * 0.7\n const frontHipHeight = leg.fu * Math.cos(frontAngle1) + leg.fl * Math.cos(frontAngle1 + frontAngle2) + footHeight\n const hindHipHeight = leg.hu * Math.cos(hindAngle1) + leg.hl * Math.cos(hindAngle1 + hindAngle2) + footHeight\n const bodyHeight = (frontHipHeight + hindHipHeight) / 2 + (spec.up ?? 0) * spec.R\n return { frontHipHeight, hindHipHeight, footHeight, bodyHeight, span: spec.L * spec.span, size: Math.max(spec.L, bodyHeight + spec.R) }\n}\n\n/** The size of a bird: the largest of its body and its standing height. */\nexport const birdSize = (spec: BirdSpec): number => Math.max(spec.r * 3, spec.tars + spec.shank * 0.9 + spec.r * 2)\n\n/** The length a species takes in a line-up: used to space the animals apart. */\nexport const lineUpLength = (body: QuadrupedSpec | BirdSpec | { readonly kind: 'snake' }): number => {\n if (body.kind === 'quadruped') return body.L * 1.3\n if (body.kind === 'snake') return 0.9\n return body.r * 4\n}\n","type Entry = { readonly id: string }\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 shallow merge); a patch for an id that does not exist is ignored. */\nexport const applyPatches = <TEntry extends Entry>(entries: Map<string, TEntry>, patches: Readonly<Record<string, 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, { ...entry, ...patch, id })\n }\n}\n\nexport const removeIds = (entries: Map<string, unknown>, ids: readonly string[] | undefined): void => {\n for (const id of ids ?? []) entries.delete(id)\n}\n","import { GAIT_PHASES } from './gaits'\nimport { applyPatches, mergeById, removeIds } from './registry'\nimport type { AnimalGaitName, AnimalOutcome, AnimalSpecies, AnimalVisualState, AnimalsOptions, GaitPhases } from './types'\n\nexport type AnimalKit = {\n readonly species: readonly AnimalSpecies[]\n get(id: string): AnimalSpecies | undefined\n gait(name: AnimalGaitName): GaitPhases\n}\n\n/**\n * What a bite does to the look of an animal: a species that turns shows the infected look, one that dies shows the dead look, and an immune one shows no\n * change. `forceDead` shows the dead look for any species (a carcass, an animal killed by something else).\n */\nexport const visualStateOf = (outcome: AnimalOutcome, isBitten: boolean, forceDead = false): AnimalVisualState => {\n if (forceDead) return 'dead'\n if (!isBitten) return 'alive'\n if (outcome === 'turns') return 'infected'\n return outcome === 'dies' ? 'dead' : 'alive'\n}\n\nexport const createAnimals = (options: AnimalsOptions = {}): AnimalKit => {\n const entries = mergeById([], options.packs?.flatMap((pack) => pack.species ?? []))\n applyPatches(entries, options.patch)\n removeIds(entries, options.remove)\n const species = [...entries.values()].sort((first, second) => first.order - second.order)\n const gaits = { ...GAIT_PHASES, ...options.gaits }\n return { species, get: (id) => entries.get(id), gait: (name) => gaits[name] }\n}\n\n","import { lineUpLength } from '@adatechnology/game-animals'\n\nimport type { AnimalRig } from './types'\n\nexport type LineUpOptions = {\n /** X where the row starts (page: -19). */\n readonly start?: number\n /** Gap between animals (page: 0.85). */\n readonly gap?: number\n /** Heading of the animals, radians (page: 1.1; snakes 2.2). */\n readonly yaw?: number\n readonly snakeYaw?: number\n}\n\n/** Puts the animals in a row on the x axis, each given room by its length, the row centred on the origin, and keeps each place as `home` (page: the line-up). */\nexport const lineUpAnimals = (rigs: readonly AnimalRig[], options: LineUpOptions = {}): void => {\n const start = options.start ?? -19\n const gap = options.gap ?? 0.85\n let cursor = start\n for (const rig of rigs) {\n const length = lineUpLength(rig.species.body)\n cursor += length / 2\n rig.root.position.set(cursor, 0, 0)\n rig.home = rig.root.position.clone()\n rig.root.rotation.y = rig.species.body.kind === 'snake' ? (options.snakeYaw ?? 2.2) : (options.yaw ?? 1.1)\n cursor += length / 2 + gap\n }\n const offset = -(cursor - gap + start) / 2\n for (const rig of rigs) {\n rig.root.position.x += offset\n rig.home.x += offset\n }\n}\n","import type { Color } from './types'\n\n/** A colour as `0xRRGGBB`: numbers as given (floored, as three's `setHex`), `'#rrggbb'` strings parsed. */\nexport function colorHex(color: Color | undefined, fallback = 0xffffff): number {\n if (color === undefined) return fallback\n if (typeof color === 'number') return Math.floor(color)\n const match = /^#([0-9a-fA-F]{6})$/.exec(color)\n if (!match) throw new Error(`game-shapes: colour \"${color}\" is not 0xRRGGBB or #rrggbb`)\n return parseInt(match[1] as string, 16)\n}\n\n/** three's `SRGBToLinear` (r160 `ColorManagement`), the same constants. */\nexport const srgbToLinear = (c: number): number => (c < 0.04045 ? c * 0.0773993808 : Math.pow(c * 0.9478672986 + 0.0521327014, 2.4))\n\n/**\n * The rgb floats of a hex colour as three's `Color.setHex` stores them: the channels over 255 (`'srgb'`), converted to the linear working space\n * when colour management is on (`'linear'`).\n */\nexport function colorChannels(hex: number, space: 'linear' | 'srgb'): [number, number, number] {\n const r = ((hex >> 16) & 255) / 255\n const g = ((hex >> 8) & 255) / 255\n const b = (hex & 255) / 255\n return space === 'linear' ? [srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)] : [r, g, b]\n}\n","/**\n * Canonical JSON (keys sorted, `undefined` left out, `-0` kept as `-0`, typed arrays as plain lists) and a 64-bit FNV-1a of it in two 32-bit\n * lanes: the key a shape is cached and shared by.\n */\nexport function canonicalJson(value: unknown): string {\n if (value === null) return 'null'\n if (typeof value === 'number') return Object.is(value, -0) ? '-0' : Number.isFinite(value) ? String(value) : JSON.stringify(String(value))\n if (typeof value === 'string') return JSON.stringify(value)\n if (typeof value === 'boolean') return value ? 'true' : 'false'\n if (typeof value === 'function') throw new Error('game-shapes: a function cannot be part of a shape (register it as a generator)')\n if (ArrayBuffer.isView(value)) return canonicalJson(Array.from(value as unknown as ArrayLike<number>))\n if (Array.isArray(value)) return `[${value.map((item) => (item === undefined ? 'null' : canonicalJson(item))).join(',')}]`\n if (typeof value === 'object') {\n const record = value as Record<string, unknown>\n const keys = Object.keys(record).filter((key) => record[key] !== undefined).sort()\n return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`\n }\n return 'null'\n}\n\nconst lane = (text: string, offset: number, prime: number): string => {\n let hash = offset\n for (let index = 0; index < text.length; index++) {\n hash ^= text.charCodeAt(index)\n hash = Math.imul(hash, prime)\n }\n return (hash >>> 0).toString(16).padStart(8, '0')\n}\n\n/** 64-bit FNV-1a of a text as 16 hex digits (two independent 32-bit lanes). */\nexport const hash64 = (text: string): string => lane(text, 0x811c9dc5, 0x01000193) + lane(text, 0x050c5d1f, 0x5bd1e995)\n\n/** `hash64(canonicalJson(value))`. */\nexport const hashValue = (value: unknown): string => hash64(canonicalJson(value))\n","/*\n * Matrix and quaternion arithmetic as three.js r160 does it (MIT, https://github.com/mrdoob/three.js): the same expressions in the same order, so a\n * vertex baked here has the same bits as one baked by `Matrix4.compose` / `multiplyMatrices` / `Matrix3.getNormalMatrix` / `Vector3.normalize`.\n * Matrices are column-major arrays of 16 (4 x 4) or 9 (3 x 3) numbers, the layout of three's `elements`.\n */\nimport type { Mat4, RotationOrder, Transform, Vec3 } from './types'\n\nexport type Matrix4 = number[]\nexport type Matrix3 = number[]\n\nexport const identity4 = (): Matrix4 => [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]\n\n/** `Quaternion.setFromEuler` (x, y, z, w). */\nexport function quaternionFromEuler(x: number, y: number, z: number, order: RotationOrder = 'XYZ'): [number, number, number, number] {\n const cos = Math.cos\n const sin = Math.sin\n const c1 = cos(x / 2)\n const c2 = cos(y / 2)\n const c3 = cos(z / 2)\n const s1 = sin(x / 2)\n const s2 = sin(y / 2)\n const s3 = sin(z / 2)\n switch (order) {\n case 'XYZ':\n return [s1 * c2 * c3 + c1 * s2 * s3, c1 * s2 * c3 - s1 * c2 * s3, c1 * c2 * s3 + s1 * s2 * c3, c1 * c2 * c3 - s1 * s2 * s3]\n case 'YXZ':\n return [s1 * c2 * c3 + c1 * s2 * s3, c1 * s2 * c3 - s1 * c2 * s3, c1 * c2 * s3 - s1 * s2 * c3, c1 * c2 * c3 + s1 * s2 * s3]\n case 'ZXY':\n return [s1 * c2 * c3 - c1 * s2 * s3, c1 * s2 * c3 + s1 * c2 * s3, c1 * c2 * s3 + s1 * s2 * c3, c1 * c2 * c3 - s1 * s2 * s3]\n case 'ZYX':\n return [s1 * c2 * c3 - c1 * s2 * s3, c1 * s2 * c3 + s1 * c2 * s3, c1 * c2 * s3 - s1 * s2 * c3, c1 * c2 * c3 + s1 * s2 * s3]\n case 'YZX':\n return [s1 * c2 * c3 + c1 * s2 * s3, c1 * s2 * c3 + s1 * c2 * s3, c1 * c2 * s3 - s1 * s2 * c3, c1 * c2 * c3 - s1 * s2 * s3]\n case 'XZY':\n return [s1 * c2 * c3 - c1 * s2 * s3, c1 * s2 * c3 - s1 * c2 * s3, c1 * c2 * s3 + s1 * s2 * c3, c1 * c2 * c3 + s1 * s2 * s3]\n default:\n throw new Error(`game-shapes: unknown rotation order \"${String(order)}\"`)\n }\n}\n\n/** `Matrix4.compose(position, quaternion, scale)`. */\nexport function compose(px: number, py: number, pz: number, q: readonly [number, number, number, number], sx: number, sy: number, sz: number): Matrix4 {\n const [x, y, z, w] = q\n const x2 = x + x\n const y2 = y + y\n const z2 = z + z\n const xx = x * x2\n const xy = x * y2\n const xz = x * z2\n const yy = y * y2\n const yz = y * z2\n const zz = z * z2\n const wx = w * x2\n const wy = w * y2\n const wz = w * z2\n return [\n (1 - (yy + zz)) * sx, (xy + wz) * sx, (xz - wy) * sx, 0,\n (xy - wz) * sy, (1 - (xx + zz)) * sy, (yz + wx) * sy, 0,\n (xz + wy) * sz, (yz - wx) * sz, (1 - (xx + yy)) * sz, 0,\n px, py, pz, 1,\n ]\n}\n\n/** `Matrix4.makeRotationY(theta)` followed by `setPosition(x, y, z)`. */\nexport function headingMatrix(theta: number, x = 0, y = 0, z = 0): Matrix4 {\n const c = Math.cos(theta)\n const s = Math.sin(theta)\n return [c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, x, y, z, 1]\n}\n\n/** `Matrix4.multiplyMatrices(a, b)`: a x b. */\nexport function multiply(ae: readonly number[], be: readonly number[]): Matrix4 {\n const a11 = ae[0]!, a12 = ae[4]!, a13 = ae[8]!, a14 = ae[12]!\n const a21 = ae[1]!, a22 = ae[5]!, a23 = ae[9]!, a24 = ae[13]!\n const a31 = ae[2]!, a32 = ae[6]!, a33 = ae[10]!, a34 = ae[14]!\n const a41 = ae[3]!, a42 = ae[7]!, a43 = ae[11]!, a44 = ae[15]!\n const b11 = be[0]!, b12 = be[4]!, b13 = be[8]!, b14 = be[12]!\n const b21 = be[1]!, b22 = be[5]!, b23 = be[9]!, b24 = be[13]!\n const b31 = be[2]!, b32 = be[6]!, b33 = be[10]!, b34 = be[14]!\n const b41 = be[3]!, b42 = be[7]!, b43 = be[11]!, b44 = be[15]!\n return [\n a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41,\n a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41,\n a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41,\n a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41,\n a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42,\n a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42,\n a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42,\n a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42,\n a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43,\n a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43,\n a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43,\n a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43,\n a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44,\n a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44,\n a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44,\n a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44,\n ]\n}\n\n/** `Matrix3.getNormalMatrix(m)`: the inverse transpose of the upper 3 x 3 (all zeros when singular, as three). */\nexport function normalMatrix(me: readonly number[]): Matrix3 {\n // setFromMatrix4\n const n11 = me[0]!, n21 = me[1]!, n31 = me[2]!\n const n12 = me[4]!, n22 = me[5]!, n32 = me[6]!\n const n13 = me[8]!, n23 = me[9]!, n33 = me[10]!\n // invert\n const t11 = n33 * n22 - n32 * n23\n const t12 = n32 * n13 - n33 * n12\n const t13 = n23 * n12 - n22 * n13\n const det = n11 * t11 + n21 * t12 + n31 * t13\n if (det === 0) return [0, 0, 0, 0, 0, 0, 0, 0, 0]\n const detInv = 1 / det\n const te = [\n t11 * detInv,\n (n31 * n23 - n33 * n21) * detInv,\n (n32 * n21 - n31 * n22) * detInv,\n t12 * detInv,\n (n33 * n11 - n31 * n13) * detInv,\n (n31 * n12 - n32 * n11) * detInv,\n t13 * detInv,\n (n21 * n13 - n23 * n11) * detInv,\n (n22 * n11 - n21 * n12) * detInv,\n ]\n // transpose\n let tmp = te[1]!\n te[1] = te[3]!\n te[3] = tmp\n tmp = te[2]!\n te[2] = te[6]!\n te[6] = tmp\n tmp = te[5]!\n te[5] = te[7]!\n te[7] = tmp\n return te\n}\n\n/** True when the last row is (0, 0, 0, 1): `applyMatrix4`'s divide by w is then a multiply by exactly 1. */\nexport const isAffine = (e: readonly number[]): boolean => e[3] === 0 && e[7] === 0 && e[11] === 0 && e[15] === 1\n\n/** `Vector3.applyMatrix4`. */\nexport function transformPoint(e: readonly number[], x: number, y: number, z: number): [number, number, number] {\n const w = 1 / (e[3]! * x + e[7]! * y + e[11]! * z + e[15]!)\n return [(e[0]! * x + e[4]! * y + e[8]! * z + e[12]!) * w, (e[1]! * x + e[5]! * y + e[9]! * z + e[13]!) * w, (e[2]! * x + e[6]! * y + e[10]! * z + e[14]!) * w]\n}\n\nconst toVec3 = (scale: number | Vec3 | undefined): Vec3 => (scale === undefined ? [1, 1, 1] : typeof scale === 'number' ? [scale, scale, scale] : scale)\n\n/** True when the transform has no field set. */\nexport const isIdentityTransform = (t: Transform): boolean =>\n t.matrix === undefined && t.heading === undefined && t.position === undefined && t.rotation === undefined && t.scale === undefined\n\n/** The local matrix of a transform, or `null` when it has none. */\nexport function matrixOf(t: Transform): Matrix4 | null {\n if (t.matrix !== undefined) return [...t.matrix]\n if (t.heading !== undefined) {\n const p = t.position ?? [0, 0, 0]\n return headingMatrix(t.heading, p[0], p[1], p[2])\n }\n if (t.position === undefined && t.rotation === undefined && t.scale === undefined) return null\n const p = t.position ?? [0, 0, 0]\n const r = t.rotation ?? [0, 0, 0]\n const s = toVec3(t.scale)\n return compose(p[0], p[1], p[2], quaternionFromEuler(r[0], r[1], r[2], t.rotationOrder ?? 'XYZ'), s[0], s[1], s[2])\n}\n\n/** The matrix as given, as a plain array (for `ShapePart.matrix`). */\nexport const asMat4 = (m: Matrix4 | null): Mat4 => (m ? [...m] : identity4())\n","/*\n * The geometry generators of three.js r160 (MIT, https://github.com/mrdoob/three.js, src/geometries/*.js), ported line by line: the same loops,\n * the same expressions in the same order, numbers pushed into plain arrays and stored as Float32 at the end, exactly as `Float32BufferAttribute`\n * does. A test compares every array of every generator with three's, bit for bit, over a grid of parameters.\n */\nimport type { GeometryGroup, PrimitiveBuffers } from '../types'\n\nexport interface Arrays {\n vertices: number[]\n normals: number[]\n uvs: number[]\n indices: number[] | null\n groups: GeometryGroup[]\n}\n\n/** three's `arrayNeedsUint32`: 32-bit indices once any index reaches 65535. */\nexport function needsUint32(array: ArrayLike<number>): boolean {\n for (let i = array.length - 1; i >= 0; --i) if ((array[i] as number) >= 65535) return true\n return false\n}\n\nexport const indexArray = (indices: ArrayLike<number>): Uint16Array | Uint32Array => (needsUint32(indices) ? new Uint32Array(indices) : new Uint16Array(indices))\n\nexport function pack(type: string, parameters: Record<string, unknown>, a: Arrays): PrimitiveBuffers {\n return {\n type,\n parameters,\n positions: new Float32Array(a.vertices),\n normals: new Float32Array(a.normals),\n uvs: new Float32Array(a.uvs),\n ...(a.indices ? { indices: indexArray(a.indices) } : {}),\n groups: a.groups,\n }\n}\n\nconst normalize3 = (x: number, y: number, z: number): [number, number, number] => {\n const length = Math.sqrt(x * x + y * y + z * z)\n const s = 1 / (length || 1)\n return [x * s, y * s, z * s]\n}\n\nexport function box(width = 1, height = 1, depth = 1, widthSegments = 1, heightSegments = 1, depthSegments = 1): PrimitiveBuffers {\n const parameters = { width, height, depth, widthSegments, heightSegments, depthSegments }\n widthSegments = Math.floor(widthSegments)\n heightSegments = Math.floor(heightSegments)\n depthSegments = Math.floor(depthSegments)\n const indices: number[] = []\n const vertices: number[] = []\n const normals: number[] = []\n const uvs: number[] = []\n const groups: GeometryGroup[] = []\n let numberOfVertices = 0\n let groupStart = 0\n type Axis = 'x' | 'y' | 'z'\n const buildPlane = (u: Axis, v: Axis, w: Axis, udir: number, vdir: number, width: number, height: number, depth: number, gridX: number, gridY: number, materialIndex: number): void => {\n const segmentWidth = width / gridX\n const segmentHeight = height / gridY\n const widthHalf = width / 2\n const heightHalf = height / 2\n const depthHalf = depth / 2\n const gridX1 = gridX + 1\n const gridY1 = gridY + 1\n let vertexCounter = 0\n let groupCount = 0\n const vector = { x: 0, y: 0, z: 0 }\n for (let iy = 0; iy < gridY1; iy++) {\n const y = iy * segmentHeight - heightHalf\n for (let ix = 0; ix < gridX1; ix++) {\n const x = ix * segmentWidth - widthHalf\n vector[u] = x * udir\n vector[v] = y * vdir\n vector[w] = depthHalf\n vertices.push(vector.x, vector.y, vector.z)\n vector[u] = 0\n vector[v] = 0\n vector[w] = depth > 0 ? 1 : -1\n normals.push(vector.x, vector.y, vector.z)\n uvs.push(ix / gridX)\n uvs.push(1 - iy / gridY)\n vertexCounter += 1\n }\n }\n for (let iy = 0; iy < gridY; iy++) {\n for (let ix = 0; ix < gridX; ix++) {\n const a = numberOfVertices + ix + gridX1 * iy\n const b = numberOfVertices + ix + gridX1 * (iy + 1)\n const c = numberOfVertices + (ix + 1) + gridX1 * (iy + 1)\n const d = numberOfVertices + (ix + 1) + gridX1 * iy\n indices.push(a, b, d)\n indices.push(b, c, d)\n groupCount += 6\n }\n }\n groups.push({ start: groupStart, count: groupCount, materialIndex })\n groupStart += groupCount\n numberOfVertices += vertexCounter\n }\n buildPlane('z', 'y', 'x', -1, -1, depth, height, width, depthSegments, heightSegments, 0)\n buildPlane('z', 'y', 'x', 1, -1, depth, height, -width, depthSegments, heightSegments, 1)\n buildPlane('x', 'z', 'y', 1, 1, width, depth, height, widthSegments, depthSegments, 2)\n buildPlane('x', 'z', 'y', 1, -1, width, depth, -height, widthSegments, depthSegments, 3)\n buildPlane('x', 'y', 'z', 1, -1, width, height, depth, widthSegments, heightSegments, 4)\n buildPlane('x', 'y', 'z', -1, -1, width, height, -depth, widthSegments, heightSegments, 5)\n return pack('BoxGeometry', parameters, { vertices, normals, uvs, indices, groups })\n}\n\nfunction cylinderArrays(radiusTop: number, radiusBottom: number, height: number, radialSegments: number, heightSegments: number, openEnded: boolean, thetaStart: number, thetaLength: number): Arrays {\n radialSegments = Math.floor(radialSegments)\n heightSegments = Math.floor(heightSegments)\n const indices: number[] = []\n const vertices: number[] = []\n const normals: number[] = []\n const uvs: number[] = []\n const groups: GeometryGroup[] = []\n let index = 0\n const indexArray: number[][] = []\n const halfHeight = height / 2\n let groupStart = 0\n\n const generateTorso = (): void => {\n let groupCount = 0\n const slope = (radiusBottom - radiusTop) / height\n for (let y = 0; y <= heightSegments; y++) {\n const indexRow: number[] = []\n const v = y / heightSegments\n const radius = v * (radiusBottom - radiusTop) + radiusTop\n for (let x = 0; x <= radialSegments; x++) {\n const u = x / radialSegments\n const theta = u * thetaLength + thetaStart\n const sinTheta = Math.sin(theta)\n const cosTheta = Math.cos(theta)\n vertices.push(radius * sinTheta, -v * height + halfHeight, radius * cosTheta)\n const n = normalize3(sinTheta, slope, cosTheta)\n normals.push(n[0], n[1], n[2])\n uvs.push(u, 1 - v)\n indexRow.push(index++)\n }\n indexArray.push(indexRow)\n }\n for (let x = 0; x < radialSegments; x++) {\n for (let y = 0; y < heightSegments; y++) {\n const a = indexArray[y]![x]!\n const b = indexArray[y + 1]![x]!\n const c = indexArray[y + 1]![x + 1]!\n const d = indexArray[y]![x + 1]!\n indices.push(a, b, d)\n indices.push(b, c, d)\n groupCount += 6\n }\n }\n groups.push({ start: groupStart, count: groupCount, materialIndex: 0 })\n groupStart += groupCount\n }\n\n const generateCap = (top: boolean): void => {\n const centerIndexStart = index\n let groupCount = 0\n const radius = top === true ? radiusTop : radiusBottom\n const sign = top === true ? 1 : -1\n for (let x = 1; x <= radialSegments; x++) {\n vertices.push(0, halfHeight * sign, 0)\n normals.push(0, sign, 0)\n uvs.push(0.5, 0.5)\n index++\n }\n const centerIndexEnd = index\n for (let x = 0; x <= radialSegments; x++) {\n const u = x / radialSegments\n const theta = u * thetaLength + thetaStart\n const cosTheta = Math.cos(theta)\n const sinTheta = Math.sin(theta)\n vertices.push(radius * sinTheta, halfHeight * sign, radius * cosTheta)\n normals.push(0, sign, 0)\n uvs.push(cosTheta * 0.5 + 0.5, sinTheta * 0.5 * sign + 0.5)\n index++\n }\n for (let x = 0; x < radialSegments; x++) {\n const c = centerIndexStart + x\n const i = centerIndexEnd + x\n if (top === true) indices.push(i, i + 1, c)\n else indices.push(i + 1, i, c)\n groupCount += 3\n }\n groups.push({ start: groupStart, count: groupCount, materialIndex: top === true ? 1 : 2 })\n groupStart += groupCount\n }\n\n generateTorso()\n if (openEnded === false) {\n if (radiusTop > 0) generateCap(true)\n if (radiusBottom > 0) generateCap(false)\n }\n return { vertices, normals, uvs, indices, groups }\n}\n\nexport function cylinder(radiusTop = 1, radiusBottom = 1, height = 1, radialSegments = 32, heightSegments = 1, openEnded = false, thetaStart = 0, thetaLength = Math.PI * 2): PrimitiveBuffers {\n const parameters = { radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength }\n return pack('CylinderGeometry', parameters, cylinderArrays(radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength))\n}\n\nexport function cone(radius = 1, height = 1, radialSegments = 32, heightSegments = 1, openEnded = false, thetaStart = 0, thetaLength = Math.PI * 2): PrimitiveBuffers {\n const parameters = { radius, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength }\n return pack('ConeGeometry', parameters, cylinderArrays(0, radius, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength))\n}\n\nexport function sphere(radius = 1, widthSegments = 32, heightSegments = 16, phiStart = 0, phiLength = Math.PI * 2, thetaStart = 0, thetaLength = Math.PI): PrimitiveBuffers {\n const parameters = { radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength }\n widthSegments = Math.max(3, Math.floor(widthSegments))\n heightSegments = Math.max(2, Math.floor(heightSegments))\n const thetaEnd = Math.min(thetaStart + thetaLength, Math.PI)\n let index = 0\n const grid: number[][] = []\n const indices: number[] = []\n const vertices: number[] = []\n const normals: number[] = []\n const uvs: number[] = []\n for (let iy = 0; iy <= heightSegments; iy++) {\n const verticesRow: number[] = []\n const v = iy / heightSegments\n let uOffset = 0\n if (iy === 0 && thetaStart === 0) uOffset = 0.5 / widthSegments\n else if (iy === heightSegments && thetaEnd === Math.PI) uOffset = -0.5 / widthSegments\n for (let ix = 0; ix <= widthSegments; ix++) {\n const u = ix / widthSegments\n const x = -radius * Math.cos(phiStart + u * phiLength) * Math.sin(thetaStart + v * thetaLength)\n const y = radius * Math.cos(thetaStart + v * thetaLength)\n const z = radius * Math.sin(phiStart + u * phiLength) * Math.sin(thetaStart + v * thetaLength)\n vertices.push(x, y, z)\n const n = normalize3(x, y, z)\n normals.push(n[0], n[1], n[2])\n uvs.push(u + uOffset, 1 - v)\n verticesRow.push(index++)\n }\n grid.push(verticesRow)\n }\n for (let iy = 0; iy < heightSegments; iy++) {\n for (let ix = 0; ix < widthSegments; ix++) {\n const a = grid[iy]![ix + 1]!\n const b = grid[iy]![ix]!\n const c = grid[iy + 1]![ix]!\n const d = grid[iy + 1]![ix + 1]!\n if (iy !== 0 || thetaStart > 0) indices.push(a, b, d)\n if (iy !== heightSegments - 1 || thetaEnd < Math.PI) indices.push(b, c, d)\n }\n }\n return pack('SphereGeometry', parameters, { vertices, normals, uvs, indices, groups: [] })\n}\n\nexport function plane(width = 1, height = 1, widthSegments = 1, heightSegments = 1): PrimitiveBuffers {\n const parameters = { width, height, widthSegments, heightSegments }\n const widthHalf = width / 2\n const heightHalf = height / 2\n const gridX = Math.floor(widthSegments)\n const gridY = Math.floor(heightSegments)\n const gridX1 = gridX + 1\n const gridY1 = gridY + 1\n const segmentWidth = width / gridX\n const segmentHeight = height / gridY\n const indices: number[] = []\n const vertices: number[] = []\n const normals: number[] = []\n const uvs: number[] = []\n for (let iy = 0; iy < gridY1; iy++) {\n const y = iy * segmentHeight - heightHalf\n for (let ix = 0; ix < gridX1; ix++) {\n const x = ix * segmentWidth - widthHalf\n vertices.push(x, -y, 0)\n normals.push(0, 0, 1)\n uvs.push(ix / gridX)\n uvs.push(1 - iy / gridY)\n }\n }\n for (let iy = 0; iy < gridY; iy++) {\n for (let ix = 0; ix < gridX; ix++) {\n const a = ix + gridX1 * iy\n const b = ix + gridX1 * (iy + 1)\n const c = ix + 1 + gridX1 * (iy + 1)\n const d = ix + 1 + gridX1 * iy\n indices.push(a, b, d)\n indices.push(b, c, d)\n }\n }\n return pack('PlaneGeometry', parameters, { vertices, normals, uvs, indices, groups: [] })\n}\n\nexport function circle(radius = 1, segments = 32, thetaStart = 0, thetaLength = Math.PI * 2): PrimitiveBuffers {\n const parameters = { radius, segments, thetaStart, thetaLength }\n segments = Math.max(3, segments)\n const indices: number[] = []\n const vertices: number[] = []\n const normals: number[] = []\n const uvs: number[] = []\n vertices.push(0, 0, 0)\n normals.push(0, 0, 1)\n uvs.push(0.5, 0.5)\n for (let s = 0, i = 3; s <= segments; s++, i += 3) {\n const segment = thetaStart + (s / segments) * thetaLength\n // the vector's z stays 0, as three's Vector3\n vertices.push(radius * Math.cos(segment), radius * Math.sin(segment), 0)\n normals.push(0, 0, 1)\n uvs.push(((vertices[i] as number) / radius + 1) / 2, ((vertices[i + 1] as number) / radius + 1) / 2)\n }\n for (let i = 1; i <= segments; i++) indices.push(i, i + 1, 0)\n return pack('CircleGeometry', parameters, { vertices, normals, uvs, indices, groups: [] })\n}\n\nexport function ring(innerRadius = 0.5, outerRadius = 1, thetaSegments = 32, phiSegments = 1, thetaStart = 0, thetaLength = Math.PI * 2): PrimitiveBuffers {\n const parameters = { innerRadius, outerRadius, thetaSegments, phiSegments, thetaStart, thetaLength }\n thetaSegments = Math.max(3, thetaSegments)\n phiSegments = Math.max(1, phiSegments)\n const indices: number[] = []\n const vertices: number[] = []\n const normals: number[] = []\n const uvs: number[] = []\n let radius = innerRadius\n const radiusStep = (outerRadius - innerRadius) / phiSegments\n for (let j = 0; j <= phiSegments; j++) {\n for (let i = 0; i <= thetaSegments; i++) {\n const segment = thetaStart + (i / thetaSegments) * thetaLength\n const x = radius * Math.cos(segment)\n const y = radius * Math.sin(segment)\n vertices.push(x, y, 0)\n normals.push(0, 0, 1)\n uvs.push((x / outerRadius + 1) / 2, (y / outerRadius + 1) / 2)\n }\n radius += radiusStep\n }\n for (let j = 0; j < phiSegments; j++) {\n const thetaSegmentLevel = j * (thetaSegments + 1)\n for (let i = 0; i < thetaSegments; i++) {\n const segment = i + thetaSegmentLevel\n const a = segment\n const b = segment + thetaSegments + 1\n const c = segment + thetaSegments + 2\n const d = segment + 1\n indices.push(a, b, d)\n indices.push(b, c, d)\n }\n }\n return pack('RingGeometry', parameters, { vertices, normals, uvs, indices, groups: [] })\n}\n\nexport function lathe(points: ReadonlyArray<readonly [number, number]>, segments = 12, phiStart = 0, phiLength = Math.PI * 2): PrimitiveBuffers {\n const parameters = { points: points.map(([x, y]) => ({ x, y })), segments, phiStart, phiLength }\n segments = Math.floor(segments)\n phiLength = Math.max(0, Math.min(Math.PI * 2, phiLength))\n const indices: number[] = []\n const vertices: number[] = []\n const uvs: number[] = []\n const initNormals: number[] = []\n const normals: number[] = []\n const inverseSegments = 1.0 / segments\n const normal = { x: 0, y: 0, z: 0 }\n const curNormal = { x: 0, y: 0, z: 0 }\n const prevNormal = { x: 0, y: 0, z: 0 }\n let dx = 0\n let dy = 0\n const last = points.length - 1\n const px = (j: number): number => (points[j] as readonly [number, number])[0]\n const py = (j: number): number => (points[j] as readonly [number, number])[1]\n const setNormalized = (): void => {\n const n = normalize3(normal.x, normal.y, normal.z)\n normal.x = n[0]\n normal.y = n[1]\n normal.z = n[2]\n }\n for (let j = 0; j <= last; j++) {\n if (j === 0) {\n dx = px(j + 1) - px(j)\n dy = py(j + 1) - py(j)\n normal.x = dy * 1.0\n normal.y = -dx\n normal.z = dy * 0.0\n prevNormal.x = normal.x\n prevNormal.y = normal.y\n prevNormal.z = normal.z\n setNormalized()\n initNormals.push(normal.x, normal.y, normal.z)\n } else if (j === last) {\n initNormals.push(prevNormal.x, prevNormal.y, prevNormal.z)\n } else {\n dx = px(j + 1) - px(j)\n dy = py(j + 1) - py(j)\n normal.x = dy * 1.0\n normal.y = -dx\n normal.z = dy * 0.0\n curNormal.x = normal.x\n curNormal.y = normal.y\n curNormal.z = normal.z\n normal.x += prevNormal.x\n normal.y += prevNormal.y\n normal.z += prevNormal.z\n setNormalized()\n initNormals.push(normal.x, normal.y, normal.z)\n prevNormal.x = curNormal.x\n prevNormal.y = curNormal.y\n prevNormal.z = curNormal.z\n }\n }\n for (let i = 0; i <= segments; i++) {\n const phi = phiStart + i * inverseSegments * phiLength\n const sin = Math.sin(phi)\n const cos = Math.cos(phi)\n for (let j = 0; j <= last; j++) {\n vertices.push(px(j) * sin, py(j), px(j) * cos)\n uvs.push(i / segments, j / (points.length - 1))\n const x = (initNormals[3 * j + 0] as number) * sin\n const y = initNormals[3 * j + 1] as number\n const z = (initNormals[3 * j + 0] as number) * cos\n normals.push(x, y, z)\n }\n }\n for (let i = 0; i < segments; i++) {\n for (let j = 0; j < points.length - 1; j++) {\n const base = j + i * points.length\n const a = base\n const b = base + points.length\n const c = base + points.length + 1\n const d = base + 1\n indices.push(a, b, d)\n indices.push(c, d, b)\n }\n }\n return pack('LatheGeometry', parameters, { vertices, normals, uvs, indices, groups: [] })\n}\n\nexport function torus(radius = 1, tube = 0.4, radialSegments = 12, tubularSegments = 48, arc = Math.PI * 2): PrimitiveBuffers {\n const parameters = { radius, tube, radialSegments, tubularSegments, arc }\n radialSegments = Math.floor(radialSegments)\n tubularSegments = Math.floor(tubularSegments)\n const indices: number[] = []\n const vertices: number[] = []\n const normals: number[] = []\n const uvs: number[] = []\n for (let j = 0; j <= radialSegments; j++) {\n for (let i = 0; i <= tubularSegments; i++) {\n const u = (i / tubularSegments) * arc\n const v = (j / radialSegments) * Math.PI * 2\n const x = (radius + tube * Math.cos(v)) * Math.cos(u)\n const y = (radius + tube * Math.cos(v)) * Math.sin(u)\n const z = tube * Math.sin(v)\n vertices.push(x, y, z)\n const cx = radius * Math.cos(u)\n const cy = radius * Math.sin(u)\n const n = normalize3(x - cx, y - cy, z - 0)\n normals.push(n[0], n[1], n[2])\n uvs.push(i / tubularSegments)\n uvs.push(j / radialSegments)\n }\n }\n for (let j = 1; j <= radialSegments; j++) {\n for (let i = 1; i <= tubularSegments; i++) {\n const a = (tubularSegments + 1) * j + i - 1\n const b = (tubularSegments + 1) * (j - 1) + i - 1\n const c = (tubularSegments + 1) * (j - 1) + i\n const d = (tubularSegments + 1) * j + i\n indices.push(a, b, d)\n indices.push(b, c, d)\n }\n }\n return pack('TorusGeometry', parameters, { vertices, normals, uvs, indices, groups: [] })\n}\n","import type { HoleSpec, PrimitiveBuffers, Vec3 } from '../types'\nimport { indexArray } from './three-generators'\n\n/**\n * A box (centred, `size = [w, h, d]`) minus rectangular prisms that go all the way through it along z: the cheap, exact boolean (walls with\n * windows and doors). The face is cut into a grid at every hole edge; each solid cell gets its front and back quads, and every cell edge between\n * solid and empty (a hole or the outside) gets a quad over the depth facing the empty side, so the outer sides and the reveals come out of the\n * same rule. Front uvs map the face to [0, 1]; side uvs run along the edge and the depth.\n */\nexport function difference(size: Vec3, subtract: readonly HoleSpec[]): PrimitiveBuffers {\n const [w, h, d] = size\n const hw = w / 2\n const hh = h / 2\n const hd = d / 2\n const holes = subtract\n .map((hole) => ({ x0: Math.max(-hw, Math.min(hole.min[0], hole.max[0])), x1: Math.min(hw, Math.max(hole.min[0], hole.max[0])), y0: Math.max(-hh, Math.min(hole.min[1], hole.max[1])), y1: Math.min(hh, Math.max(hole.min[1], hole.max[1])) }))\n .filter((hole) => hole.x1 > hole.x0 && hole.y1 > hole.y0)\n const cuts = (lo: number, hi: number, values: number[]): number[] => [...new Set([lo, hi, ...values.filter((v) => v > lo && v < hi)])].sort((a, b) => a - b)\n const xs = cuts(-hw, hw, holes.flatMap((hole) => [hole.x0, hole.x1]))\n const ys = cuts(-hh, hh, holes.flatMap((hole) => [hole.y0, hole.y1]))\n const nx = xs.length - 1\n const ny = ys.length - 1\n const solid = (i: number, j: number): boolean => {\n if (i < 0 || j < 0 || i >= nx || j >= ny) return false\n const cx = ((xs[i] as number) + (xs[i + 1] as number)) / 2\n const cy = ((ys[j] as number) + (ys[j + 1] as number)) / 2\n return !holes.some((hole) => cx > hole.x0 && cx < hole.x1 && cy > hole.y0 && cy < hole.y1)\n }\n const positions: number[] = []\n const normals: number[] = []\n const uvs: number[] = []\n const indices: number[] = []\n const quad = (corners: readonly (readonly [number, number, number])[], normal: readonly [number, number, number], uv: readonly (readonly [number, number])[]): void => {\n const base = positions.length / 3\n for (let k = 0; k < 4; k++) {\n positions.push(...(corners[k] as [number, number, number]))\n normals.push(...normal)\n uvs.push(...(uv[k] as [number, number]))\n }\n indices.push(base, base + 1, base + 2, base, base + 2, base + 3)\n }\n const fu = (x: number): number => (x + hw) / w\n const fv = (y: number): number => (y + hh) / h\n for (let j = 0; j < ny; j++) {\n for (let i = 0; i < nx; i++) {\n if (!solid(i, j)) continue\n const x0 = xs[i] as number\n const x1 = xs[i + 1] as number\n const y0 = ys[j] as number\n const y1 = ys[j + 1] as number\n quad([[x0, y0, hd], [x1, y0, hd], [x1, y1, hd], [x0, y1, hd]], [0, 0, 1], [[fu(x0), fv(y0)], [fu(x1), fv(y0)], [fu(x1), fv(y1)], [fu(x0), fv(y1)]])\n quad([[x1, y0, -hd], [x0, y0, -hd], [x0, y1, -hd], [x1, y1, -hd]], [0, 0, -1], [[1 - fu(x1), fv(y0)], [1 - fu(x0), fv(y0)], [1 - fu(x0), fv(y1)], [1 - fu(x1), fv(y1)]])\n const zu = (z: number): number => (z + hd) / d\n if (!solid(i - 1, j)) quad([[x0, y0, -hd], [x0, y0, hd], [x0, y1, hd], [x0, y1, -hd]], [-1, 0, 0], [[zu(-hd), fv(y0)], [zu(hd), fv(y0)], [zu(hd), fv(y1)], [zu(-hd), fv(y1)]])\n if (!solid(i + 1, j)) quad([[x1, y0, hd], [x1, y0, -hd], [x1, y1, -hd], [x1, y1, hd]], [1, 0, 0], [[zu(hd), fv(y0)], [zu(-hd), fv(y0)], [zu(-hd), fv(y1)], [zu(hd), fv(y1)]])\n if (!solid(i, j - 1)) quad([[x0, y0, -hd], [x1, y0, -hd], [x1, y0, hd], [x0, y0, hd]], [0, -1, 0], [[fu(x0), zu(-hd)], [fu(x1), zu(-hd)], [fu(x1), zu(hd)], [fu(x0), zu(hd)]])\n if (!solid(i, j + 1)) quad([[x0, y1, hd], [x1, y1, hd], [x1, y1, -hd], [x0, y1, -hd]], [0, 1, 0], [[fu(x0), zu(hd)], [fu(x1), zu(hd)], [fu(x1), zu(-hd)], [fu(x0), zu(-hd)]])\n }\n }\n return {\n type: 'BufferGeometry',\n parameters: { size: [w, h, d], subtract: holes.map((hole) => ({ min: [hole.x0, hole.y0], max: [hole.x1, hole.y1] })) },\n positions: new Float32Array(positions),\n normals: new Float32Array(normals),\n uvs: new Float32Array(uvs),\n indices: indexArray(indices),\n groups: [],\n }\n}\n","/* eslint-disable */\n// @ts-nocheck -- verbatim copy of three.js r160 src/extras/Earcut.js (MIT), itself a port of mapbox/earcut v2.2.4 (ISC); kept as is so triangulation matches three\n/**\n * Port from https://github.com/mapbox/earcut (v2.2.4)\n */\n\nconst Earcut = {\n\n\ttriangulate: function ( data, holeIndices, dim = 2 ) {\n\n\t\tconst hasHoles = holeIndices && holeIndices.length;\n\t\tconst outerLen = hasHoles ? holeIndices[ 0 ] * dim : data.length;\n\t\tlet outerNode = linkedList( data, 0, outerLen, dim, true );\n\t\tconst triangles = [];\n\n\t\tif ( ! outerNode || outerNode.next === outerNode.prev ) return triangles;\n\n\t\tlet minX, minY, maxX, maxY, x, y, invSize;\n\n\t\tif ( hasHoles ) outerNode = eliminateHoles( data, holeIndices, outerNode, dim );\n\n\t\t// if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox\n\t\tif ( data.length > 80 * dim ) {\n\n\t\t\tminX = maxX = data[ 0 ];\n\t\t\tminY = maxY = data[ 1 ];\n\n\t\t\tfor ( let i = dim; i < outerLen; i += dim ) {\n\n\t\t\t\tx = data[ i ];\n\t\t\t\ty = data[ i + 1 ];\n\t\t\t\tif ( x < minX ) minX = x;\n\t\t\t\tif ( y < minY ) minY = y;\n\t\t\t\tif ( x > maxX ) maxX = x;\n\t\t\t\tif ( y > maxY ) maxY = y;\n\n\t\t\t}\n\n\t\t\t// minX, minY and invSize are later used to transform coords into integers for z-order calculation\n\t\t\tinvSize = Math.max( maxX - minX, maxY - minY );\n\t\t\tinvSize = invSize !== 0 ? 32767 / invSize : 0;\n\n\t\t}\n\n\t\tearcutLinked( outerNode, triangles, dim, minX, minY, invSize, 0 );\n\n\t\treturn triangles;\n\n\t}\n\n};\n\n// create a circular doubly linked list from polygon points in the specified winding order\nfunction linkedList( data, start, end, dim, clockwise ) {\n\n\tlet i, last;\n\n\tif ( clockwise === ( signedArea( data, start, end, dim ) > 0 ) ) {\n\n\t\tfor ( i = start; i < end; i += dim ) last = insertNode( i, data[ i ], data[ i + 1 ], last );\n\n\t} else {\n\n\t\tfor ( i = end - dim; i >= start; i -= dim ) last = insertNode( i, data[ i ], data[ i + 1 ], last );\n\n\t}\n\n\tif ( last && equals( last, last.next ) ) {\n\n\t\tremoveNode( last );\n\t\tlast = last.next;\n\n\t}\n\n\treturn last;\n\n}\n\n// eliminate colinear or duplicate points\nfunction filterPoints( start, end ) {\n\n\tif ( ! start ) return start;\n\tif ( ! end ) end = start;\n\n\tlet p = start,\n\t\tagain;\n\tdo {\n\n\t\tagain = false;\n\n\t\tif ( ! p.steiner && ( equals( p, p.next ) || area( p.prev, p, p.next ) === 0 ) ) {\n\n\t\t\tremoveNode( p );\n\t\t\tp = end = p.prev;\n\t\t\tif ( p === p.next ) break;\n\t\t\tagain = true;\n\n\t\t} else {\n\n\t\t\tp = p.next;\n\n\t\t}\n\n\t} while ( again || p !== end );\n\n\treturn end;\n\n}\n\n// main ear slicing loop which triangulates a polygon (given as a linked list)\nfunction earcutLinked( ear, triangles, dim, minX, minY, invSize, pass ) {\n\n\tif ( ! ear ) return;\n\n\t// interlink polygon nodes in z-order\n\tif ( ! pass && invSize ) indexCurve( ear, minX, minY, invSize );\n\n\tlet stop = ear,\n\t\tprev, next;\n\n\t// iterate through ears, slicing them one by one\n\twhile ( ear.prev !== ear.next ) {\n\n\t\tprev = ear.prev;\n\t\tnext = ear.next;\n\n\t\tif ( invSize ? isEarHashed( ear, minX, minY, invSize ) : isEar( ear ) ) {\n\n\t\t\t// cut off the triangle\n\t\t\ttriangles.push( prev.i / dim | 0 );\n\t\t\ttriangles.push( ear.i / dim | 0 );\n\t\t\ttriangles.push( next.i / dim | 0 );\n\n\t\t\tremoveNode( ear );\n\n\t\t\t// skipping the next vertex leads to less sliver triangles\n\t\t\tear = next.next;\n\t\t\tstop = next.next;\n\n\t\t\tcontinue;\n\n\t\t}\n\n\t\tear = next;\n\n\t\t// if we looped through the whole remaining polygon and can't find any more ears\n\t\tif ( ear === stop ) {\n\n\t\t\t// try filtering points and slicing again\n\t\t\tif ( ! pass ) {\n\n\t\t\t\tearcutLinked( filterPoints( ear ), triangles, dim, minX, minY, invSize, 1 );\n\n\t\t\t\t// if this didn't work, try curing all small self-intersections locally\n\n\t\t\t} else if ( pass === 1 ) {\n\n\t\t\t\tear = cureLocalIntersections( filterPoints( ear ), triangles, dim );\n\t\t\t\tearcutLinked( ear, triangles, dim, minX, minY, invSize, 2 );\n\n\t\t\t\t// as a last resort, try splitting the remaining polygon into two\n\n\t\t\t} else if ( pass === 2 ) {\n\n\t\t\t\tsplitEarcut( ear, triangles, dim, minX, minY, invSize );\n\n\t\t\t}\n\n\t\t\tbreak;\n\n\t\t}\n\n\t}\n\n}\n\n// check whether a polygon node forms a valid ear with adjacent nodes\nfunction isEar( ear ) {\n\n\tconst a = ear.prev,\n\t\tb = ear,\n\t\tc = ear.next;\n\n\tif ( area( a, b, c ) >= 0 ) return false; // reflex, can't be an ear\n\n\t// now make sure we don't have other points inside the potential ear\n\tconst ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;\n\n\t// triangle bbox; min & max are calculated like this for speed\n\tconst x0 = ax < bx ? ( ax < cx ? ax : cx ) : ( bx < cx ? bx : cx ),\n\t\ty0 = ay < by ? ( ay < cy ? ay : cy ) : ( by < cy ? by : cy ),\n\t\tx1 = ax > bx ? ( ax > cx ? ax : cx ) : ( bx > cx ? bx : cx ),\n\t\ty1 = ay > by ? ( ay > cy ? ay : cy ) : ( by > cy ? by : cy );\n\n\tlet p = c.next;\n\twhile ( p !== a ) {\n\n\t\tif ( p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 &&\n\t\t\tpointInTriangle( ax, ay, bx, by, cx, cy, p.x, p.y ) &&\n\t\t\tarea( p.prev, p, p.next ) >= 0 ) return false;\n\t\tp = p.next;\n\n\t}\n\n\treturn true;\n\n}\n\nfunction isEarHashed( ear, minX, minY, invSize ) {\n\n\tconst a = ear.prev,\n\t\tb = ear,\n\t\tc = ear.next;\n\n\tif ( area( a, b, c ) >= 0 ) return false; // reflex, can't be an ear\n\n\tconst ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;\n\n\t// triangle bbox; min & max are calculated like this for speed\n\tconst x0 = ax < bx ? ( ax < cx ? ax : cx ) : ( bx < cx ? bx : cx ),\n\t\ty0 = ay < by ? ( ay < cy ? ay : cy ) : ( by < cy ? by : cy ),\n\t\tx1 = ax > bx ? ( ax > cx ? ax : cx ) : ( bx > cx ? bx : cx ),\n\t\ty1 = ay > by ? ( ay > cy ? ay : cy ) : ( by > cy ? by : cy );\n\n\t// z-order range for the current triangle bbox;\n\tconst minZ = zOrder( x0, y0, minX, minY, invSize ),\n\t\tmaxZ = zOrder( x1, y1, minX, minY, invSize );\n\n\tlet p = ear.prevZ,\n\t\tn = ear.nextZ;\n\n\t// look for points inside the triangle in both directions\n\twhile ( p && p.z >= minZ && n && n.z <= maxZ ) {\n\n\t\tif ( p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&\n\t\t\tpointInTriangle( ax, ay, bx, by, cx, cy, p.x, p.y ) && area( p.prev, p, p.next ) >= 0 ) return false;\n\t\tp = p.prevZ;\n\n\t\tif ( n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&\n\t\t\tpointInTriangle( ax, ay, bx, by, cx, cy, n.x, n.y ) && area( n.prev, n, n.next ) >= 0 ) return false;\n\t\tn = n.nextZ;\n\n\t}\n\n\t// look for remaining points in decreasing z-order\n\twhile ( p && p.z >= minZ ) {\n\n\t\tif ( p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&\n\t\t\tpointInTriangle( ax, ay, bx, by, cx, cy, p.x, p.y ) && area( p.prev, p, p.next ) >= 0 ) return false;\n\t\tp = p.prevZ;\n\n\t}\n\n\t// look for remaining points in increasing z-order\n\twhile ( n && n.z <= maxZ ) {\n\n\t\tif ( n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&\n\t\t\tpointInTriangle( ax, ay, bx, by, cx, cy, n.x, n.y ) && area( n.prev, n, n.next ) >= 0 ) return false;\n\t\tn = n.nextZ;\n\n\t}\n\n\treturn true;\n\n}\n\n// go through all polygon nodes and cure small local self-intersections\nfunction cureLocalIntersections( start, triangles, dim ) {\n\n\tlet p = start;\n\tdo {\n\n\t\tconst a = p.prev,\n\t\t\tb = p.next.next;\n\n\t\tif ( ! equals( a, b ) && intersects( a, p, p.next, b ) && locallyInside( a, b ) && locallyInside( b, a ) ) {\n\n\t\t\ttriangles.push( a.i / dim | 0 );\n\t\t\ttriangles.push( p.i / dim | 0 );\n\t\t\ttriangles.push( b.i / dim | 0 );\n\n\t\t\t// remove two nodes involved\n\t\t\tremoveNode( p );\n\t\t\tremoveNode( p.next );\n\n\t\t\tp = start = b;\n\n\t\t}\n\n\t\tp = p.next;\n\n\t} while ( p !== start );\n\n\treturn filterPoints( p );\n\n}\n\n// try splitting polygon into two and triangulate them independently\nfunction splitEarcut( start, triangles, dim, minX, minY, invSize ) {\n\n\t// look for a valid diagonal that divides the polygon into two\n\tlet a = start;\n\tdo {\n\n\t\tlet b = a.next.next;\n\t\twhile ( b !== a.prev ) {\n\n\t\t\tif ( a.i !== b.i && isValidDiagonal( a, b ) ) {\n\n\t\t\t\t// split the polygon in two by the diagonal\n\t\t\t\tlet c = splitPolygon( a, b );\n\n\t\t\t\t// filter colinear points around the cuts\n\t\t\t\ta = filterPoints( a, a.next );\n\t\t\t\tc = filterPoints( c, c.next );\n\n\t\t\t\t// run earcut on each half\n\t\t\t\tearcutLinked( a, triangles, dim, minX, minY, invSize, 0 );\n\t\t\t\tearcutLinked( c, triangles, dim, minX, minY, invSize, 0 );\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t\tb = b.next;\n\n\t\t}\n\n\t\ta = a.next;\n\n\t} while ( a !== start );\n\n}\n\n// link every hole into the outer loop, producing a single-ring polygon without holes\nfunction eliminateHoles( data, holeIndices, outerNode, dim ) {\n\n\tconst queue = [];\n\tlet i, len, start, end, list;\n\n\tfor ( i = 0, len = holeIndices.length; i < len; i ++ ) {\n\n\t\tstart = holeIndices[ i ] * dim;\n\t\tend = i < len - 1 ? holeIndices[ i + 1 ] * dim : data.length;\n\t\tlist = linkedList( data, start, end, dim, false );\n\t\tif ( list === list.next ) list.steiner = true;\n\t\tqueue.push( getLeftmost( list ) );\n\n\t}\n\n\tqueue.sort( compareX );\n\n\t// process holes from left to right\n\tfor ( i = 0; i < queue.length; i ++ ) {\n\n\t\touterNode = eliminateHole( queue[ i ], outerNode );\n\n\t}\n\n\treturn outerNode;\n\n}\n\nfunction compareX( a, b ) {\n\n\treturn a.x - b.x;\n\n}\n\n// find a bridge between vertices that connects hole with an outer ring and link it\nfunction eliminateHole( hole, outerNode ) {\n\n\tconst bridge = findHoleBridge( hole, outerNode );\n\tif ( ! bridge ) {\n\n\t\treturn outerNode;\n\n\t}\n\n\tconst bridgeReverse = splitPolygon( bridge, hole );\n\n\t// filter collinear points around the cuts\n\tfilterPoints( bridgeReverse, bridgeReverse.next );\n\treturn filterPoints( bridge, bridge.next );\n\n}\n\n// David Eberly's algorithm for finding a bridge between hole and outer polygon\nfunction findHoleBridge( hole, outerNode ) {\n\n\tlet p = outerNode,\n\t\tqx = - Infinity,\n\t\tm;\n\n\tconst hx = hole.x, hy = hole.y;\n\n\t// find a segment intersected by a ray from the hole's leftmost point to the left;\n\t// segment's endpoint with lesser x will be potential connection point\n\tdo {\n\n\t\tif ( hy <= p.y && hy >= p.next.y && p.next.y !== p.y ) {\n\n\t\t\tconst x = p.x + ( hy - p.y ) * ( p.next.x - p.x ) / ( p.next.y - p.y );\n\t\t\tif ( x <= hx && x > qx ) {\n\n\t\t\t\tqx = x;\n\t\t\t\tm = p.x < p.next.x ? p : p.next;\n\t\t\t\tif ( x === hx ) return m; // hole touches outer segment; pick leftmost endpoint\n\n\t\t\t}\n\n\t\t}\n\n\t\tp = p.next;\n\n\t} while ( p !== outerNode );\n\n\tif ( ! m ) return null;\n\n\t// look for points inside the triangle of hole point, segment intersection and endpoint;\n\t// if there are no points found, we have a valid connection;\n\t// otherwise choose the point of the minimum angle with the ray as connection point\n\n\tconst stop = m,\n\t\tmx = m.x,\n\t\tmy = m.y;\n\tlet tanMin = Infinity, tan;\n\n\tp = m;\n\n\tdo {\n\n\t\tif ( hx >= p.x && p.x >= mx && hx !== p.x &&\n\t\t\t\tpointInTriangle( hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y ) ) {\n\n\t\t\ttan = Math.abs( hy - p.y ) / ( hx - p.x ); // tangential\n\n\t\t\tif ( locallyInside( p, hole ) && ( tan < tanMin || ( tan === tanMin && ( p.x > m.x || ( p.x === m.x && sectorContainsSector( m, p ) ) ) ) ) ) {\n\n\t\t\t\tm = p;\n\t\t\t\ttanMin = tan;\n\n\t\t\t}\n\n\t\t}\n\n\t\tp = p.next;\n\n\t} while ( p !== stop );\n\n\treturn m;\n\n}\n\n// whether sector in vertex m contains sector in vertex p in the same coordinates\nfunction sectorContainsSector( m, p ) {\n\n\treturn area( m.prev, m, p.prev ) < 0 && area( p.next, m, m.next ) < 0;\n\n}\n\n// interlink polygon nodes in z-order\nfunction indexCurve( start, minX, minY, invSize ) {\n\n\tlet p = start;\n\tdo {\n\n\t\tif ( p.z === 0 ) p.z = zOrder( p.x, p.y, minX, minY, invSize );\n\t\tp.prevZ = p.prev;\n\t\tp.nextZ = p.next;\n\t\tp = p.next;\n\n\t} while ( p !== start );\n\n\tp.prevZ.nextZ = null;\n\tp.prevZ = null;\n\n\tsortLinked( p );\n\n}\n\n// Simon Tatham's linked list merge sort algorithm\n// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html\nfunction sortLinked( list ) {\n\n\tlet i, p, q, e, tail, numMerges, pSize, qSize,\n\t\tinSize = 1;\n\n\tdo {\n\n\t\tp = list;\n\t\tlist = null;\n\t\ttail = null;\n\t\tnumMerges = 0;\n\n\t\twhile ( p ) {\n\n\t\t\tnumMerges ++;\n\t\t\tq = p;\n\t\t\tpSize = 0;\n\t\t\tfor ( i = 0; i < inSize; i ++ ) {\n\n\t\t\t\tpSize ++;\n\t\t\t\tq = q.nextZ;\n\t\t\t\tif ( ! q ) break;\n\n\t\t\t}\n\n\t\t\tqSize = inSize;\n\n\t\t\twhile ( pSize > 0 || ( qSize > 0 && q ) ) {\n\n\t\t\t\tif ( pSize !== 0 && ( qSize === 0 || ! q || p.z <= q.z ) ) {\n\n\t\t\t\t\te = p;\n\t\t\t\t\tp = p.nextZ;\n\t\t\t\t\tpSize --;\n\n\t\t\t\t} else {\n\n\t\t\t\t\te = q;\n\t\t\t\t\tq = q.nextZ;\n\t\t\t\t\tqSize --;\n\n\t\t\t\t}\n\n\t\t\t\tif ( tail ) tail.nextZ = e;\n\t\t\t\telse list = e;\n\n\t\t\t\te.prevZ = tail;\n\t\t\t\ttail = e;\n\n\t\t\t}\n\n\t\t\tp = q;\n\n\t\t}\n\n\t\ttail.nextZ = null;\n\t\tinSize *= 2;\n\n\t} while ( numMerges > 1 );\n\n\treturn list;\n\n}\n\n// z-order of a point given coords and inverse of the longer side of data bbox\nfunction zOrder( x, y, minX, minY, invSize ) {\n\n\t// coords are transformed into non-negative 15-bit integer range\n\tx = ( x - minX ) * invSize | 0;\n\ty = ( y - minY ) * invSize | 0;\n\n\tx = ( x | ( x << 8 ) ) & 0x00FF00FF;\n\tx = ( x | ( x << 4 ) ) & 0x0F0F0F0F;\n\tx = ( x | ( x << 2 ) ) & 0x33333333;\n\tx = ( x | ( x << 1 ) ) & 0x55555555;\n\n\ty = ( y | ( y << 8 ) ) & 0x00FF00FF;\n\ty = ( y | ( y << 4 ) ) & 0x0F0F0F0F;\n\ty = ( y | ( y << 2 ) ) & 0x33333333;\n\ty = ( y | ( y << 1 ) ) & 0x55555555;\n\n\treturn x | ( y << 1 );\n\n}\n\n// find the leftmost node of a polygon ring\nfunction getLeftmost( start ) {\n\n\tlet p = start,\n\t\tleftmost = start;\n\tdo {\n\n\t\tif ( p.x < leftmost.x || ( p.x === leftmost.x && p.y < leftmost.y ) ) leftmost = p;\n\t\tp = p.next;\n\n\t} while ( p !== start );\n\n\treturn leftmost;\n\n}\n\n// check if a point lies within a convex triangle\nfunction pointInTriangle( ax, ay, bx, by, cx, cy, px, py ) {\n\n\treturn ( cx - px ) * ( ay - py ) >= ( ax - px ) * ( cy - py ) &&\n ( ax - px ) * ( by - py ) >= ( bx - px ) * ( ay - py ) &&\n ( bx - px ) * ( cy - py ) >= ( cx - px ) * ( by - py );\n\n}\n\n// check if a diagonal between two polygon nodes is valid (lies in polygon interior)\nfunction isValidDiagonal( a, b ) {\n\n\treturn a.next.i !== b.i && a.prev.i !== b.i && ! intersectsPolygon( a, b ) && // dones't intersect other edges\n ( locallyInside( a, b ) && locallyInside( b, a ) && middleInside( a, b ) && // locally visible\n ( area( a.prev, a, b.prev ) || area( a, b.prev, b ) ) || // does not create opposite-facing sectors\n equals( a, b ) && area( a.prev, a, a.next ) > 0 && area( b.prev, b, b.next ) > 0 ); // special zero-length case\n\n}\n\n// signed area of a triangle\nfunction area( p, q, r ) {\n\n\treturn ( q.y - p.y ) * ( r.x - q.x ) - ( q.x - p.x ) * ( r.y - q.y );\n\n}\n\n// check if two points are equal\nfunction equals( p1, p2 ) {\n\n\treturn p1.x === p2.x && p1.y === p2.y;\n\n}\n\n// check if two segments intersect\nfunction intersects( p1, q1, p2, q2 ) {\n\n\tconst o1 = sign( area( p1, q1, p2 ) );\n\tconst o2 = sign( area( p1, q1, q2 ) );\n\tconst o3 = sign( area( p2, q2, p1 ) );\n\tconst o4 = sign( area( p2, q2, q1 ) );\n\n\tif ( o1 !== o2 && o3 !== o4 ) return true; // general case\n\n\tif ( o1 === 0 && onSegment( p1, p2, q1 ) ) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1\n\tif ( o2 === 0 && onSegment( p1, q2, q1 ) ) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1\n\tif ( o3 === 0 && onSegment( p2, p1, q2 ) ) return true; // p2, q2 and p1 are collinear and p1 lies on p2q2\n\tif ( o4 === 0 && onSegment( p2, q1, q2 ) ) return true; // p2, q2 and q1 are collinear and q1 lies on p2q2\n\n\treturn false;\n\n}\n\n// for collinear points p, q, r, check if point q lies on segment pr\nfunction onSegment( p, q, r ) {\n\n\treturn q.x <= Math.max( p.x, r.x ) && q.x >= Math.min( p.x, r.x ) && q.y <= Math.max( p.y, r.y ) && q.y >= Math.min( p.y, r.y );\n\n}\n\nfunction sign( num ) {\n\n\treturn num > 0 ? 1 : num < 0 ? - 1 : 0;\n\n}\n\n// check if a polygon diagonal intersects any polygon segments\nfunction intersectsPolygon( a, b ) {\n\n\tlet p = a;\n\tdo {\n\n\t\tif ( p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&\n\t\t\tintersects( p, p.next, a, b ) ) return true;\n\t\tp = p.next;\n\n\t} while ( p !== a );\n\n\treturn false;\n\n}\n\n// check if a polygon diagonal is locally inside the polygon\nfunction locallyInside( a, b ) {\n\n\treturn area( a.prev, a, a.next ) < 0 ?\n\t\tarea( a, b, a.next ) >= 0 && area( a, a.prev, b ) >= 0 :\n\t\tarea( a, b, a.prev ) < 0 || area( a, a.next, b ) < 0;\n\n}\n\n// check if the middle point of a polygon diagonal is inside the polygon\nfunction middleInside( a, b ) {\n\n\tlet p = a,\n\t\tinside = false;\n\tconst px = ( a.x + b.x ) / 2,\n\t\tpy = ( a.y + b.y ) / 2;\n\tdo {\n\n\t\tif ( ( ( p.y > py ) !== ( p.next.y > py ) ) && p.next.y !== p.y &&\n\t\t\t( px < ( p.next.x - p.x ) * ( py - p.y ) / ( p.next.y - p.y ) + p.x ) )\n\t\t\tinside = ! inside;\n\t\tp = p.next;\n\n\t} while ( p !== a );\n\n\treturn inside;\n\n}\n\n// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;\n// if one belongs to the outer ring and another to a hole, it merges it into a single ring\nfunction splitPolygon( a, b ) {\n\n\tconst a2 = new Node( a.i, a.x, a.y ),\n\t\tb2 = new Node( b.i, b.x, b.y ),\n\t\tan = a.next,\n\t\tbp = b.prev;\n\n\ta.next = b;\n\tb.prev = a;\n\n\ta2.next = an;\n\tan.prev = a2;\n\n\tb2.next = a2;\n\ta2.prev = b2;\n\n\tbp.next = b2;\n\tb2.prev = bp;\n\n\treturn b2;\n\n}\n\n// create a node and optionally link it with previous one (in a circular doubly linked list)\nfunction insertNode( i, x, y, last ) {\n\n\tconst p = new Node( i, x, y );\n\n\tif ( ! last ) {\n\n\t\tp.prev = p;\n\t\tp.next = p;\n\n\t} else {\n\n\t\tp.next = last.next;\n\t\tp.prev = last;\n\t\tlast.next.prev = p;\n\t\tlast.next = p;\n\n\t}\n\n\treturn p;\n\n}\n\nfunction removeNode( p ) {\n\n\tp.next.prev = p.prev;\n\tp.prev.next = p.next;\n\n\tif ( p.prevZ ) p.prevZ.nextZ = p.nextZ;\n\tif ( p.nextZ ) p.nextZ.prevZ = p.prevZ;\n\n}\n\nfunction Node( i, x, y ) {\n\n\t// vertex index in coordinates array\n\tthis.i = i;\n\n\t// vertex coordinates\n\tthis.x = x;\n\tthis.y = y;\n\n\t// previous and next vertex nodes in a polygon ring\n\tthis.prev = null;\n\tthis.next = null;\n\n\t// z-order curve value\n\tthis.z = 0;\n\n\t// previous and next nodes in z-order\n\tthis.prevZ = null;\n\tthis.nextZ = null;\n\n\t// indicates whether this is a steiner point\n\tthis.steiner = false;\n\n}\n\nfunction signedArea( data, start, end, dim ) {\n\n\tlet sum = 0;\n\tfor ( let i = start, j = end - dim; i < end; i += dim ) {\n\n\t\tsum += ( data[ j ] - data[ i ] ) * ( data[ i + 1 ] + data[ j + 1 ] );\n\t\tj = i;\n\n\t}\n\n\treturn sum;\n\n}\n\nexport { Earcut };\n","/*\n * three.js r160 `ExtrudeGeometry` (MIT) for straight-edged shapes, ported line by line: the outline is sampled as `Shape.extractPoints` samples a\n * path of `LineCurve`s, triangulated by three's `ShapeUtils.triangulateShape` (Earcut), bevelled and walled in the same order, with the\n * `WorldUVGenerator` uvs and `computeVertexNormals` on the Float32 positions. Path extrusion (`extrudePath`) is not supported.\n */\nimport type { BevelSpec, GeometryGroup, PrimitiveBuffers, ShapeOutline, Vec2 } from '../types'\nimport { Earcut } from './earcut'\n\ninterface P {\n x: number\n y: number\n}\n\nconst equals = (a: P, b: P): boolean => a.x === b.x && a.y === b.y\n\n/** `LineCurve.getPoint(t)`. */\nconst linePoint = (v1: P, v2: P, t: number): P => (t === 1 ? { x: v2.x, y: v2.y } : { x: (v2.x - v1.x) * t + v1.x, y: (v2.y - v1.y) * t + v1.y })\n\n/** `Path.setFromPoints(points).getPoints()`: one `LineCurve` per edge, resolution 1, consecutive duplicates dropped, not closed. */\nfunction pathPoints(points: readonly Vec2[]): P[] {\n const out: P[] = []\n let last: P | undefined\n for (let i = 1; i < points.length; i++) {\n const v1 = { x: (points[i - 1] as Vec2)[0], y: (points[i - 1] as Vec2)[1] }\n const v2 = { x: (points[i] as Vec2)[0], y: (points[i] as Vec2)[1] }\n for (let d = 0; d <= 1; d++) {\n const point = linePoint(v1, v2, d / 1)\n if (last && equals(last, point)) continue\n out.push(point)\n last = point\n }\n }\n return out\n}\n\nfunction area(contour: readonly P[]): number {\n const n = contour.length\n let a = 0.0\n for (let p = n - 1, q = 0; q < n; p = q++) a += (contour[p] as P).x * (contour[q] as P).y - (contour[q] as P).x * (contour[p] as P).y\n return a * 0.5\n}\nconst isClockWise = (pts: readonly P[]): boolean => area(pts) < 0\n\nfunction removeDupEndPts(points: P[]): void {\n const l = points.length\n if (l > 2 && equals(points[l - 1] as P, points[0] as P)) points.pop()\n}\n\nfunction triangulateShape(contour: P[], holes: P[][]): number[][] {\n const vertices: number[] = []\n const holeIndices: number[] = []\n const faces: number[][] = []\n removeDupEndPts(contour)\n for (const p of contour) vertices.push(p.x, p.y)\n let holeIndex = contour.length\n holes.forEach(removeDupEndPts)\n for (let i = 0; i < holes.length; i++) {\n holeIndices.push(holeIndex)\n holeIndex += (holes[i] as P[]).length\n for (const p of holes[i] as P[]) vertices.push(p.x, p.y)\n }\n const triangles: number[] = Earcut.triangulate(vertices, holeIndices)\n for (let i = 0; i < triangles.length; i += 3) faces.push(triangles.slice(i, i + 3))\n return faces\n}\n\n/** `BufferGeometry.computeVertexNormals` for non-indexed Float32 positions, then `normalizeNormals`, both stored as Float32. */\nexport function computeVertexNormals(positions: Float32Array, indices?: ArrayLike<number>): Float32Array {\n const normals = new Float32Array(positions.length)\n const get = (array: Float32Array, i: number): [number, number, number] => [array[i * 3] as number, array[i * 3 + 1] as number, array[i * 3 + 2] as number]\n const face = (vA: number, vB: number, vC: number): [number, number, number] => {\n const pA = get(positions, vA)\n const pB = get(positions, vB)\n const pC = get(positions, vC)\n const cb = [pC[0] - pB[0], pC[1] - pB[1], pC[2] - pB[2]] as const\n const ab = [pA[0] - pB[0], pA[1] - pB[1], pA[2] - pB[2]] as const\n // Vector3.cross\n return [cb[1] * ab[2] - cb[2] * ab[1], cb[2] * ab[0] - cb[0] * ab[2], cb[0] * ab[1] - cb[1] * ab[0]]\n }\n const set = (i: number, x: number, y: number, z: number): void => {\n normals[i * 3] = x\n normals[i * 3 + 1] = y\n normals[i * 3 + 2] = z\n }\n if (indices) {\n for (let i = 0; i < indices.length; i += 3) {\n const vA = indices[i] as number\n const vB = indices[i + 1] as number\n const vC = indices[i + 2] as number\n const cb = face(vA, vB, vC)\n for (const v of [vA, vB, vC]) {\n const n = get(normals, v)\n set(v, n[0] + cb[0], n[1] + cb[1], n[2] + cb[2])\n }\n }\n } else {\n for (let i = 0; i < positions.length / 3; i += 3) {\n const cb = face(i, i + 1, i + 2)\n set(i, cb[0], cb[1], cb[2])\n set(i + 1, cb[0], cb[1], cb[2])\n set(i + 2, cb[0], cb[1], cb[2])\n }\n }\n for (let i = 0; i < normals.length / 3; i++) {\n const [x, y, z] = get(normals, i)\n const s = 1 / (Math.sqrt(x * x + y * y + z * z) || 1)\n set(i, x * s, y * s, z * s)\n }\n return normals\n}\n\nexport function extrude(outline: ShapeOutline, depth = 1, steps = 1, bevel: false | BevelSpec | undefined = undefined): PrimitiveBuffers {\n const parameters = { shape: outline, depth, steps, bevel: bevel ?? {} }\n const verticesArray: number[] = []\n const uvArray: number[] = []\n const groups: GeometryGroup[] = []\n const placeholder: number[] = []\n\n const bevelEnabled = bevel !== false\n const spec: BevelSpec = bevel === false || bevel === undefined ? {} : bevel\n let bevelThickness = spec.thickness !== undefined ? spec.thickness : 0.2\n let bevelSize = spec.size !== undefined ? spec.size : bevelThickness - 0.1\n let bevelOffset = spec.offset !== undefined ? spec.offset : 0\n let bevelSegments = spec.segments !== undefined ? spec.segments : 3\n if (!bevelEnabled) {\n bevelSegments = 0\n bevelThickness = 0\n bevelSize = 0\n bevelOffset = 0\n }\n\n let vertices = pathPoints(outline.outer)\n const holes = (outline.holes ?? []).map(pathPoints)\n const reverse = !isClockWise(vertices)\n if (reverse) {\n vertices = vertices.reverse()\n for (let h = 0; h < holes.length; h++) {\n const ahole = holes[h] as P[]\n if (isClockWise(ahole)) holes[h] = ahole.reverse()\n }\n }\n const faces = triangulateShape(vertices, holes)\n const contour = vertices\n for (const ahole of holes) vertices = vertices.concat(ahole)\n\n const scalePt2 = (pt: P, vec: P, size: number): P => ({ x: pt.x + vec.x * size, y: pt.y + vec.y * size })\n const vlen = vertices.length\n const flen = faces.length\n\n const getBevelVec = (inPt: P, inPrev: P, inNext: P): P => {\n let vTransX: number\n let vTransY: number\n let shrinkBy: number\n const vPrevX = inPt.x - inPrev.x\n const vPrevY = inPt.y - inPrev.y\n const vNextX = inNext.x - inPt.x\n const vNextY = inNext.y - inPt.y\n const vPrevLensq = vPrevX * vPrevX + vPrevY * vPrevY\n const collinear0 = vPrevX * vNextY - vPrevY * vNextX\n if (Math.abs(collinear0) > Number.EPSILON) {\n const vPrevLen = Math.sqrt(vPrevLensq)\n const vNextLen = Math.sqrt(vNextX * vNextX + vNextY * vNextY)\n const ptPrevShiftX = inPrev.x - vPrevY / vPrevLen\n const ptPrevShiftY = inPrev.y + vPrevX / vPrevLen\n const ptNextShiftX = inNext.x - vNextY / vNextLen\n const ptNextShiftY = inNext.y + vNextX / vNextLen\n const sf = ((ptNextShiftX - ptPrevShiftX) * vNextY - (ptNextShiftY - ptPrevShiftY) * vNextX) / (vPrevX * vNextY - vPrevY * vNextX)\n vTransX = ptPrevShiftX + vPrevX * sf - inPt.x\n vTransY = ptPrevShiftY + vPrevY * sf - inPt.y\n const vTransLensq = vTransX * vTransX + vTransY * vTransY\n if (vTransLensq <= 2) return { x: vTransX, y: vTransY }\n shrinkBy = Math.sqrt(vTransLensq / 2)\n } else {\n let directionEq = false\n if (vPrevX > Number.EPSILON) {\n if (vNextX > Number.EPSILON) directionEq = true\n } else if (vPrevX < -Number.EPSILON) {\n if (vNextX < -Number.EPSILON) directionEq = true\n } else if (Math.sign(vPrevY) === Math.sign(vNextY)) {\n directionEq = true\n }\n if (directionEq) {\n vTransX = -vPrevY\n vTransY = vPrevX\n shrinkBy = Math.sqrt(vPrevLensq)\n } else {\n vTransX = vPrevX\n vTransY = vPrevY\n shrinkBy = Math.sqrt(vPrevLensq / 2)\n }\n }\n return { x: vTransX / shrinkBy, y: vTransY / shrinkBy }\n }\n\n const movements = (ring: readonly P[]): P[] => {\n const out: P[] = []\n for (let i = 0, il = ring.length, j = il - 1, k = i + 1; i < il; i++, j++, k++) {\n if (j === il) j = 0\n if (k === il) k = 0\n out[i] = getBevelVec(ring[i] as P, ring[j] as P, ring[k] as P)\n }\n return out\n }\n const contourMovements = movements(contour)\n const holesMovements: P[][] = []\n let verticesMovements = contourMovements.concat()\n for (const ahole of holes) {\n const oneHoleMovements = movements(ahole)\n holesMovements.push(oneHoleMovements)\n verticesMovements = verticesMovements.concat(oneHoleMovements)\n }\n\n const v = (x: number, y: number, z: number): void => {\n placeholder.push(x)\n placeholder.push(y)\n placeholder.push(z)\n }\n const bevelLayer = (b: number, zOf: (z: number) => number): void => {\n const t = b / bevelSegments\n const z = bevelThickness * Math.cos((t * Math.PI) / 2)\n const bs = bevelSize * Math.sin((t * Math.PI) / 2) + bevelOffset\n for (let i = 0; i < contour.length; i++) {\n const vert = scalePt2(contour[i] as P, contourMovements[i] as P, bs)\n v(vert.x, vert.y, zOf(z))\n }\n for (let h = 0; h < holes.length; h++) {\n const ahole = holes[h] as P[]\n const oneHoleMovements = holesMovements[h] as P[]\n for (let i = 0; i < ahole.length; i++) {\n const vert = scalePt2(ahole[i] as P, oneHoleMovements[i] as P, bs)\n v(vert.x, vert.y, zOf(z))\n }\n }\n }\n for (let b = 0; b < bevelSegments; b++) bevelLayer(b, (z) => -z)\n const bs = bevelSize + bevelOffset\n for (let i = 0; i < vlen; i++) {\n const vert = bevelEnabled ? scalePt2(vertices[i] as P, verticesMovements[i] as P, bs) : (vertices[i] as P)\n v(vert.x, vert.y, 0)\n }\n for (let s = 1; s <= steps; s++) {\n for (let i = 0; i < vlen; i++) {\n const vert = bevelEnabled ? scalePt2(vertices[i] as P, verticesMovements[i] as P, bs) : (vertices[i] as P)\n v(vert.x, vert.y, (depth / steps) * s)\n }\n }\n for (let b = bevelSegments - 1; b >= 0; b--) bevelLayer(b, (z) => depth + z)\n\n const addVertex = (index: number): void => {\n verticesArray.push(placeholder[index * 3 + 0] as number)\n verticesArray.push(placeholder[index * 3 + 1] as number)\n verticesArray.push(placeholder[index * 3 + 2] as number)\n }\n const addUV = (x: number, y: number): void => {\n uvArray.push(x)\n uvArray.push(y)\n }\n const at = (index: number, axis: number): number => verticesArray[index * 3 + axis] as number\n const f3 = (a: number, b: number, c: number): void => {\n addVertex(a)\n addVertex(b)\n addVertex(c)\n const nextIndex = verticesArray.length / 3\n for (const index of [nextIndex - 3, nextIndex - 2, nextIndex - 1]) addUV(at(index, 0), at(index, 1))\n }\n const f4 = (a: number, b: number, c: number, d: number): void => {\n addVertex(a)\n addVertex(b)\n addVertex(d)\n addVertex(b)\n addVertex(c)\n addVertex(d)\n const nextIndex = verticesArray.length / 3\n const iA = nextIndex - 6\n const iB = nextIndex - 3\n const iC = nextIndex - 2\n const iD = nextIndex - 1\n const byX = Math.abs(at(iA, 1) - at(iB, 1)) < Math.abs(at(iA, 0) - at(iB, 0))\n const uv = (index: number): [number, number] => [byX ? at(index, 0) : at(index, 1), 1 - at(index, 2)]\n const uvs = [uv(iA), uv(iB), uv(iC), uv(iD)]\n for (const k of [0, 1, 3, 1, 2, 3]) addUV(...(uvs[k] as [number, number]))\n }\n\n // lid faces\n let start = verticesArray.length / 3\n if (bevelEnabled) {\n let offset = 0\n for (let i = 0; i < flen; i++) {\n const face = faces[i] as number[]\n f3((face[2] as number) + offset, (face[1] as number) + offset, (face[0] as number) + offset)\n }\n const layer = steps + bevelSegments * 2\n offset = vlen * layer\n for (let i = 0; i < flen; i++) {\n const face = faces[i] as number[]\n f3((face[0] as number) + offset, (face[1] as number) + offset, (face[2] as number) + offset)\n }\n } else {\n for (let i = 0; i < flen; i++) {\n const face = faces[i] as number[]\n f3(face[2] as number, face[1] as number, face[0] as number)\n }\n for (let i = 0; i < flen; i++) {\n const face = faces[i] as number[]\n f3((face[0] as number) + vlen * steps, (face[1] as number) + vlen * steps, (face[2] as number) + vlen * steps)\n }\n }\n groups.push({ start, count: verticesArray.length / 3 - start, materialIndex: 0 })\n\n // side faces\n start = verticesArray.length / 3\n const sidewalls = (ring: readonly P[], layeroffset: number): void => {\n let i = ring.length\n while (--i >= 0) {\n const j = i\n let k = i - 1\n if (k < 0) k = ring.length - 1\n for (let s = 0, sl = steps + bevelSegments * 2; s < sl; s++) {\n const slen1 = vlen * s\n const slen2 = vlen * (s + 1)\n f4(layeroffset + j + slen1, layeroffset + k + slen1, layeroffset + k + slen2, layeroffset + j + slen2)\n }\n }\n }\n let layeroffset = 0\n sidewalls(contour, layeroffset)\n layeroffset += contour.length\n for (const ahole of holes) {\n sidewalls(ahole, layeroffset)\n layeroffset += ahole.length\n }\n groups.push({ start, count: verticesArray.length / 3 - start, materialIndex: 1 })\n\n const positions = new Float32Array(verticesArray)\n return { type: 'ExtrudeGeometry', parameters, positions, normals: computeVertexNormals(positions), uvs: new Float32Array(uvArray), groups }\n}\n","import { canonicalJson } from '../hash'\nimport type { PrimitiveBuffers, PrimitiveNode } from '../types'\nimport { difference } from './difference'\nimport { computeVertexNormals, extrude } from './extrude'\nimport { box, circle, cone, cylinder, indexArray, lathe, plane, ring, sphere, torus } from './three-generators'\n\nexport { box, circle, cone, cylinder, lathe, plane, ring, sphere, torus } from './three-generators'\nexport { computeVertexNormals, extrude } from './extrude'\nexport { difference } from './difference'\n\n/** How a level of detail changes a primitive. */\nexport interface DetailOptions {\n /** Multiplies the segment counts of round primitives (1 leaves them as given). */\n readonly detail?: number\n /** Floor of the scaled segment counts (default 3). */\n readonly minSegments?: number\n}\n\nconst PARAMETER_KEYS: Readonly<Record<PrimitiveNode['kind'], readonly string[]>> = {\n box: ['size', 'segments'],\n cylinder: ['radiusTop', 'radiusBottom', 'height', 'radialSegments', 'heightSegments', 'openEnded', 'thetaStart', 'thetaLength'],\n cone: ['radius', 'height', 'radialSegments', 'heightSegments', 'openEnded', 'thetaStart', 'thetaLength'],\n sphere: ['radius', 'widthSegments', 'heightSegments', 'phiStart', 'phiLength', 'thetaStart', 'thetaLength'],\n plane: ['width', 'height', 'widthSegments', 'heightSegments'],\n circle: ['radius', 'segments', 'thetaStart', 'thetaLength'],\n ring: ['innerRadius', 'outerRadius', 'thetaSegments', 'phiSegments', 'thetaStart', 'thetaLength'],\n lathe: ['points', 'segments', 'phiStart', 'phiLength'],\n torus: ['radius', 'tube', 'radialSegments', 'tubularSegments', 'arc'],\n extrude: ['shape', 'depth', 'steps', 'bevel'],\n difference: ['size', 'subtract'],\n mesh: ['positions', 'normals', 'uvs', 'indices'],\n}\n\n/** The segment fields `detail` scales, per kind. */\nconst SEGMENT_KEYS: Readonly<Partial<Record<PrimitiveNode['kind'], readonly string[]>>> = {\n cylinder: ['radialSegments'],\n cone: ['radialSegments'],\n sphere: ['widthSegments', 'heightSegments'],\n circle: ['segments'],\n ring: ['thetaSegments'],\n lathe: ['segments'],\n torus: ['radialSegments', 'tubularSegments'],\n}\n/** three's defaults of the segment fields, so a scaled default is scaled from the right number. */\nconst SEGMENT_DEFAULTS: Readonly<Record<string, number>> = { radialSegments: 32, widthSegments: 32, heightSegments: 16, segments: 12, thetaSegments: 32, tubularSegments: 48 }\nconst segmentDefault = (kind: string, key: string): number => (kind === 'circle' && key === 'segments' ? 32 : kind === 'torus' && key === 'radialSegments' ? 12 : (SEGMENT_DEFAULTS[key] as number))\n\n/** The parameters of a primitive node (no transform, colour or name), with the level of detail applied: what its geometry depends on. */\nexport function primitiveParameters(node: PrimitiveNode, options: DetailOptions = {}): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n const source = node as unknown as Record<string, unknown>\n for (const key of PARAMETER_KEYS[node.kind]) if (source[key] !== undefined) out[key] = source[key]\n const detail = options.detail ?? 1\n if (detail !== 1) {\n const floor = options.minSegments ?? 3\n for (const key of SEGMENT_KEYS[node.kind] ?? []) {\n const given = (out[key] as number | undefined) ?? segmentDefault(node.kind, key)\n out[key] = Math.max(floor, Math.round(given * detail))\n }\n }\n return out\n}\n\nfunction mesh(p: Record<string, unknown>): PrimitiveBuffers {\n const positions = new Float32Array(p.positions as number[])\n const indices = p.indices ? indexArray(p.indices as number[]) : undefined\n return {\n type: 'BufferGeometry',\n parameters: {},\n positions,\n normals: p.normals ? new Float32Array(p.normals as number[]) : computeVertexNormals(positions, indices),\n uvs: p.uvs ? new Float32Array(p.uvs as number[]) : new Float32Array((positions.length / 3) * 2),\n ...(indices ? { indices } : {}),\n groups: [],\n }\n}\n\nfunction make(kind: PrimitiveNode['kind'], p: Record<string, any>): PrimitiveBuffers {\n switch (kind) {\n case 'box': {\n const size = (p.size ?? [1, 1, 1]) as number[]\n const segments = (p.segments ?? [1, 1, 1]) as number[]\n return box(size[0], size[1], size[2], segments[0], segments[1], segments[2])\n }\n case 'cylinder':\n return cylinder(p.radiusTop, p.radiusBottom, p.height, p.radialSegments, p.heightSegments, p.openEnded, p.thetaStart, p.thetaLength)\n case 'cone':\n return cone(p.radius, p.height, p.radialSegments, p.heightSegments, p.openEnded, p.thetaStart, p.thetaLength)\n case 'sphere':\n return sphere(p.radius, p.widthSegments, p.heightSegments, p.phiStart, p.phiLength, p.thetaStart, p.thetaLength)\n case 'plane':\n return plane(p.width, p.height, p.widthSegments, p.heightSegments)\n case 'circle':\n return circle(p.radius, p.segments, p.thetaStart, p.thetaLength)\n case 'ring':\n return ring(p.innerRadius, p.outerRadius, p.thetaSegments, p.phiSegments, p.thetaStart, p.thetaLength)\n case 'lathe':\n return lathe(p.points, p.segments, p.phiStart, p.phiLength)\n case 'torus':\n return torus(p.radius, p.tube, p.radialSegments, p.tubularSegments, p.arc)\n case 'extrude':\n return extrude(p.shape, p.depth, p.steps, p.bevel)\n case 'difference':\n return difference(p.size, p.subtract ?? [])\n case 'mesh':\n return mesh(p)\n default:\n throw new Error(`game-shapes: unknown primitive kind \"${String(kind)}\"`)\n }\n}\n\n/** Templates by their parameter text: the same cylinder is generated once per process. Read-only by contract. */\nconst templates = new Map<string, PrimitiveBuffers>()\nlet templateLimit = 4096\nconst stats = { hits: 0, misses: 0 }\n\n/** Sets how many primitive templates are kept (least recently used out; 0 turns the cache off). */\nexport function setTemplateCacheSize(size: number): void {\n templateLimit = Math.max(0, Math.floor(size))\n while (templates.size > templateLimit) templates.delete(templates.keys().next().value as string)\n}\nexport function clearTemplateCache(): void {\n templates.clear()\n stats.hits = 0\n stats.misses = 0\n}\nexport const templateCacheStats = (): { size: number; hits: number; misses: number } => ({ size: templates.size, ...stats })\n\n/**\n * The geometry of one primitive on its own, not baked: the arrays three's generator of the same kind builds, bit for bit (meshes and differences\n * are `BufferGeometry`). The result comes from a shared template cache and must not be written to (copy it with `clonePrimitive`).\n */\nexport function buildPrimitive(node: PrimitiveNode, options: DetailOptions = {}): PrimitiveBuffers {\n const parameters = primitiveParameters(node, options)\n // raw meshes are not worth a key of their whole arrays; they are cheap to wrap\n if (node.kind === 'mesh' || templateLimit === 0) return make(node.kind, parameters)\n const key = `${node.kind}:${canonicalJson(parameters)}`\n const cached = templates.get(key)\n if (cached) {\n stats.hits++\n templates.delete(key)\n templates.set(key, cached)\n return cached\n }\n stats.misses++\n const built = make(node.kind, parameters)\n templates.set(key, built)\n if (templates.size > templateLimit) templates.delete(templates.keys().next().value as string)\n return built\n}\n\n/** A copy that owns its arrays. */\nexport function clonePrimitive(p: PrimitiveBuffers): PrimitiveBuffers {\n return {\n type: p.type,\n parameters: p.parameters,\n positions: p.positions.slice(),\n normals: p.normals.slice(),\n uvs: p.uvs.slice(),\n ...(p.indices ? { indices: p.indices.slice() } : {}),\n groups: p.groups.map((group) => ({ ...group })),\n }\n}\n","import { hash64 } from './hash'\n\n/** mulberry32: a small, fast 32-bit seeded stream in [0, 1). */\nexport function mulberry32(seed: number): () => number {\n let state = seed >>> 0\n return () => {\n state = (state + 0x6d2b79f5) >>> 0\n let t = state\n t = Math.imul(t ^ (t >>> 15), t | 1)\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n }\n}\n\n/** The stream of one node: its own per (seed, generator, path), so adding a node never shifts another node's draws. */\nexport const streamFor = (seed: number | string, generator: string, path: string): (() => number) =>\n mulberry32(parseInt(hash64(`${String(seed)}\\u0000${generator}\\u0000${path}`).slice(0, 8), 16))\n","import type { ShapeGenerator } from './types'\n\nconst GENERATOR_NAME = /^[a-zA-Z][\\w.:-]*$/\n\n/** The generators of one set: what `{ kind: 'custom', generator: name, params }` nodes look up. */\nexport interface ShapeGenerators {\n /**\n * Registers a generator: a pure function of its params and a seeded stream that returns a node. A worker that generates shapes with custom nodes must\n * register the same generators (import the same module in the worker script).\n */\n register(name: string, generator: ShapeGenerator): void\n unregister(name: string): boolean\n get(name: string): ShapeGenerator | undefined\n list(): string[]\n /** The generators by name, as `GenerateOptions.generators` takes them. */\n record(): Readonly<Record<string, ShapeGenerator>>\n}\n\nexport const generatorsOver = (generators: Map<string, ShapeGenerator>): ShapeGenerators => ({\n register(name, generator) {\n if (!GENERATOR_NAME.test(name)) throw new Error(`game-shapes: generator name \"${name}\" must start with a letter and use letters, digits, \".\", \":\", \"-\" or \"_\"`)\n generators.set(name, generator)\n },\n unregister: (name) => generators.delete(name),\n get: (name) => generators.get(name),\n list: () => [...generators.keys()].sort(),\n record: () => Object.fromEntries(generators),\n})\n\nlet defaultGenerators: ShapeGenerators | undefined\n\n/** The set behind `registerGenerator`, `getGenerator` and the rest of the global functions, created on first use. */\nexport const defaultShapeGenerators = (): ShapeGenerators => (defaultGenerators ??= generatorsOver(new Map()))\n\n/** @deprecated use `createShapes(...).register`. */\nexport const registerGenerator = (name: string, generator: ShapeGenerator): void => defaultShapeGenerators().register(name, generator)\n/** @deprecated use `createShapes(...).unregister`. */\nexport const unregisterGenerator = (name: string): boolean => defaultShapeGenerators().unregister(name)\n/** @deprecated use `createShapes(...).get`. */\nexport const getGenerator = (name: string): ShapeGenerator | undefined => defaultShapeGenerators().get(name)\n/** @deprecated use `createShapes(...).list`. */\nexport const listGenerators = (): string[] => defaultShapeGenerators().list()\n","{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://github.com/Andersonfrfilho/adatechnology-game-kit/packages/game-shapes/schema/shape.schema.json\",\n \"title\": \"game-shapes shape\",\n \"type\": \"object\",\n \"required\": [\n \"version\",\n \"root\"\n ],\n \"additionalProperties\": false,\n \"properties\": {\n \"version\": {\n \"const\": 1\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"materials\": {\n \"type\": \"object\",\n \"additionalProperties\": {\n \"$ref\": \"#/$defs/material\"\n }\n },\n \"root\": {\n \"$ref\": \"#/$defs/node\"\n },\n \"lod\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/lodLevel\"\n }\n },\n \"cullDistance\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n }\n },\n \"$defs\": {\n \"number\": {\n \"type\": \"number\"\n },\n \"count\": {\n \"type\": \"integer\",\n \"minimum\": 0\n },\n \"vec2\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"number\"\n },\n \"minItems\": 2,\n \"maxItems\": 2\n },\n \"vec3\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"number\"\n },\n \"minItems\": 3,\n \"maxItems\": 3\n },\n \"mat4\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"number\"\n },\n \"minItems\": 16,\n \"maxItems\": 16\n },\n \"color\": {\n \"oneOf\": [\n {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 16777215\n },\n {\n \"type\": \"string\",\n \"pattern\": \"^#[0-9a-fA-F]{6}$\"\n }\n ]\n },\n \"uvRect\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\n \"u0\",\n \"v0\",\n \"u1\",\n \"v1\"\n ],\n \"properties\": {\n \"u0\": {\n \"type\": \"number\"\n },\n \"v0\": {\n \"type\": \"number\"\n },\n \"u1\": {\n \"type\": \"number\"\n },\n \"v1\": {\n \"type\": \"number\"\n }\n }\n },\n \"rotationOrder\": {\n \"enum\": [\n \"XYZ\",\n \"YXZ\",\n \"ZXY\",\n \"ZYX\",\n \"YZX\",\n \"XZY\"\n ]\n },\n \"transform\": {\n \"type\": \"object\",\n \"properties\": {\n \"position\": {\n \"$ref\": \"#/$defs/vec3\"\n },\n \"rotation\": {\n \"$ref\": \"#/$defs/vec3\"\n },\n \"rotationOrder\": {\n \"$ref\": \"#/$defs/rotationOrder\"\n },\n \"scale\": {\n \"oneOf\": [\n {\n \"type\": \"number\"\n },\n {\n \"$ref\": \"#/$defs/vec3\"\n }\n ]\n },\n \"heading\": {\n \"type\": \"number\"\n },\n \"matrix\": {\n \"$ref\": \"#/$defs/mat4\"\n }\n }\n },\n \"instance\": {\n \"allOf\": [\n {\n \"$ref\": \"#/$defs/transform\"\n }\n ],\n \"properties\": {\n \"position\": true,\n \"rotation\": true,\n \"rotationOrder\": true,\n \"scale\": true,\n \"heading\": true,\n \"matrix\": true,\n \"color\": {\n \"$ref\": \"#/$defs/color\"\n }\n },\n \"unevaluatedProperties\": false\n },\n \"material\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"properties\": {\n \"color\": {\n \"$ref\": \"#/$defs/color\"\n },\n \"roughness\": {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n \"metalness\": {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n \"opacity\": {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n \"transparent\": {\n \"type\": \"boolean\"\n },\n \"emissive\": {\n \"$ref\": \"#/$defs/color\"\n },\n \"emissiveIntensity\": {\n \"type\": \"number\",\n \"minimum\": 0\n },\n \"side\": {\n \"enum\": [\n \"front\",\n \"back\",\n \"double\"\n ]\n },\n \"flatShading\": {\n \"type\": \"boolean\"\n },\n \"vertexColors\": {\n \"type\": \"boolean\"\n },\n \"depthWrite\": {\n \"type\": \"boolean\"\n }\n }\n },\n \"lodLevel\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\n \"distance\",\n \"detail\"\n ],\n \"properties\": {\n \"distance\": {\n \"type\": \"number\",\n \"minimum\": 0\n },\n \"detail\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0,\n \"maximum\": 1\n },\n \"minSize\": {\n \"type\": \"number\",\n \"minimum\": 0\n },\n \"minSegments\": {\n \"type\": \"integer\",\n \"minimum\": 3\n }\n }\n },\n \"common\": {\n \"allOf\": [\n {\n \"$ref\": \"#/$defs/transform\"\n }\n ],\n \"properties\": {\n \"kind\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"part\": {\n \"type\": \"string\"\n },\n \"material\": {\n \"type\": \"string\"\n },\n \"color\": {\n \"$ref\": \"#/$defs/color\"\n },\n \"uv\": {\n \"$ref\": \"#/$defs/uvRect\"\n },\n \"instances\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/instance\"\n }\n },\n \"minDetail\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0,\n \"maximum\": 1\n },\n \"tags\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n },\n \"node\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\"\n ],\n \"allOf\": [\n {\n \"$ref\": \"#/$defs/common\"\n }\n ],\n \"oneOf\": [\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"group\"\n },\n \"children\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/node\"\n }\n }\n }\n },\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"box\"\n },\n \"size\": {\n \"$ref\": \"#/$defs/vec3\"\n },\n \"segments\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"minItems\": 3,\n \"maxItems\": 3\n }\n }\n },\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"cylinder\"\n },\n \"radiusTop\": {\n \"type\": \"number\"\n },\n \"radiusBottom\": {\n \"type\": \"number\"\n },\n \"height\": {\n \"type\": \"number\"\n },\n \"radialSegments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"heightSegments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"openEnded\": {\n \"type\": \"boolean\"\n },\n \"thetaStart\": {\n \"type\": \"number\"\n },\n \"thetaLength\": {\n \"type\": \"number\"\n }\n }\n },\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"cone\"\n },\n \"radius\": {\n \"type\": \"number\"\n },\n \"height\": {\n \"type\": \"number\"\n },\n \"radialSegments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"heightSegments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"openEnded\": {\n \"type\": \"boolean\"\n },\n \"thetaStart\": {\n \"type\": \"number\"\n },\n \"thetaLength\": {\n \"type\": \"number\"\n }\n }\n },\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"sphere\"\n },\n \"radius\": {\n \"type\": \"number\"\n },\n \"widthSegments\": {\n \"type\": \"integer\",\n \"minimum\": 3\n },\n \"heightSegments\": {\n \"type\": \"integer\",\n \"minimum\": 2\n },\n \"phiStart\": {\n \"type\": \"number\"\n },\n \"phiLength\": {\n \"type\": \"number\"\n },\n \"thetaStart\": {\n \"type\": \"number\"\n },\n \"thetaLength\": {\n \"type\": \"number\"\n }\n }\n },\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"plane\"\n },\n \"width\": {\n \"type\": \"number\"\n },\n \"height\": {\n \"type\": \"number\"\n },\n \"widthSegments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"heightSegments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n }\n }\n },\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"circle\"\n },\n \"radius\": {\n \"type\": \"number\"\n },\n \"segments\": {\n \"type\": \"integer\",\n \"minimum\": 3\n },\n \"thetaStart\": {\n \"type\": \"number\"\n },\n \"thetaLength\": {\n \"type\": \"number\"\n }\n }\n },\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"ring\"\n },\n \"innerRadius\": {\n \"type\": \"number\"\n },\n \"outerRadius\": {\n \"type\": \"number\"\n },\n \"thetaSegments\": {\n \"type\": \"integer\",\n \"minimum\": 3\n },\n \"phiSegments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"thetaStart\": {\n \"type\": \"number\"\n },\n \"thetaLength\": {\n \"type\": \"number\"\n }\n }\n },\n {\n \"required\": [\n \"points\"\n ],\n \"properties\": {\n \"kind\": {\n \"const\": \"lathe\"\n },\n \"points\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/vec2\"\n },\n \"minItems\": 2\n },\n \"segments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"phiStart\": {\n \"type\": \"number\"\n },\n \"phiLength\": {\n \"type\": \"number\"\n }\n }\n },\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"torus\"\n },\n \"radius\": {\n \"type\": \"number\"\n },\n \"tube\": {\n \"type\": \"number\"\n },\n \"radialSegments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"tubularSegments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"arc\": {\n \"type\": \"number\"\n }\n }\n },\n {\n \"required\": [\n \"shape\"\n ],\n \"properties\": {\n \"kind\": {\n \"const\": \"extrude\"\n },\n \"shape\": {\n \"type\": \"object\",\n \"required\": [\n \"outer\"\n ],\n \"additionalProperties\": false,\n \"properties\": {\n \"outer\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/vec2\"\n },\n \"minItems\": 3\n },\n \"holes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/vec2\"\n },\n \"minItems\": 3\n }\n }\n }\n },\n \"depth\": {\n \"type\": \"number\"\n },\n \"steps\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"bevel\": {\n \"oneOf\": [\n {\n \"const\": false\n },\n {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"properties\": {\n \"thickness\": {\n \"type\": \"number\"\n },\n \"size\": {\n \"type\": \"number\"\n },\n \"offset\": {\n \"type\": \"number\"\n },\n \"segments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n }\n }\n }\n ]\n }\n }\n },\n {\n \"required\": [\n \"size\",\n \"subtract\"\n ],\n \"properties\": {\n \"kind\": {\n \"const\": \"difference\"\n },\n \"size\": {\n \"$ref\": \"#/$defs/vec3\"\n },\n \"subtract\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"min\",\n \"max\"\n ],\n \"additionalProperties\": false,\n \"properties\": {\n \"min\": {\n \"$ref\": \"#/$defs/vec2\"\n },\n \"max\": {\n \"$ref\": \"#/$defs/vec2\"\n }\n }\n }\n }\n }\n },\n {\n \"required\": [\n \"positions\"\n ],\n \"properties\": {\n \"kind\": {\n \"const\": \"mesh\"\n },\n \"positions\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"number\"\n }\n },\n \"normals\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"number\"\n }\n },\n \"uvs\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"number\"\n }\n },\n \"indices\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/count\"\n }\n }\n }\n },\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"point\"\n },\n \"data\": {}\n }\n },\n {\n \"required\": [\n \"generator\"\n ],\n \"properties\": {\n \"kind\": {\n \"const\": \"custom\"\n },\n \"generator\": {\n \"type\": \"string\"\n },\n \"params\": {}\n }\n }\n ]\n }\n }\n}\n","import schemaJson from '../schema/shape.schema.json'\nimport type { ShapeSchema } from './types'\n\n/** The JSON Schema (draft 2020-12) of a shape document; the same file ships as `@adatechnology/game-shapes/schema.json`. */\nexport const SHAPE_SCHEMA: Readonly<Record<string, unknown>> = schemaJson\n\ntype Check = (value: unknown) => string | null\n\nconst num: Check = (v) => (typeof v === 'number' && Number.isFinite(v) ? null : 'expected a finite number')\nconst bool: Check = (v) => (typeof v === 'boolean' ? null : 'expected a boolean')\nconst str: Check = (v) => (typeof v === 'string' ? null : 'expected a string')\nconst int = (min: number): Check => (v) => (typeof v === 'number' && Number.isInteger(v) && v >= min ? null : `expected an integer >= ${min}`)\nconst range = (lo: number, hi: number, loOpen = false): Check => (v) =>\n typeof v === 'number' && (loOpen ? v > lo : v >= lo) && v <= hi ? null : `expected a number in ${loOpen ? '(' : '['}${lo}, ${hi}]`\nconst tuple = (n: number, item: Check = num): Check => (v) => {\n if (!Array.isArray(v) || v.length !== n) return `expected an array of ${n}`\n for (const x of v) if (item(x)) return `expected an array of ${n} numbers`\n return null\n}\nconst list = (item: Check, min = 0): Check => (v) => {\n if (!Array.isArray(v)) return 'expected an array'\n if (v.length < min) return `expected at least ${min} items`\n for (let i = 0; i < v.length; i++) {\n const e = item(v[i])\n if (e) return `[${i}]: ${e}`\n }\n return null\n}\nconst color: Check = (v) =>\n (typeof v === 'number' && Number.isInteger(v) && v >= 0 && v <= 0xffffff) || (typeof v === 'string' && /^#[0-9a-fA-F]{6}$/.test(v)) ? null : 'expected 0xRRGGBB or \"#rrggbb\"'\nconst oneOf = (values: readonly unknown[]): Check => (v) => (values.includes(v) ? null : `expected one of ${values.map((x) => JSON.stringify(x)).join(', ')}`)\nconst object = (fields: Readonly<Record<string, Check>>, required: readonly string[] = []): Check => (v) => {\n if (!v || typeof v !== 'object' || Array.isArray(v)) return 'expected an object'\n const record = v as Record<string, unknown>\n for (const key of required) if (record[key] === undefined) return `${key}: required`\n for (const key of Object.keys(record)) {\n if (record[key] === undefined) continue\n const check = fields[key]\n if (!check) return `${key}: unknown field`\n const e = check(record[key])\n if (e) return `${key}: ${e}`\n }\n return null\n}\n\nconst ORDERS = ['XYZ', 'YXZ', 'ZXY', 'ZYX', 'YZX', 'XZY'] as const\nconst TRANSFORM: Record<string, Check> = {\n position: tuple(3),\n rotation: tuple(3),\n rotationOrder: oneOf(ORDERS),\n scale: (v) => (typeof v === 'number' ? num(v) : tuple(3)(v)),\n heading: num,\n matrix: tuple(16),\n}\nconst UV = object({ u0: num, v0: num, u1: num, v1: num }, ['u0', 'v0', 'u1', 'v1'])\nconst INSTANCE = object({ ...TRANSFORM, color })\nconst COMMON: Record<string, Check> = {\n ...TRANSFORM,\n kind: str,\n name: str,\n part: str,\n material: str,\n color,\n uv: UV,\n instances: list(INSTANCE),\n minDetail: range(0, 1, true),\n tags: list(str),\n}\nconst vec2 = tuple(2)\nconst BEVEL: Check = (v) => (v === false ? null : object({ thickness: num, size: num, offset: num, segments: int(1) })(v))\nconst KINDS: Readonly<Record<string, { fields: Record<string, Check>; required?: string[] }>> = {\n group: { fields: {} },\n box: { fields: { size: tuple(3), segments: tuple(3, int(1)) } },\n cylinder: { fields: { radiusTop: num, radiusBottom: num, height: num, radialSegments: int(1), heightSegments: int(1), openEnded: bool, thetaStart: num, thetaLength: num } },\n cone: { fields: { radius: num, height: num, radialSegments: int(1), heightSegments: int(1), openEnded: bool, thetaStart: num, thetaLength: num } },\n sphere: { fields: { radius: num, widthSegments: int(3), heightSegments: int(2), phiStart: num, phiLength: num, thetaStart: num, thetaLength: num } },\n plane: { fields: { width: num, height: num, widthSegments: int(1), heightSegments: int(1) } },\n circle: { fields: { radius: num, segments: int(3), thetaStart: num, thetaLength: num } },\n ring: { fields: { innerRadius: num, outerRadius: num, thetaSegments: int(3), phiSegments: int(1), thetaStart: num, thetaLength: num } },\n lathe: { fields: { points: list(vec2, 2), segments: int(1), phiStart: num, phiLength: num }, required: ['points'] },\n torus: { fields: { radius: num, tube: num, radialSegments: int(1), tubularSegments: int(1), arc: num } },\n extrude: {\n fields: { shape: object({ outer: list(vec2, 3), holes: list(list(vec2, 3)) }, ['outer']), depth: num, steps: int(1), bevel: BEVEL },\n required: ['shape'],\n },\n difference: { fields: { size: tuple(3), subtract: list(object({ min: vec2, max: vec2 }, ['min', 'max'])) }, required: ['size', 'subtract'] },\n mesh: {\n fields: {\n positions: (v) => list(num)(v) ?? ((v as unknown[]).length % 3 === 0 ? null : 'expected a multiple of 3 numbers'),\n normals: list(num),\n uvs: list(num),\n indices: list(int(0)),\n },\n required: ['positions'],\n },\n point: { fields: { data: () => null } },\n custom: { fields: { generator: str, params: () => null }, required: ['generator'] },\n}\n\nfunction validateNode(node: unknown, path: string, errors: string[]): void {\n if (!node || typeof node !== 'object' || Array.isArray(node)) {\n errors.push(`${path}: expected a node object`)\n return\n }\n const record = node as Record<string, unknown>\n const spec = typeof record.kind === 'string' ? KINDS[record.kind] : undefined\n if (!spec) {\n errors.push(`${path}.kind: expected one of ${Object.keys(KINDS).join(', ')}`)\n return\n }\n for (const key of spec.required ?? []) if (record[key] === undefined) errors.push(`${path}.${key}: required`)\n for (const key of Object.keys(record)) {\n if (record[key] === undefined || key === 'children') continue\n const check = spec.fields[key] ?? COMMON[key]\n if (!check) {\n errors.push(`${path}.${key}: unknown field for a ${String(record.kind)}`)\n continue\n }\n const e = check(record[key])\n if (e) errors.push(`${path}.${key}: ${e}`)\n }\n const exclusive = ['matrix', 'heading'].filter((key) => record[key] !== undefined)\n if (record.matrix !== undefined && (record.position !== undefined || record.rotation !== undefined || record.scale !== undefined || record.heading !== undefined))\n errors.push(`${path}.matrix: cannot be combined with position, rotation, scale or heading`)\n else if (exclusive.includes('heading') && (record.rotation !== undefined || record.scale !== undefined)) errors.push(`${path}.heading: cannot be combined with rotation or scale`)\n if (record.children !== undefined) {\n if (record.kind !== 'group') errors.push(`${path}.children: only a group has children`)\n else if (!Array.isArray(record.children)) errors.push(`${path}.children: expected an array`)\n else record.children.forEach((child, index) => validateNode(child, `${path}.children[${index}]`, errors))\n }\n}\n\n/** Every rule of the JSON Schema, checked without a schema engine: one message per problem, with its path (`root.children[3].radialSegments: ...`). */\nexport function validateShape(schema: unknown): string[] {\n const errors: string[] = []\n if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return ['expected a shape object']\n const record = schema as Record<string, unknown>\n for (const key of Object.keys(record)) if (!['version', 'name', 'materials', 'root', 'lod', 'cullDistance'].includes(key) && record[key] !== undefined) errors.push(`${key}: unknown field`)\n if (record.version !== 1) errors.push('version: expected 1')\n if (record.name !== undefined && typeof record.name !== 'string') errors.push('name: expected a string')\n if (record.materials !== undefined) {\n if (!record.materials || typeof record.materials !== 'object' || Array.isArray(record.materials)) errors.push('materials: expected an object')\n else {\n const material = object({\n color,\n roughness: range(0, 1),\n metalness: range(0, 1),\n opacity: range(0, 1),\n transparent: bool,\n emissive: color,\n emissiveIntensity: (v) => (typeof v === 'number' && v >= 0 ? null : 'expected a number >= 0'),\n side: oneOf(['front', 'back', 'double']),\n flatShading: bool,\n vertexColors: bool,\n depthWrite: bool,\n })\n for (const [id, hint] of Object.entries(record.materials)) {\n const e = material(hint)\n if (e) errors.push(`materials.${id}: ${e}`)\n }\n }\n }\n if (record.lod !== undefined) {\n const e = list(object({ distance: (v) => (typeof v === 'number' && v >= 0 ? null : 'expected a number >= 0'), detail: range(0, 1, true), minSize: (v) => (typeof v === 'number' && v >= 0 ? null : 'expected a number >= 0'), minSegments: int(3) }, ['distance', 'detail']))(record.lod)\n if (e) errors.push(`lod${e.startsWith('[') ? '' : ': '}${e}`)\n }\n if (record.cullDistance !== undefined && !(typeof record.cullDistance === 'number' && record.cullDistance > 0)) errors.push('cullDistance: expected a number > 0')\n if (record.root === undefined) errors.push('root: required')\n else validateNode(record.root, 'root', errors)\n return errors\n}\n\n/** Throws with every problem when the schema is not valid. */\nexport function assertValidShape(schema: unknown): asserts schema is ShapeSchema {\n const errors = validateShape(schema)\n if (errors.length) throw new Error(`game-shapes: invalid shape\\n ${errors.slice(0, 20).join('\\n ')}${errors.length > 20 ? `\\n ... ${errors.length - 20} more` : ''}`)\n}\n","import { colorChannels, colorHex } from './color'\nimport { hashValue } from './hash'\nimport { asMat4, identity4, isAffine, matrixOf, multiply, normalMatrix, transformPoint } from './math'\nimport type { Matrix4 } from './math'\nimport { buildPrimitive, primitiveParameters } from './primitives'\nimport { canonicalJson } from './hash'\nimport { streamFor } from './random'\nimport { getGenerator } from './registry'\nimport { assertValidShape } from './schema'\nimport type {\n Bounds,\n GenerateOptions,\n InstancedBatch,\n NamedRange,\n PrimitiveBuffers,\n PrimitiveNode,\n ShapeBatch,\n ShapeBuffers,\n ShapeLevel,\n ShapeNode,\n ShapePart,\n ShapePoint,\n ShapeSchema,\n UvRect,\n} from './types'\n\nconst PRIMITIVE_KINDS = new Set(['box', 'cylinder', 'cone', 'sphere', 'plane', 'circle', 'ring', 'lathe', 'torus', 'extrude', 'difference', 'mesh'])\nexport const isPrimitiveNode = (node: ShapeNode): node is PrimitiveNode => PRIMITIVE_KINDS.has(node.kind)\n\n/** The options that change the output, as they enter the cache key. */\nexport function keyOptions(options: GenerateOptions = {}): Record<string, unknown> {\n return {\n seed: options.seed ?? 0,\n colors: options.colors ?? 'linear',\n indexed: options.indexed ?? true,\n materials: options.materials,\n fallbackMaterial: options.fallbackMaterial,\n materialAlias: options.materialAlias,\n instancing: options.instancing,\n levels: options.levels,\n }\n}\n\n/** The cache key of a shape under some options: 64-bit FNV-1a of the canonical schema text and the options that change the output. */\nexport const shapeKey = (schema: ShapeSchema, options: GenerateOptions = {}): string => hashValue({ schema, options: keyOptions(options) })\n\nconst IDENTITY_UV: UvRect = { u0: 0, v0: 0, u1: 1, v1: 1 }\n\nclass FloatList {\n array: Float32Array\n length = 0\n constructor(capacity = 256) {\n this.array = new Float32Array(capacity)\n }\n reserve(extra: number): void {\n if (this.length + extra <= this.array.length) return\n let capacity = this.array.length * 2\n while (capacity < this.length + extra) capacity *= 2\n const next = new Float32Array(capacity)\n next.set(this.array.subarray(0, this.length))\n this.array = next\n }\n done(): Float32Array {\n return this.array.slice(0, this.length)\n }\n}\nclass IndexList {\n array: Uint32Array\n length = 0\n constructor(capacity = 256) {\n this.array = new Uint32Array(capacity)\n }\n reserve(extra: number): void {\n if (this.length + extra <= this.array.length) return\n let capacity = this.array.length * 2\n while (capacity < this.length + extra) capacity *= 2\n const next = new Uint32Array(capacity)\n next.set(this.array.subarray(0, this.length))\n this.array = next\n }\n}\n\n/** Growing buffers of one (target, material). */\nclass Accumulator {\n readonly positions = new FloatList()\n readonly normals = new FloatList()\n readonly uvs = new FloatList(170)\n readonly colors = new FloatList()\n readonly indices = new IndexList()\n readonly ranges: NamedRange[] = []\n vertices = 0\n constructor(\n readonly material: string,\n readonly part: string | null,\n ) {}\n}\n\ninterface Target {\n readonly part: string | null\n readonly accs: Map<string, Accumulator>\n}\n\ninterface Settings {\n readonly colors: 'linear' | 'srgb' | 'none'\n readonly indexed: boolean\n readonly materials?: ReadonlySet<string>\n readonly fallback?: string\n readonly alias?: Readonly<Record<string, string>>\n readonly seed: number | string\n readonly generators?: GenerateOptions['generators']\n readonly isolatedGenerators?: boolean\n readonly autoMin?: number\n}\n\ninterface LevelSpec {\n readonly distance: number\n readonly detail: number\n readonly minSize?: number\n readonly minSegments?: number\n readonly index: number\n}\n\ninterface Inherited {\n readonly material: string\n readonly color: number\n readonly uv: UvRect\n readonly target: Target\n}\n\n/**\n * Bakes a template into an accumulator: positions by the world matrix, normals by its normal matrix and normalised, uvs into the atlas cell,\n * one colour for all, indices offset. The arithmetic is three's (`createMeshApi`, `BufferGeometry.applyMatrix4`), so the bits match.\n */\nfunction bake(acc: Accumulator, t: PrimitiveBuffers, world: readonly number[], rgb: readonly number[] | null, u: UvRect, indexed: boolean): void {\n const e = world\n const nm = normalMatrix(world)\n const affine = isAffine(world)\n const source = t.positions\n const count = source.length / 3\n const order: ArrayLike<number> | null = indexed ? null : t.indices ?? null\n const emitted = order ? order.length : count\n acc.positions.reserve(emitted * 3)\n acc.normals.reserve(emitted * 3)\n acc.uvs.reserve(emitted * 2)\n if (rgb) acc.colors.reserve(emitted * 3)\n const P = acc.positions.array\n const N = acc.normals.array\n const U = acc.uvs.array\n const C = acc.colors.array\n let pi = acc.positions.length\n let ni = acc.normals.length\n let ui = acc.uvs.length\n let ci = acc.colors.length\n const du = u.u1 - u.u0\n const dv = u.v1 - u.v0\n const tn = t.normals\n const tu = t.uvs\n for (let k = 0; k < emitted; k++) {\n const i = order ? (order[k] as number) : k\n const x = source[i * 3] as number\n const y = source[i * 3 + 1] as number\n const z = source[i * 3 + 2] as number\n if (affine) {\n P[pi++] = e[0]! * x + e[4]! * y + e[8]! * z + e[12]!\n P[pi++] = e[1]! * x + e[5]! * y + e[9]! * z + e[13]!\n P[pi++] = e[2]! * x + e[6]! * y + e[10]! * z + e[14]!\n } else {\n const p = transformPoint(e, x, y, z)\n P[pi++] = p[0]\n P[pi++] = p[1]\n P[pi++] = p[2]\n }\n const nx = tn[i * 3] as number\n const ny = tn[i * 3 + 1] as number\n const nz = tn[i * 3 + 2] as number\n const vx = nm[0]! * nx + nm[3]! * ny + nm[6]! * nz\n const vy = nm[1]! * nx + nm[4]! * ny + nm[7]! * nz\n const vz = nm[2]! * nx + nm[5]! * ny + nm[8]! * nz\n const s = 1 / (Math.sqrt(vx * vx + vy * vy + vz * vz) || 1)\n N[ni++] = vx * s\n N[ni++] = vy * s\n N[ni++] = vz * s\n U[ui++] = u.u0 + (tu[i * 2] as number) * du\n U[ui++] = u.v0 + (tu[i * 2 + 1] as number) * dv\n if (rgb) {\n C[ci++] = rgb[0]!\n C[ci++] = rgb[1]!\n C[ci++] = rgb[2]!\n }\n }\n acc.positions.length = pi\n acc.normals.length = ni\n acc.uvs.length = ui\n acc.colors.length = ci\n if (indexed) {\n const base = acc.vertices\n const idx = t.indices\n const n = idx ? idx.length : count\n acc.indices.reserve(n)\n const I = acc.indices.array\n let ii = acc.indices.length\n for (let k = 0; k < n; k++) I[ii++] = base + (idx ? (idx[k] as number) : k)\n acc.indices.length = ii\n }\n acc.vertices += emitted\n}\n\nfunction boundsOf(positions: Float32Array): Bounds {\n const min: [number, number, number] = [Infinity, Infinity, Infinity]\n const max: [number, number, number] = [-Infinity, -Infinity, -Infinity]\n for (let i = 0; i < positions.length; i += 3) {\n for (let axis = 0; axis < 3; axis++) {\n const v = positions[i + axis] as number\n if (v < min[axis]!) min[axis] = v\n if (v > max[axis]!) max[axis] = v\n }\n }\n return { min, max }\n}\n\nfunction finish(acc: Accumulator, indexed: boolean, withColors: boolean): ShapeBatch {\n const positions = acc.positions.done()\n const indices = indexed ? (acc.vertices > 65535 ? acc.indices.array.slice(0, acc.indices.length) : Uint16Array.from(acc.indices.array.subarray(0, acc.indices.length))) : undefined\n return {\n material: acc.material,\n part: acc.part,\n positions,\n normals: acc.normals.done(),\n uvs: acc.uvs.done(),\n ...(withColors ? { colors: acc.colors.done() } : {}),\n ...(indices ? { indices } : {}),\n ranges: acc.ranges,\n bounds: boundsOf(positions),\n }\n}\n\nconst radiusOf = (t: PrimitiveBuffers, world: readonly number[]): number => {\n let r = 0\n const p = t.positions\n for (let i = 0; i < p.length; i += 3) r = Math.max(r, Math.hypot(p[i] as number, p[i + 1] as number, p[i + 2] as number))\n const sx = Math.hypot(world[0]!, world[1]!, world[2]!)\n const sy = Math.hypot(world[4]!, world[5]!, world[6]!)\n const sz = Math.hypot(world[8]!, world[9]!, world[10]!)\n return r * Math.max(sx, sy, sz)\n}\n\ninterface LevelResult {\n readonly level: ShapeLevel\n readonly parts: ShapePart[]\n readonly points: ShapePoint[]\n readonly primitives: number\n}\n\n/** Expands a custom node into the node its generator returns (wrapped so that the custom node's own fields still apply). */\nfunction expandCustom(node: Extract<ShapeNode, { kind: 'custom' }>, settings: Settings, detail: number, path: string): ShapeNode {\n const generator = settings.generators?.[node.generator] ?? (settings.isolatedGenerators ? undefined : getGenerator(node.generator))\n if (!generator) throw new Error(`game-shapes: no generator \"${node.generator}\" (register it with registerGenerator, in the worker too) at ${path}`)\n const generated = generator(node.params, { random: streamFor(settings.seed, node.generator, path), seed: settings.seed, detail, path })\n const { kind: _kind, generator: _generator, params: _params, ...common } = node\n return { ...common, kind: 'group', children: [generated] }\n}\n\n/** The key two leaf primitives share when one can stand for the other as an instance. */\nconst leafKey = (node: PrimitiveNode, material: string, color: number, uv: UvRect, level: LevelSpec): string =>\n `${node.kind}|${canonicalJson(primitiveParameters(node, level))}|${material}|${color}|${uv.u0},${uv.v0},${uv.u1},${uv.v1}`\n\nfunction runLevel(schema: ShapeSchema, settings: Settings, level: LevelSpec): LevelResult {\n const bakeParts = level.index > 0\n const root: Target = { part: null, accs: new Map() }\n const targets: Target[] = [root]\n const partTargets = new Map<string, Target>()\n const parts: ShapePart[] = []\n const points: ShapePoint[] = []\n const instanced: InstancedBatch[] = []\n let primitives = 0\n const space = settings.colors === 'none' ? null : settings.colors\n const channels = new Map<number, readonly number[]>()\n const rgbOf = (hex: number): readonly number[] | null => {\n if (!space) return null\n let c = channels.get(hex)\n if (!c) {\n c = colorChannels(hex, space)\n channels.set(hex, c)\n }\n return c\n }\n const resolveMaterial = (id: string): string => {\n let out = id\n if (settings.materials && !settings.materials.has(out)) out = settings.fallback ?? out\n return settings.alias?.[out] ?? out\n }\n const accOf = (target: Target, material: string): Accumulator => {\n let acc = target.accs.get(material)\n if (!acc) {\n acc = new Accumulator(material, target.part)\n target.accs.set(material, acc)\n }\n return acc\n }\n\n // automatic instancing: count the leaves first\n const autoCounts = new Map<string, number>()\n const autoGroups = new Map<string, { node: PrimitiveNode; material: string; color: number; part: string | null; matrices: number[] }>()\n\n const count = (node: ShapeNode, inherited: { material: string; color: number; uv: UvRect; inPart: boolean }, path: string): void => {\n if (node.minDetail !== undefined && level.detail < node.minDetail) return\n if (node.kind === 'custom') return count(expandCustom(node, settings, level.detail, path), inherited, path)\n const next = {\n material: node.material !== undefined ? resolveMaterial(node.material) : inherited.material,\n color: node.color !== undefined ? colorHex(node.color) : inherited.color,\n uv: node.uv ?? inherited.uv,\n inPart: inherited.inPart || (node.part !== undefined && !bakeParts),\n }\n if (node.instances) return\n if (node.kind === 'group') {\n node.children?.forEach((child, index) => count(child, next, `${path}/${index}`))\n return\n }\n if (isPrimitiveNode(node) && node.name === undefined && !next.inPart) {\n const key = leafKey(node, next.material, next.color, next.uv, level)\n autoCounts.set(key, (autoCounts.get(key) ?? 0) + 1)\n }\n }\n const rootMaterial = resolveMaterial('default')\n if (settings.autoMin !== undefined) count(schema.root, { material: rootMaterial, color: 0xffffff, uv: IDENTITY_UV, inPart: false }, 'root')\n\n /** Draws a subtree into `collector` (a target) under `parent` (null: identity). */\n const visit = (node: ShapeNode, parent: Matrix4 | null, inherited: Inherited, path: string): void => {\n if (node.minDetail !== undefined && level.detail < node.minDetail) return\n if (node.kind === 'custom') return visit(expandCustom(node, settings, level.detail, path), parent, inherited, path)\n let target = inherited.target\n let parentWorld = parent\n if (node.part !== undefined) {\n if (bakeParts) {\n // far levels freeze the parts at rest: the part's pivot is relative to the root\n parentWorld = null\n } else {\n let partTarget = partTargets.get(node.part)\n if (!partTarget) {\n partTarget = { part: node.part, accs: new Map() }\n partTargets.set(node.part, partTarget)\n targets.push(partTarget)\n const { position, rotation, rotationOrder, scale, heading, matrix } = node\n parts.push({\n id: node.part,\n ...(position ? { position } : {}),\n ...(rotation ? { rotation } : {}),\n ...(rotationOrder ? { rotationOrder } : {}),\n ...(scale !== undefined ? { scale } : {}),\n ...(heading !== undefined ? { heading } : {}),\n ...(matrix ? { matrix } : { matrix: asMat4(matrixOf(node)) }),\n })\n }\n target = partTarget\n }\n }\n const isPart = node.part !== undefined && !bakeParts\n const local = isPart ? null : matrixOf(node)\n const world = isPart ? null : local ? (parentWorld ? multiply(parentWorld, local) : local) : parentWorld\n const next: Inherited = {\n material: node.material !== undefined ? resolveMaterial(node.material) : inherited.material,\n color: node.color !== undefined ? colorHex(node.color) : inherited.color,\n uv: node.uv ?? inherited.uv,\n target,\n }\n\n if (node.instances) {\n drawInstanced(node, parentWorld, next, path)\n return\n }\n\n const snapshot = node.name !== undefined ? new Map([...target.accs].map(([m, a]) => [m, { v: a.vertices, i: a.indices.length }])) : null\n\n if (node.kind === 'group') {\n node.children?.forEach((child, index) => visit(child, world, next, `${path}/${index}`))\n } else if (node.kind === 'point') {\n const p = node.position ?? [0, 0, 0]\n points.push({\n ...(node.name !== undefined ? { name: node.name } : {}),\n part: target.part,\n position: parent ? transformPoint(parent, p[0], p[1], p[2]) : [p[0], p[1], p[2]],\n ...(node.data !== undefined ? { data: node.data } : {}),\n ...(node.tags ? { tags: node.tags } : {}),\n })\n } else if (isPrimitiveNode(node)) {\n const template = buildPrimitive(node, level)\n const m = world ?? identity4()\n if (level.minSize !== undefined && radiusOf(template, m) < level.minSize) return\n primitives++\n if (settings.autoMin !== undefined && node.name === undefined && target.part === null) {\n const key = leafKey(node, next.material, next.color, next.uv, level)\n if ((autoCounts.get(key) ?? 0) >= settings.autoMin) {\n let group = autoGroups.get(key)\n if (!group) {\n group = { node, material: next.material, color: next.color, part: null, matrices: [] }\n autoGroups.set(key, group)\n }\n group.matrices.push(...m)\n return\n }\n }\n bake(accOf(target, next.material), template, m, rgbOf(next.color), next.uv, settings.indexed)\n }\n\n if (snapshot) {\n for (const [material, acc] of target.accs) {\n const before = snapshot.get(material) ?? { v: 0, i: 0 }\n if (acc.vertices === before.v) continue\n acc.ranges.push({\n name: node.name as string,\n ...(node.tags ? { tags: node.tags } : {}),\n vertexStart: before.v,\n vertexCount: acc.vertices - before.v,\n indexStart: before.i,\n indexCount: acc.indices.length - before.i,\n })\n }\n }\n }\n\n const drawInstanced = (node: ShapeNode, parentWorld: Matrix4 | null, inherited: Inherited, path: string): void => {\n const collector: Target = { part: inherited.target.part, accs: new Map() }\n const { instances, ...single } = node as ShapeNode & { instances: NonNullable<ShapeNode['instances']> }\n const before = parts.length\n visit(single as ShapeNode, null, { ...inherited, target: collector }, path)\n if (parts.length !== before) throw new Error(`game-shapes: an instanced node cannot hold parts (${path})`)\n const matrices = new Float32Array(instances.length * 16)\n const hasColors = instances.some((instance) => instance.color !== undefined)\n const colors = hasColors ? new Float32Array(instances.length * 3) : undefined\n instances.forEach((instance, index) => {\n const local = matrixOf(instance)\n const m = local ? (parentWorld ? multiply(parentWorld, local) : local) : parentWorld ?? identity4()\n matrices.set(m, index * 16)\n if (colors) colors.set(colorChannels(colorHex(instance.color), space ?? 'srgb'), index * 3)\n })\n for (const acc of collector.accs.values()) {\n const batch = finish(acc, settings.indexed, false)\n instanced.push({\n material: acc.material,\n part: collector.part,\n ...(node.name !== undefined ? { name: node.name } : {}),\n geometry: { type: 'BufferGeometry', parameters: {}, positions: batch.positions, normals: batch.normals, uvs: batch.uvs, ...(batch.indices ? { indices: batch.indices } : {}), groups: [] },\n matrices: matrices.slice(),\n ...(colors ? { colors: colors.slice() } : {}),\n count: instances.length,\n })\n }\n }\n\n visit(schema.root, null, { material: rootMaterial, color: 0xffffff, uv: IDENTITY_UV, target: root }, 'root')\n\n for (const group of autoGroups.values()) {\n const template = buildPrimitive(group.node, level)\n const n = group.matrices.length / 16\n const rgb = colorChannels(group.color, space ?? 'srgb')\n const colors = new Float32Array(n * 3)\n for (let i = 0; i < n; i++) colors.set(rgb, i * 3)\n instanced.push({\n material: group.material,\n part: null,\n geometry: { type: template.type, parameters: template.parameters, positions: template.positions.slice(), normals: template.normals.slice(), uvs: template.uvs.slice(), ...(template.indices ? { indices: template.indices.slice() } : {}), groups: template.groups.map((g) => ({ ...g })) },\n matrices: new Float32Array(group.matrices),\n ...(space ? { colors } : {}),\n count: n,\n })\n }\n\n const batches: ShapeBatch[] = []\n for (const target of targets) for (const acc of target.accs.values()) if (acc.vertices > 0 && (!settings.indexed || acc.indices.length > 0)) batches.push(finish(acc, settings.indexed, space !== null))\n return { level: { distance: level.distance, detail: level.detail, batches, instanced }, parts, points, primitives }\n}\n\n/**\n * Turns a shape into buffers: one batch per (part, material) per level, baked; instanced nodes as instance batches; parts with their pivots;\n * points. Pure: the same schema and options give the same bytes, here or in a worker.\n */\nexport function generate(schema: ShapeSchema, options: GenerateOptions = {}): ShapeBuffers {\n if (options.validate !== false) assertValidShape(schema)\n const settings: Settings = {\n colors: options.colors ?? 'linear',\n indexed: options.indexed ?? true,\n ...(options.materials ? { materials: new Set(options.materials) } : {}),\n ...(options.fallbackMaterial !== undefined ? { fallback: options.fallbackMaterial } : {}),\n ...(options.materialAlias ? { alias: options.materialAlias } : {}),\n seed: options.seed ?? 0,\n ...(options.generators ? { generators: options.generators } : {}),\n ...(options.isolatedGenerators ? { isolatedGenerators: true } : {}),\n ...(options.instancing ? { autoMin: Math.max(2, options.instancing.minCount) } : {}),\n }\n const specs: LevelSpec[] = [{ distance: 0, detail: 1, index: 0 }, ...(schema.lod ?? []).map((rule, index) => ({ ...rule, index: index + 1 }))]\n const wanted = options.levels ? specs.filter((spec) => options.levels!.includes(spec.index)) : specs\n let parts: ShapePart[] = []\n let points: ShapePoint[] = []\n let primitives = 0\n const levels: ShapeLevel[] = []\n for (const spec of wanted) {\n const result = runLevel(schema, settings, spec)\n if (spec.index === 0 || levels.length === 0) {\n parts = result.parts\n points = result.points\n primitives = result.primitives\n }\n levels.push(result.level)\n }\n const first = levels[0]\n const vertices = first ? first.batches.reduce((sum, batch) => sum + batch.positions.length / 3, 0) : 0\n const triangles = first\n ? first.batches.reduce((sum, batch) => sum + (batch.indices ? batch.indices.length : batch.positions.length / 3) / 3, 0) +\n first.instanced.reduce((sum, batch) => sum + ((batch.geometry.indices ? batch.geometry.indices.length : batch.geometry.positions.length / 3) / 3) * batch.count, 0)\n : 0\n return {\n key: options.key === false ? '' : options.key ?? shapeKey(schema, options),\n ...(schema.name !== undefined ? { name: schema.name } : {}),\n levels,\n parts,\n points,\n ...(schema.materials ? { materials: schema.materials } : {}),\n ...(schema.cullDistance !== undefined ? { cullDistance: schema.cullDistance } : {}),\n stats: { vertices, triangles, batches: first ? first.batches.length : 0, instances: first ? first.instanced.reduce((sum, batch) => sum + batch.count, 0) : 0, primitives },\n }\n}\n","import { normalMatrix, transformPoint } from './math'\nimport type { InstancedBatch, PrimitiveBuffers, ShapeBatch, ShapeBuffers } from './types'\n\n/** One piece for `mergeTransformed`: arrays (any may be missing) and the matrix that takes them into the merged space. */\nexport interface TransformedPiece {\n readonly positions: ArrayLike<number>\n readonly normals?: ArrayLike<number>\n readonly uvs?: ArrayLike<number>\n readonly indices?: ArrayLike<number> | null\n /** Column-major 4 x 4. */\n readonly matrix: readonly number[]\n}\n\nexport interface MergedArrays {\n readonly positions: Float32Array\n readonly normals: Float32Array\n readonly uvs: Float32Array\n}\n\n/**\n * Joins pieces into one non-indexed set of arrays, each piece de-indexed, its missing normals and uvs zero-filled, its positions by\n * `Vector3.applyMatrix4` and its normals by the normal matrix and normalised: what three's `toNonIndexed` + `applyMatrix4` + a join do,\n * bit for bit (the port of `mergeByMaterial`).\n */\nexport function mergeTransformed(pieces: readonly TransformedPiece[]): MergedArrays {\n let total = 0\n for (const piece of pieces) total += piece.indices ? piece.indices.length : piece.positions.length / 3\n const positions = new Float32Array(total * 3)\n const normals = new Float32Array(total * 3)\n const uvs = new Float32Array(total * 2)\n let at = 0\n for (const piece of pieces) {\n const e = piece.matrix\n const nm = normalMatrix(e)\n const count = piece.indices ? piece.indices.length : piece.positions.length / 3\n // de-index into Float32 first, as toNonIndexed does, then transform the stored values\n const p = new Float32Array(count * 3)\n const n = new Float32Array(count * 3)\n for (let k = 0; k < count; k++) {\n const i = piece.indices ? (piece.indices[k] as number) : k\n for (let c = 0; c < 3; c++) {\n p[k * 3 + c] = piece.positions[i * 3 + c] as number\n if (piece.normals) n[k * 3 + c] = piece.normals[i * 3 + c] as number\n }\n if (piece.uvs) {\n uvs[(at + k) * 2] = piece.uvs[i * 2] as number\n uvs[(at + k) * 2 + 1] = piece.uvs[i * 2 + 1] as number\n }\n }\n for (let k = 0; k < count; k++) {\n const q = transformPoint(e, p[k * 3] as number, p[k * 3 + 1] as number, p[k * 3 + 2] as number)\n positions[(at + k) * 3] = q[0]\n positions[(at + k) * 3 + 1] = q[1]\n positions[(at + k) * 3 + 2] = q[2]\n const x = n[k * 3] as number\n const y = n[k * 3 + 1] as number\n const z = n[k * 3 + 2] as number\n const vx = nm[0]! * x + nm[3]! * y + nm[6]! * z\n const vy = nm[1]! * x + nm[4]! * y + nm[7]! * z\n const vz = nm[2]! * x + nm[5]! * y + nm[8]! * z\n const s = 1 / (Math.sqrt(vx * vx + vy * vy + vz * vz) || 1)\n normals[(at + k) * 3] = vx * s\n normals[(at + k) * 3 + 1] = vy * s\n normals[(at + k) * 3 + 2] = vz * s\n }\n at += count\n }\n return { positions, normals, uvs }\n}\n\nconst primitiveArrays = (p: PrimitiveBuffers): ArrayBuffer[] => [p.positions, p.normals, p.uvs, ...(p.indices ? [p.indices] : [])].map((a) => a.buffer as ArrayBuffer)\nconst batchArrays = (b: ShapeBatch): ArrayBuffer[] => [b.positions, b.normals, b.uvs, ...(b.colors ? [b.colors] : []), ...(b.indices ? [b.indices] : [])].map((a) => a.buffer as ArrayBuffer)\nconst instancedArrays = (b: InstancedBatch): ArrayBuffer[] => [...primitiveArrays(b.geometry), b.matrices.buffer as ArrayBuffer, ...(b.colors ? [b.colors.buffer as ArrayBuffer] : [])]\n\n/** Every `ArrayBuffer` of a result, for `postMessage(buffers, transferablesOf(buffers))`. */\nexport function transferablesOf(buffers: ShapeBuffers): ArrayBuffer[] {\n const out = new Set<ArrayBuffer>()\n for (const level of buffers.levels) {\n for (const batch of level.batches) for (const a of batchArrays(batch)) out.add(a)\n for (const batch of level.instanced) for (const a of instancedArrays(batch)) out.add(a)\n }\n return [...out]\n}\n\nconst copyPrimitive = (p: PrimitiveBuffers): PrimitiveBuffers => ({\n ...p,\n positions: p.positions.slice(),\n normals: p.normals.slice(),\n uvs: p.uvs.slice(),\n ...(p.indices ? { indices: p.indices.slice() } : {}),\n groups: p.groups.map((g) => ({ ...g })),\n})\n\n/** A deep copy whose arrays are its own (cached results are shared and read-only; copy before deforming). */\nexport function cloneBuffers(buffers: ShapeBuffers): ShapeBuffers {\n return {\n ...buffers,\n levels: buffers.levels.map((level) => ({\n ...level,\n batches: level.batches.map((b) => ({\n ...b,\n positions: b.positions.slice(),\n normals: b.normals.slice(),\n uvs: b.uvs.slice(),\n ...(b.colors ? { colors: b.colors.slice() } : {}),\n ...(b.indices ? { indices: b.indices.slice() } : {}),\n ranges: b.ranges.map((r) => ({ ...r })),\n bounds: { min: [...b.bounds.min], max: [...b.bounds.max] },\n })),\n instanced: level.instanced.map((b) => ({ ...b, geometry: copyPrimitive(b.geometry), matrices: b.matrices.slice(), ...(b.colors ? { colors: b.colors.slice() } : {}) })),\n })),\n }\n}\n\n/** Bytes held by the arrays of a result (what the cache counts). */\nexport function byteSizeOf(buffers: ShapeBuffers): number {\n return transferablesOf(buffers).reduce((sum, buffer) => sum + buffer.byteLength, 0)\n}\n","import { generate } from './generate'\nimport { transferablesOf } from './merge'\nimport type { GenerateOptions, ShapeBuffers, ShapeGenerator, ShapeSchema } from './types'\n\n/** A job the pool posts: `options` without `generators` (functions do not cross threads; register them in the worker script). */\nexport interface ShapeJob {\n readonly id: number\n readonly schema: ShapeSchema\n readonly options?: Omit<GenerateOptions, 'generators'>\n}\n\nexport type ShapeReply = { readonly id: number; readonly ok: true; readonly buffers: ShapeBuffers } | { readonly id: number; readonly ok: false; readonly error: string }\n\n/** What a worker scope looks like: `self` in a browser worker, a `worker_threads` `parentPort` wrapper in Node, or an in-thread fake. */\nexport interface WorkerScopeLike {\n onmessage: ((event: { data: unknown }) => void) | null\n postMessage(message: unknown, transfer?: ArrayBuffer[]): void\n}\n\nexport interface ShapeWorkerOptions {\n /** Generators of the `custom` nodes, the same ones the main thread registers. */\n readonly generators?: Readonly<Record<string, ShapeGenerator>>\n /** Transfer the arrays back (default true: zero copy, the worker keeps nothing). */\n readonly transfer?: boolean\n}\n\n/**\n * The worker side of `createShapePool`: answers each job with `generate(schema, options)`, transferring every array.\n *\n * ```ts\n * // shape-worker.ts\n * import { installShapeWorker } from '@adatechnology/game-shapes/worker'\n * installShapeWorker(self as never, { generators: myGenerators })\n * ```\n */\nexport function installShapeWorker(scope: WorkerScopeLike, options: ShapeWorkerOptions = {}): void {\n const transfer = options.transfer !== false\n scope.onmessage = (event) => {\n const job = event.data as ShapeJob\n try {\n const buffers = generate(job.schema, { ...(job.options ?? {}), ...(options.generators ? { generators: options.generators } : {}) })\n const reply: ShapeReply = { id: job.id, ok: true, buffers }\n scope.postMessage(reply, transfer ? transferablesOf(buffers) : [])\n } catch (error) {\n const reply: ShapeReply = { id: job.id, ok: false, error: error instanceof Error ? error.message : String(error) }\n scope.postMessage(reply)\n }\n }\n}\n","import type { PrimitiveBuffers, ShapeBatch } from '@adatechnology/game-shapes'\nimport type * as THREE_NS from 'three'\n\nexport type Three = typeof THREE_NS\n\nexport interface ToGeometryOptions {\n /** Copy the arrays into the attributes (default true). Without a copy the geometry shares the buffers (a cached result is read-only: do not deform it). */\n readonly copy?: boolean\n}\n\n/** The order in which three's generator of each type sets its attributes (toJSON and attribute iteration keep it). */\nconst UV_BEFORE_NORMAL = new Set(['LatheGeometry', 'ExtrudeGeometry'])\n/** Types whose constructor sets the index before the attributes. */\nconst INDEX_FIRST = new Set(['BoxGeometry', 'CylinderGeometry', 'ConeGeometry', 'SphereGeometry', 'PlaneGeometry', 'CircleGeometry', 'RingGeometry', 'LatheGeometry', 'TorusGeometry'])\n\nconst isPrimitive = (value: PrimitiveBuffers | ShapeBatch): value is PrimitiveBuffers => typeof (value as PrimitiveBuffers).type === 'string'\n\n/** Three's own parameters object for a type (lathe points as `Vector2`). */\nfunction threeParameters(THREE: Three, p: PrimitiveBuffers): Record<string, unknown> {\n if (p.type === 'LatheGeometry') {\n const points = (p.parameters.points as Array<{ x: number; y: number }>).map(({ x, y }) => new THREE.Vector2(x, y))\n return { ...p.parameters, points }\n }\n return { ...p.parameters }\n}\n\n/**\n * A `BufferGeometry` from game-shapes buffers. A primitive becomes an instance of the three class it reproduces (`type`, `parameters`, prototype:\n * `instanceof THREE.CylinderGeometry`, `clone()` and `toJSON()` behave as three's), with the same attributes, index and groups. A batch gets\n * `position`, `normal`, `uv`, `color` (when present) and its index.\n */\nexport function toGeometry(THREE: Three, source: PrimitiveBuffers | ShapeBatch, options: ToGeometryOptions = {}): THREE_NS.BufferGeometry {\n const copy = options.copy !== false\n const primitive = isPrimitive(source) ? source : null\n const ThreeClass = primitive && primitive.type !== 'BufferGeometry' ? ((THREE as unknown as Record<string, unknown>)[primitive.type] as (new () => THREE_NS.BufferGeometry) | undefined) : undefined\n // the BufferGeometry constructor only, with the prototype of the class it stands for\n const geometry = (ThreeClass ? Reflect.construct(THREE.BufferGeometry, [], ThreeClass) : new THREE.BufferGeometry()) as THREE_NS.BufferGeometry & { parameters?: Record<string, unknown> }\n if (primitive && ThreeClass) {\n Object.assign(geometry, { type: primitive.type, parameters: threeParameters(THREE, primitive) })\n }\n const attribute = (array: Float32Array, size: number): THREE_NS.BufferAttribute => (copy ? new THREE.Float32BufferAttribute(array, size) : new THREE.BufferAttribute(array, size))\n const index = source.indices\n const setIndex = (): void => {\n if (!index) return\n geometry.setIndex(copy ? (index instanceof Uint32Array ? new THREE.Uint32BufferAttribute(index, 1) : new THREE.Uint16BufferAttribute(index, 1)) : new THREE.BufferAttribute(index, 1))\n }\n const type = primitive?.type ?? 'BufferGeometry'\n if (INDEX_FIRST.has(type)) setIndex()\n geometry.setAttribute('position', attribute(source.positions, 3))\n if (UV_BEFORE_NORMAL.has(type)) {\n geometry.setAttribute('uv', attribute(source.uvs, 2))\n geometry.setAttribute('normal', attribute(source.normals, 3))\n } else {\n geometry.setAttribute('normal', attribute(source.normals, 3))\n geometry.setAttribute('uv', attribute(source.uvs, 2))\n }\n if (!primitive && (source as ShapeBatch).colors) geometry.setAttribute('color', attribute((source as ShapeBatch).colors as Float32Array, 3))\n if (!INDEX_FIRST.has(type)) setIndex()\n if (primitive) for (const group of primitive.groups) geometry.addGroup(group.start, group.count, group.materialIndex)\n return geometry\n}\n","import { box, circle, cone, cylinder, extrude, lathe, plane, ring, sphere, torus } from '@adatechnology/game-shapes'\nimport type { BevelSpec, PrimitiveBuffers, PrimitiveNode, ShapeOutline, Vec2 } from '@adatechnology/game-shapes'\nimport { buildPrimitive } from '@adatechnology/game-shapes'\nimport type * as THREE_NS from 'three'\n\nimport { toGeometry } from './geometry'\nimport type { Three } from './geometry'\n\n/** The geometry constructors `shapeGeometries` replaces. */\nexport const SHAPE_GEOMETRY_TYPES = ['BoxGeometry', 'CylinderGeometry', 'ConeGeometry', 'SphereGeometry', 'PlaneGeometry', 'CircleGeometry', 'RingGeometry', 'LatheGeometry', 'TorusGeometry', 'ExtrudeGeometry'] as const\n\ntype Curve = { isLineCurve?: boolean; v1?: { x: number; y: number }; v2?: { x: number; y: number } }\ntype PathLike = { curves: Curve[]; autoClose?: boolean; holes?: PathLike[] }\n\n/** The point list of a path made only of contiguous `LineCurve`s (what `moveTo` + `lineTo` builds), or null. */\nfunction linePoints(path: PathLike): Vec2[] | null {\n if (path.autoClose || path.curves.length === 0) return null\n const points: Vec2[] = []\n for (const [index, curve] of path.curves.entries()) {\n if (!curve.isLineCurve || !curve.v1 || !curve.v2) return null\n if (index === 0) points.push([curve.v1.x, curve.v1.y])\n else {\n const last = points[points.length - 1] as Vec2\n if (last[0] !== curve.v1.x || last[1] !== curve.v1.y) return null\n }\n points.push([curve.v2.x, curve.v2.y])\n }\n return points\n}\n\ntype ExtrudeOptions = { depth?: number; steps?: number; bevelEnabled?: boolean; bevelThickness?: number; bevelSize?: number; bevelOffset?: number; bevelSegments?: number; extrudePath?: unknown; UVGenerator?: unknown; curveSegments?: number }\n\n/** An extrusion game-shapes can make exactly: one straight-edged shape, no path, the world uvs. */\nfunction extrudeOf(shapes: unknown, options: ExtrudeOptions = {}): PrimitiveBuffers | null {\n if (Array.isArray(shapes) || !shapes || options.extrudePath !== undefined || options.UVGenerator !== undefined) return null\n const shape = shapes as PathLike\n const outer = linePoints(shape)\n if (!outer) return null\n const holes: Vec2[][] = []\n for (const hole of shape.holes ?? []) {\n const points = linePoints(hole)\n if (!points) return null\n holes.push(points)\n }\n const outline: ShapeOutline = holes.length ? { outer, holes } : { outer }\n const bevel: false | BevelSpec =\n options.bevelEnabled === false\n ? false\n : {\n ...(options.bevelThickness !== undefined ? { thickness: options.bevelThickness } : {}),\n ...(options.bevelSize !== undefined ? { size: options.bevelSize } : {}),\n ...(options.bevelOffset !== undefined ? { offset: options.bevelOffset } : {}),\n ...(options.bevelSegments !== undefined ? { segments: options.bevelSegments } : {}),\n }\n return extrude(outline, options.depth, options.steps, bevel)\n}\n\nexport interface ShapeGeometriesOptions {\n /** Go through the template cache of game-shapes (default true): the same primitive is generated once per process. */\n readonly cache?: boolean\n}\n\nconst namespaces = new WeakMap<object, Map<string, Three>>()\n/** The namespaces this module made: wrapping one again gives it back (a package handing its `T` to another). */\nconst shims = new WeakSet<object>()\n\n/**\n * A three namespace whose `BoxGeometry`, `CylinderGeometry`, `ConeGeometry`, `SphereGeometry`, `PlaneGeometry`, `CircleGeometry`, `RingGeometry`,\n * `LatheGeometry`, `TorusGeometry` and (straight-edged) `ExtrudeGeometry` are made by game-shapes, bit-identical to three's and instances of three's\n * classes; everything else is three's own. Hand it to code ported from the teams as `T`, so it keeps reading `new T.CylinderGeometry(...)`.\n * An extrusion with curves, several shapes, a path or a custom uv generator falls back to three.\n */\nexport function shapeGeometries(THREE: Three, options: ShapeGeometriesOptions = {}): Three {\n if (shims.has(THREE)) return THREE\n const cache = options.cache !== false\n const key = cache ? 'cache' : 'nocache'\n let byKey = namespaces.get(THREE)\n if (!byKey) {\n byKey = new Map()\n namespaces.set(THREE, byKey)\n }\n const existing = byKey.get(key)\n if (existing) return existing\n const made = (p: PrimitiveBuffers): THREE_NS.BufferGeometry => toGeometry(THREE, p)\n const viaCache = (node: PrimitiveNode, direct: () => PrimitiveBuffers): PrimitiveBuffers => (cache ? buildPrimitive(node) : direct())\n const defined = <T extends object>(o: T): T => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined)) as T\n const constructors: Record<(typeof SHAPE_GEOMETRY_TYPES)[number], (...args: any[]) => THREE_NS.BufferGeometry> = {\n BoxGeometry: (width?: number, height?: number, depth?: number, ws = 1, hs = 1, ds = 1) =>\n made(viaCache({ kind: 'box', size: [width ?? 1, height ?? 1, depth ?? 1], segments: [ws, hs, ds] }, () => box(width, height, depth, ws, hs, ds))),\n CylinderGeometry: (radiusTop?: number, radiusBottom?: number, height?: number, radialSegments?: number, heightSegments?: number, openEnded?: boolean, thetaStart?: number, thetaLength?: number) =>\n made(viaCache({ kind: 'cylinder', ...defined({ radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength }) }, () => cylinder(radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength))),\n ConeGeometry: (radius?: number, height?: number, radialSegments?: number, heightSegments?: number, openEnded?: boolean, thetaStart?: number, thetaLength?: number) =>\n made(viaCache({ kind: 'cone', ...defined({ radius, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength }) }, () => cone(radius, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength))),\n SphereGeometry: (radius?: number, widthSegments?: number, heightSegments?: number, phiStart?: number, phiLength?: number, thetaStart?: number, thetaLength?: number) =>\n made(viaCache({ kind: 'sphere', ...defined({ radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength }) }, () => sphere(radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength))),\n PlaneGeometry: (width?: number, height?: number, widthSegments?: number, heightSegments?: number) =>\n made(viaCache({ kind: 'plane', ...defined({ width, height, widthSegments, heightSegments }) }, () => plane(width, height, widthSegments, heightSegments))),\n CircleGeometry: (radius?: number, segments?: number, thetaStart?: number, thetaLength?: number) =>\n made(viaCache({ kind: 'circle', ...defined({ radius, segments, thetaStart, thetaLength }) }, () => circle(radius, segments, thetaStart, thetaLength))),\n RingGeometry: (innerRadius?: number, outerRadius?: number, thetaSegments?: number, phiSegments?: number, thetaStart?: number, thetaLength?: number) =>\n made(viaCache({ kind: 'ring', ...defined({ innerRadius, outerRadius, thetaSegments, phiSegments, thetaStart, thetaLength }) }, () => ring(innerRadius, outerRadius, thetaSegments, phiSegments, thetaStart, thetaLength))),\n LatheGeometry: (points?: Array<{ x: number; y: number }>, segments?: number, phiStart?: number, phiLength?: number) => {\n if (!points) return new THREE.LatheGeometry(undefined, segments, phiStart, phiLength)\n const profile = points.map((p) => [p.x, p.y] as const)\n return made(viaCache({ kind: 'lathe', points: profile, ...defined({ segments, phiStart, phiLength }) }, () => lathe(profile, segments, phiStart, phiLength)))\n },\n TorusGeometry: (radius?: number, tube?: number, radialSegments?: number, tubularSegments?: number, arc?: number) =>\n made(viaCache({ kind: 'torus', ...defined({ radius, tube, radialSegments, tubularSegments, arc }) }, () => torus(radius, tube, radialSegments, tubularSegments, arc))),\n ExtrudeGeometry: (shapes?: unknown, extrudeOptions?: ExtrudeOptions) => {\n const buffers = shapes === undefined ? null : extrudeOf(shapes, extrudeOptions)\n if (!buffers) return new THREE.ExtrudeGeometry(shapes as THREE_NS.Shape, extrudeOptions as THREE_NS.ExtrudeGeometryOptions)\n const geometry = toGeometry(THREE, buffers) as THREE_NS.BufferGeometry & { parameters: unknown }\n geometry.parameters = { shapes, options: extrudeOptions ?? {} }\n return geometry\n },\n }\n const namespace: Record<string, unknown> = { ...(THREE as unknown as Record<string, unknown>) }\n for (const type of SHAPE_GEOMETRY_TYPES) {\n const make = constructors[type]\n // callable with `new` (the returned object wins), and `instanceof` checks three's prototype\n const Constructor = function (this: unknown, ...args: unknown[]) {\n return make(...args)\n } as unknown as { prototype: unknown }\n Constructor.prototype = (THREE as unknown as Record<string, { prototype: unknown }>)[type]!.prototype\n Object.defineProperty(Constructor, 'name', { value: type })\n namespace[type] = Constructor\n }\n const result = namespace as unknown as Three\n byKey.set(key, result)\n shims.add(result)\n return result\n}\n","import { colorHex } from '@adatechnology/game-shapes'\nimport type { MaterialHint, ShapeBuffers, ShapeLevel, ShapePart, ShapePoint } from '@adatechnology/game-shapes'\nimport type * as THREE_NS from 'three'\n\nimport { toGeometry } from './geometry'\nimport type { Three } from './geometry'\n\n/** A `MeshStandardMaterial` per material id from the schema's hints (vertex colours on unless the hint says otherwise). */\nexport function createMaterials(THREE: Three, hints: Readonly<Record<string, MaterialHint>> = {}, ids: Iterable<string> = []): Record<string, THREE_NS.Material> {\n const out: Record<string, THREE_NS.Material> = {}\n const all = new Set([...Object.keys(hints), ...ids])\n for (const id of all) {\n const hint = hints[id] ?? {}\n const parameters: THREE_NS.MeshStandardMaterialParameters = { vertexColors: hint.vertexColors ?? true }\n if (hint.color !== undefined) parameters.color = colorHex(hint.color)\n if (hint.roughness !== undefined) parameters.roughness = hint.roughness\n if (hint.metalness !== undefined) parameters.metalness = hint.metalness\n if (hint.opacity !== undefined) parameters.opacity = hint.opacity\n if (hint.transparent !== undefined) parameters.transparent = hint.transparent\n if (hint.emissive !== undefined) parameters.emissive = colorHex(hint.emissive)\n if (hint.emissiveIntensity !== undefined) parameters.emissiveIntensity = hint.emissiveIntensity\n if (hint.flatShading !== undefined) parameters.flatShading = hint.flatShading\n if (hint.depthWrite !== undefined) parameters.depthWrite = hint.depthWrite\n if (hint.side !== undefined) parameters.side = hint.side === 'double' ? THREE.DoubleSide : hint.side === 'back' ? THREE.BackSide : THREE.FrontSide\n const material = new THREE.MeshStandardMaterial(parameters)\n material.name = id\n out[id] = material\n }\n return out\n}\n\nexport interface ToObjectOptions {\n /** Materials by id; missing ids get one from the schema hints (`createMaterials`). */\n readonly materials?: Readonly<Record<string, THREE_NS.Material>>\n /** The material of one draw (wins over `materials`): per material id and part (`null` for the static body). */\n readonly materialFor?: (material: string, part: string | null) => THREE_NS.Material | undefined\n /** Cast and receive shadows (default true). */\n readonly shadows?: boolean\n /** Copy the arrays into the attributes (default true; false shares them, read-only). */\n readonly copy?: boolean\n /** Name of the root. */\n readonly name?: string\n}\n\nexport interface ShapeObject {\n /** A `Group`, or a `LOD` when the shape has levels or a cull distance. */\n readonly object: THREE_NS.Object3D\n /** The level 0 group (the root itself without LOD). */\n readonly near: THREE_NS.Group\n /** Part groups of level 0 by id, at their pivots; animate these. */\n readonly parts: Record<string, THREE_NS.Group>\n readonly meshes: THREE_NS.Mesh[]\n readonly instanced: THREE_NS.InstancedMesh[]\n readonly points: readonly ShapePoint[]\n readonly materials: Record<string, THREE_NS.Material>\n}\n\nfunction placePart(THREE: Three, group: THREE_NS.Group, part: ShapePart): void {\n if (part.matrix && part.position === undefined && part.rotation === undefined && part.scale === undefined && part.heading === undefined) {\n new THREE.Matrix4().fromArray(part.matrix as number[]).decompose(group.position, group.quaternion, group.scale)\n return\n }\n if (part.position) group.position.set(part.position[0], part.position[1], part.position[2])\n if (part.heading !== undefined) group.rotation.y = part.heading\n else if (part.rotation) group.rotation.set(part.rotation[0], part.rotation[1], part.rotation[2], part.rotationOrder ?? 'XYZ')\n if (part.scale !== undefined) {\n if (typeof part.scale === 'number') group.scale.setScalar(part.scale)\n else group.scale.set(part.scale[0], part.scale[1], part.scale[2])\n }\n group.updateMatrix()\n}\n\n/**\n * The three objects of a shape: one mesh per batch (`static:<material>` or `part:<id>`, `userData.material`, `userData.part`), part groups at their\n * pivots, an `InstancedMesh` per instanced batch, and a `LOD` (levels at their distances, nothing from `cullDistance`) when there are levels.\n */\nexport function toObject3D(THREE: Three, buffers: ShapeBuffers, options: ToObjectOptions = {}): ShapeObject {\n const shadows = options.shadows !== false\n const ids = new Set<string>()\n for (const level of buffers.levels) {\n for (const b of level.batches) ids.add(b.material)\n for (const b of level.instanced) ids.add(b.material)\n }\n const made = createMaterials(THREE, buffers.materials, [...ids].filter((id) => !options.materials?.[id]))\n const materials: Record<string, THREE_NS.Material> = { ...made, ...(options.materials ?? {}) }\n const materialOf = (id: string, part: string | null): THREE_NS.Material => options.materialFor?.(id, part) ?? (materials[id] as THREE_NS.Material)\n const meshes: THREE_NS.Mesh[] = []\n const instanced: THREE_NS.InstancedMesh[] = []\n\n const build = (level: ShapeLevel, withParts: boolean): { group: THREE_NS.Group; parts: Record<string, THREE_NS.Group> } => {\n const group = new THREE.Group()\n group.name = options.name ?? buffers.name ?? 'shape'\n const parts: Record<string, THREE_NS.Group> = {}\n if (withParts) {\n for (const part of buffers.parts) {\n const g = new THREE.Group()\n g.name = `part:${part.id}`\n g.userData.part = part.id\n placePart(THREE, g, part)\n parts[part.id] = g\n }\n }\n const into = (part: string | null): THREE_NS.Object3D => (part !== null && parts[part]) || group\n for (const batch of level.batches) {\n const mesh = new THREE.Mesh(toGeometry(THREE, batch, { copy: options.copy !== false }), materialOf(batch.material, batch.part))\n mesh.geometry.computeBoundingSphere()\n mesh.geometry.computeBoundingBox()\n mesh.name = batch.part === null ? `static:${batch.material}` : `part:${batch.part}`\n if (batch.part !== null) mesh.userData.part = batch.part\n mesh.userData.material = batch.material\n if (batch.ranges.length) mesh.userData.ranges = batch.ranges\n mesh.castShadow = shadows\n mesh.receiveShadow = shadows\n into(batch.part).add(mesh)\n meshes.push(mesh)\n }\n for (const batch of level.instanced) {\n const mesh = new THREE.InstancedMesh(toGeometry(THREE, batch.geometry, { copy: options.copy !== false }), materialOf(batch.material, batch.part), batch.count)\n mesh.instanceMatrix.array.set(batch.matrices)\n mesh.instanceMatrix.needsUpdate = true\n if (batch.colors) mesh.instanceColor = new THREE.InstancedBufferAttribute(batch.colors.slice(), 3)\n mesh.name = `instanced:${batch.name ?? batch.material}`\n mesh.userData.material = batch.material\n if (batch.part !== null) mesh.userData.part = batch.part\n mesh.castShadow = shadows\n mesh.receiveShadow = shadows\n mesh.computeBoundingSphere()\n into(batch.part).add(mesh)\n instanced.push(mesh)\n }\n for (const part of Object.values(parts)) group.add(part)\n return { group, parts }\n }\n\n const levels = buffers.levels\n const first = build(levels[0] ?? { distance: 0, detail: 1, batches: [], instanced: [] }, true)\n let object: THREE_NS.Object3D = first.group\n if (levels.length > 1 || buffers.cullDistance !== undefined) {\n const lod = new THREE.LOD()\n lod.name = first.group.name\n lod.addLevel(first.group, 0)\n for (const level of levels.slice(1)) lod.addLevel(build(level, false).group, level.distance)\n if (buffers.cullDistance !== undefined) lod.addLevel(new THREE.Group(), buffers.cullDistance)\n object = lod\n }\n object.userData.points = buffers.points\n return { object, near: first.group, parts: first.parts, meshes, instanced, points: buffers.points, materials }\n}\n","import { mergeTransformed } from '@adatechnology/game-shapes'\nimport type { TransformedPiece } from '@adatechnology/game-shapes'\nimport type * as THREE_NS from 'three'\n\nimport type { Three } from './geometry'\n\nconst isVisible = (object: THREE_NS.Object3D): boolean => {\n for (let node: THREE_NS.Object3D | null = object; node; node = node.parent) if (!node.visible) return false\n return true\n}\n\n/** A plain, non-interleaved, not normalised Float32 attribute: what the core merge reads directly. */\nconst plain = (attribute: THREE_NS.BufferAttribute | THREE_NS.InterleavedBufferAttribute | undefined): attribute is THREE_NS.BufferAttribute =>\n attribute !== undefined && !(attribute as THREE_NS.InterleavedBufferAttribute).isInterleavedBufferAttribute && attribute.array instanceof Float32Array && !attribute.normalized\n\n/** Arrays of a geometry the core merge can take as they are; for anything else, the arrays three's own `toNonIndexed` makes (then not indexed). */\nfunction piece(geometry: THREE_NS.BufferGeometry, matrix: THREE_NS.Matrix4): TransformedPiece {\n const position = geometry.getAttribute('position')\n const normal = geometry.getAttribute('normal')\n const uv = geometry.getAttribute('uv')\n const ok = plain(position) && (normal === undefined || plain(normal)) && (uv === undefined || plain(uv)) && position.itemSize === 3 && (uv === undefined || uv.itemSize === 2)\n if (ok) {\n return { positions: position.array, ...(normal ? { normals: normal.array } : {}), ...(uv ? { uvs: uv.array } : {}), indices: geometry.index ? geometry.index.array : null, matrix: matrix.elements }\n }\n const flat = geometry.index ? geometry.toNonIndexed() : geometry\n const read = (name: string, size: number): Float32Array | undefined => {\n const attribute = flat.getAttribute(name)\n if (!attribute) return undefined\n const out = new Float32Array(attribute.count * size)\n for (let i = 0; i < attribute.count; i++) for (let c = 0; c < size; c++) out[i * size + c] = c === 0 ? attribute.getX(i) : c === 1 ? attribute.getY(i) : attribute.getZ(i)\n return out\n }\n const positions = read('position', 3) as Float32Array\n const normals = read('normal', 3)\n const uvs = read('uv', 2)\n if (flat !== geometry) flat.dispose()\n return { positions, ...(normals ? { normals } : {}), ...(uvs ? { uvs } : {}), indices: null, matrix: matrix.elements }\n}\n\n/**\n * Every visible mesh under `root` (whose world matrix must be up to date), baked into the space of `space` and joined by material: one non-indexed\n * mesh per material (position, normal, uv), no shadow cast. The bake is game-shapes' `mergeTransformed`, bit-identical to three's\n * `toNonIndexed` + `applyMatrix4` + join, so the result is the one `game-props3d`'s `mergeByMaterial` always gave.\n */\nexport function mergeByMaterial(THREE: Three, root: THREE_NS.Object3D, space: THREE_NS.Object3D): THREE_NS.Mesh[] {\n const toSpace = space.matrixWorld.clone().invert()\n const byMaterial = new Map<THREE_NS.Material, TransformedPiece[]>()\n root.traverse((object) => {\n const mesh = object as THREE_NS.Mesh\n if (!mesh.isMesh || Array.isArray(mesh.material) || !isVisible(mesh)) return\n const list = byMaterial.get(mesh.material) ?? []\n list.push(piece(mesh.geometry, new THREE.Matrix4().multiplyMatrices(toSpace, mesh.matrixWorld)))\n byMaterial.set(mesh.material, list)\n })\n return [...byMaterial].map(([material, pieces]) => {\n const merged = mergeTransformed(pieces)\n const geometry = new THREE.BufferGeometry()\n geometry.setAttribute('position', new THREE.BufferAttribute(merged.positions, 3))\n geometry.setAttribute('normal', new THREE.BufferAttribute(merged.normals, 3))\n geometry.setAttribute('uv', new THREE.BufferAttribute(merged.uvs, 2))\n const mesh = new THREE.Mesh(geometry, material)\n mesh.castShadow = false\n mesh.receiveShadow = true\n return mesh\n })\n}\n","import type * as ThreeNS from 'three'\n\nexport type Vec3 = [number, number, number]\ntype ThreeLib = typeof ThreeNS\n\n/**\n * Points of a hanging cord between `a` and `b` (n points including both ends, n >= 2). The deepest point sits `sag` meters below\n * the straight line between the ends, on a real catenary (y = a cosh(x/a)); when the ends differ in height the curve is\n * measured against the straight line between them.\n */\nexport function catenary(a: Vec3, b: Vec3, sag: number, n = 12): Vec3[] {\n const count = Math.max(2, Math.floor(n))\n const span = Math.hypot(b[0] - a[0], b[2] - a[2]) || Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2])\n const depth = Math.max(0, sag)\n const out: Vec3[] = []\n // Solve depth = c (cosh(span / 2c) - 1) for c by bisection (depth grows as c shrinks).\n let shape: (s: number) => number = () => 0\n if (depth > 1e-6 && span > 1e-6) {\n let lo = 1e-4\n let hi = span * 1000\n for (let i = 0; i < 80; i++) {\n const c = (lo + hi) / 2\n const d = c * (Math.cosh(span / (2 * c)) - 1)\n if (d > depth) lo = c\n else hi = c\n }\n const c = (lo + hi) / 2\n shape = (s) => c * (Math.cosh(((s - 0.5) * span) / c) - Math.cosh(span / (2 * c)))\n }\n for (let i = 0; i < count; i++) {\n const s = i / (count - 1)\n out.push([a[0] + (b[0] - a[0]) * s, a[1] + (b[1] - a[1]) * s + shape(s), a[2] + (b[2] - a[2]) * s])\n }\n return out\n}\n\n/** A thin tube through `points` (Catmull-Rom smoothed) for wires, cords and conduits. */\nexport function createCord(THREE: ThreeLib, points: readonly Vec3[], radius: number, material: ThreeNS.Material, radialSegments = 5): ThreeNS.Mesh {\n if (points.length < 2) throw new Error('createCord needs at least two points')\n const curve = new THREE.CatmullRomCurve3(points.map((p) => new THREE.Vector3(p[0], p[1], p[2])), false, 'centripetal')\n const segments = Math.max(4, (points.length - 1) * 4)\n const mesh = new THREE.Mesh(new THREE.TubeGeometry(curve, segments, radius, radialSegments, false), material)\n mesh.name = 'cord'\n mesh.castShadow = true\n return mesh\n}\n","import type * as THREE_NS from 'three'\n\n/** What `releaseObject3D` freed. */\nexport interface ReleaseReport {\n geometries: number\n materials: number\n textures: number\n}\n\nexport interface ReleaseOptions {\n /**\n * Trees that stay drawn (usually the scene): a geometry, material or texture any of them still uses is kept, so a resource shared by a clone that is\n * still on screen (a catalogue model, a material of the kit's palette) is not freed under it.\n */\n keep?: THREE_NS.Object3D | readonly THREE_NS.Object3D[]\n}\n\ntype Disposable = { dispose(): void; userData?: Record<string, unknown> }\ntype Owner = {\n geometry?: Disposable\n material?: THREE_NS.Material | THREE_NS.Material[]\n skeleton?: { dispose(): void }\n traverse(fn: (o: THREE_NS.Object3D) => void): void\n}\n\nconst isTexture = (v: unknown): v is THREE_NS.Texture => !!v && typeof v === 'object' && (v as { isTexture?: boolean }).isTexture === true\n\nfunction texturesOf(material: THREE_NS.Material, out: Set<THREE_NS.Texture>): void {\n for (const value of Object.values(material)) if (isTexture(value)) out.add(value)\n const uniforms = (material as { uniforms?: Record<string, { value?: unknown }> }).uniforms\n if (uniforms) for (const u of Object.values(uniforms)) if (isTexture(u?.value)) out.add(u.value)\n}\n\nfunction collect(roots: readonly THREE_NS.Object3D[]): { geometries: Set<Disposable>; materials: Set<THREE_NS.Material>; textures: Set<THREE_NS.Texture>; skeletons: Set<{ dispose(): void }> } {\n const geometries = new Set<Disposable>()\n const materials = new Set<THREE_NS.Material>()\n const textures = new Set<THREE_NS.Texture>()\n const skeletons = new Set<{ dispose(): void }>()\n for (const root of roots) {\n root.traverse((object) => {\n const o = object as unknown as Owner\n if (o.geometry && typeof o.geometry.dispose === 'function') geometries.add(o.geometry)\n if (o.skeleton && typeof o.skeleton.dispose === 'function') skeletons.add(o.skeleton)\n const list = Array.isArray(o.material) ? o.material : o.material ? [o.material] : []\n for (const m of list) {\n if (!m || typeof m.dispose !== 'function') continue\n materials.add(m)\n texturesOf(m, textures)\n }\n })\n }\n return { geometries, materials, textures, skeletons }\n}\n\nconst marked = new WeakSet<object>()\n/**\n * Marks geometries, materials or textures as shared on purpose (a palette, a model kit, a catalog): `releaseObject3D` and `discardObject3D` never free\n * them. Same as `userData.shared = true`, without touching the resource (its bytes and userData stay as they were, for digests and goldens).\n */\nexport function markShared<T extends object>(...resources: T[]): T[] {\n for (const resource of resources) marked.add(resource)\n return resources\n}\nconst shared = (r: { userData?: Record<string, unknown> }): boolean => r.userData?.shared === true || marked.has(r)\n\n/**\n * Free the GPU side of trees taken out of the scene: their geometries, materials, textures (maps and shader uniforms) and skeletons. three.js keeps a removed\n * mesh's buffers, programs and textures on the GPU (and the JS objects in its caches) until `dispose()`, so every object a game removes without this stays\n * in memory for good. A resource marked `userData.shared = true`, or still used by a tree in `keep`, is left alone. Disposing a resource that is used again\n * later is safe: three.js uploads it again on the next draw.\n */\nexport function releaseObject3D(roots: THREE_NS.Object3D | readonly THREE_NS.Object3D[], options: ReleaseOptions = {}): ReleaseReport {\n const list = (Array.isArray(roots) ? roots : [roots]) as THREE_NS.Object3D[]\n const report: ReleaseReport = { geometries: 0, materials: 0, textures: 0 }\n if (list.length === 0) return report\n const found = collect(list)\n const keepRoots = options.keep === undefined ? [] : ((Array.isArray(options.keep) ? options.keep : [options.keep]) as THREE_NS.Object3D[])\n // a released tree still hanging under a kept one is not \"kept\": skip it while walking the kept trees\n const releasing = new Set<THREE_NS.Object3D>(list)\n for (const keep of keepRoots) {\n const walk = (o: THREE_NS.Object3D): void => {\n if (releasing.has(o)) return\n const owner = o as unknown as Owner\n if (owner.geometry) found.geometries.delete(owner.geometry)\n if (owner.skeleton) found.skeletons.delete(owner.skeleton)\n const mats = Array.isArray(owner.material) ? owner.material : owner.material ? [owner.material] : []\n for (const m of mats) {\n if (!m) continue\n found.materials.delete(m)\n const t = new Set<THREE_NS.Texture>()\n texturesOf(m, t)\n for (const x of t) found.textures.delete(x)\n }\n for (const c of o.children) walk(c)\n }\n walk(keep)\n }\n for (const g of found.geometries) {\n if (shared(g)) continue\n g.dispose()\n report.geometries++\n }\n for (const m of found.materials) {\n if (shared(m as unknown as Disposable)) continue\n m.dispose()\n report.materials++\n }\n for (const t of found.textures) {\n if (shared(t as unknown as Disposable)) continue\n t.dispose()\n report.textures++\n }\n for (const s of found.skeletons) s.dispose()\n return report\n}\n\n/**\n * Take `objects` out of their trees and free what only they used (`releaseObject3D`, keeping whatever the rest of those trees, usually the scene,\n * still draws). The one call for pieces, fragments, helpers or bodies that leave the game for good. A list costs one walk of each scene, so pass\n * the whole list instead of calling it once per object.\n */\nexport function discardObject3D(objects: THREE_NS.Object3D | readonly THREE_NS.Object3D[], options: ReleaseOptions = {}): ReleaseReport {\n const list = (Array.isArray(objects) ? objects : [objects]) as THREE_NS.Object3D[]\n const keep = options.keep === undefined ? [] : Array.isArray(options.keep) ? [...(options.keep as THREE_NS.Object3D[])] : [options.keep as THREE_NS.Object3D]\n for (const object of list) {\n let top = object\n while (top.parent) top = top.parent\n object.removeFromParent()\n if (top !== object && !keep.includes(top)) keep.push(top)\n }\n return releaseObject3D(list, { keep })\n}\n","import { shapeGeometries } from '@adatechnology/game-shapes-three'\nimport type * as THREE_NS from 'three'\n\nimport type { Three } from './types'\n\n/** A body of revolution whose radius follows `profile` along the length, rear to front, closed by a point at each end. */\nexport const profileGeometry = (THREE: Three, length: number, profile: readonly number[], segments?: number): THREE_NS.LatheGeometry => {\n const points = profile.map((radius, index) => new THREE.Vector2(index / (profile.length - 1), radius))\n const curve = new THREE.SplineCurve(points)\n const outline: THREE_NS.Vector2[] = []\n outline.push(new THREE.Vector2(0.001, -length / 2 - 0.0))\n for (let step = 0; step <= 30; step++) {\n const point = curve.getPoint(step / 30)\n outline.push(new THREE.Vector2(Math.max(0.001, point.y), -length / 2 + point.x * length))\n }\n outline.push(new THREE.Vector2(0.001, length / 2))\n // the lathe comes from game-shapes (the same bytes as three's); a namespace already wrapped is given back as it is\n return new (shapeGeometries(THREE).LatheGeometry)(outline, segments || 22).rotateX(Math.PI / 2)\n}\n","import type { TextureMarker } from '@adatechnology/game-animals'\nimport type * as THREE_NS from 'three'\n\nimport { createStream } from './context'\nimport type { CanvasLike, DocumentLike, Three } from './types'\n\ntype Painter = {\n fillStyle: string\n strokeStyle: string\n lineWidth: number\n fillRect(x: number, y: number, width: number, height: number): void\n beginPath(): void\n arc(x: number, y: number, radius: number, from: number, to: number): void\n stroke(): void\n fill(): void\n}\n\nconst canvasTexture = (THREE: Three, document: DocumentLike, width: number, height: number, draw: (painter: Painter, width: number, height: number) => void): THREE_NS.CanvasTexture => {\n const canvas: CanvasLike = document.createElement('canvas')\n canvas.width = width\n canvas.height = height\n draw(canvas.getContext('2d') as Painter, width, height)\n const texture = new THREE.CanvasTexture(canvas as unknown as HTMLCanvasElement)\n texture.colorSpace = THREE.SRGBColorSpace\n return texture\n}\n\nconst spotsTexture = (THREE: Three, document: DocumentLike, base: string, spot: string): THREE_NS.CanvasTexture =>\n canvasTexture(THREE, document, 256, 256, (painter, width, height) => {\n painter.fillStyle = base\n painter.fillRect(0, 0, width, height)\n const next = createStream(11)\n for (let index = 0; index < 70; index++) {\n const x = next() * width\n const y = next() * height\n const radius = 5 + next() * 7\n painter.strokeStyle = spot\n painter.lineWidth = 3\n painter.beginPath()\n painter.arc(x, y, radius, 0, 7)\n painter.stroke()\n painter.fillStyle = 'rgba(120,70,20,.45)'\n painter.beginPath()\n painter.arc(x, y, radius * 0.6, 0, 7)\n painter.fill()\n }\n for (let index = 0; index < 60; index++) {\n painter.fillStyle = spot\n painter.beginPath()\n painter.arc(next() * width, next() * height, 1.5 + next() * 2.5, 0, 7)\n painter.fill()\n }\n })\n\nconst stripesTexture = (THREE: Three, document: DocumentLike, base: string, stripe: string): THREE_NS.CanvasTexture =>\n canvasTexture(THREE, document, 128, 128, (painter, width) => {\n painter.fillStyle = base\n painter.fillRect(0, 0, width, 128)\n painter.fillStyle = stripe\n for (let index = 0; index < 9; index++) painter.fillRect(0, index * 14 + (index % 2) * 3, width, 4)\n })\n\n/** The coat texture a species spec asks for (cat stripes, jaguar rosettes), painted on a canvas. */\nexport const createCoatTexture = (THREE: Three, document: DocumentLike, marker: TextureMarker): THREE_NS.CanvasTexture =>\n marker.kind === 'stripes' ? stripesTexture(THREE, document, marker.base, marker.stripe) : spotsTexture(THREE, document, marker.base, marker.spot)\n","import type { AnimalVisualState } from '@adatechnology/game-animals'\nimport type * as THREE_NS from 'three'\n\nimport type { AnimalRig, Three } from './types'\n\nconst INFECTED_TINT = 0x7d8a66\nconst POOL_COLOR = 0x3a0806\n\n/**\n * Applies a visual state (page: `setInfected`, with the dead look for every species): infected greys the coat, glows the eyes, shows the wounds and hides\n * the first ear; dead lets the animation lay the body down and shows a blood pool at `home`. The pool goes under `rig.root.parent`; with no parent yet,\n * it is created hidden from the scene and `rig.pool` is for the caller to add.\n */\nexport const applyVisualState = (THREE: Three, rig: AnimalRig, state: AnimalVisualState): void => {\n rig.infected = state === 'infected'\n rig.dead = state === 'dead'\n const tint = new THREE.Color(INFECTED_TINT)\n rig.materials.forEach((material) => {\n if (material === rig.eyeMaterial || material === rig.woundMaterial) return\n material.color.copy(material.userData.base as THREE_NS.Color)\n if (rig.infected) material.color.lerp(tint, 0.45).multiplyScalar(0.72)\n })\n const eye = rig.eyeMaterial\n if (eye) {\n if (rig.infected) {\n eye.color.set(0xdcd8b0)\n eye.emissive = new THREE.Color(0x8a8660)\n eye.emissiveIntensity = 0.8\n } else {\n eye.color.copy(eye.userData.base as THREE_NS.Color)\n eye.emissive = new THREE.Color(0)\n }\n }\n rig.infectionParts.forEach((part) => (part.visible = rig.infected))\n const firstEar = rig.ears[0]\n if (firstEar) firstEar.visible = !rig.infected\n if (!rig.dead) {\n rig.root.rotation.z = 0\n rig.root.position.y = 0\n }\n if (rig.pool) {\n rig.pool.visible = rig.dead\n return\n }\n if (!rig.dead) return\n const pool = new THREE.Mesh(new THREE.CircleGeometry(1, 22).rotateX(-Math.PI / 2), new THREE.MeshStandardMaterial({ color: POOL_COLOR, roughness: 0.25 }))\n pool.position.set(rig.home.x, 0.005, rig.home.z + 0.1)\n pool.receiveShadow = true\n pool.scale.setScalar(rig.size * 0.45)\n rig.root.parent?.add(pool)\n rig.pool = pool\n}\n","import type * as THREE_NS from 'three'\n\nimport type { AnimalRig } from './types'\n\n/** The \"joints\" view (page: `setJoint`): the coat turns see-through and every joint ball shows in `jointMaterial`. */\nexport const setJointView = (rig: AnimalRig, on: boolean, jointMaterial: THREE_NS.Material): void => {\n rig.materials.forEach((material) => {\n if (material === rig.eyeMaterial) return\n material.transparent = on\n material.opacity = on ? 0.28 : 1\n material.depthWrite = !on\n material.needsUpdate = true\n })\n rig.joints.forEach((joint) => {\n if (on) joint.userData.restMaterial ??= joint.material\n joint.material = on ? jointMaterial : (joint.userData.restMaterial as THREE_NS.Material)\n joint.renderOrder = on ? 2 : 0\n })\n}\n\n/** The \"split\" view (page: `setSplit`): sets where each pivot should go, the rest position or the rest plus its offset; `updateSplit` moves it there. */\nexport const setSplit = (rig: AnimalRig, on: boolean): void => {\n rig.pivots.forEach((pivot) => {\n pivot.group.userData.target = on ? pivot.rest.clone().add(pivot.offset) : pivot.rest.clone()\n })\n}\n\n/** Moves each pivot toward its split target (the page does it in the frame loop: a lerp of 5 per second). */\nexport const updateSplit = (rig: AnimalRig, delta: number): void => {\n rig.pivots.forEach((pivot) => {\n const target = pivot.group.userData.target as THREE_NS.Vector3 | undefined\n if (target) pivot.group.position.lerp(target, Math.min(1, delta * 5))\n })\n}\n","import type { Color, PropItem } from './types'\n\n/** Deterministic PRNG (mulberry32 variant) returning floats in [0, 1). */\nexport function rng(seed: number): () => number {\n let s = seed >>> 0\n return () => {\n s = (s + 0x6d2b79f5) >>> 0\n let t = s\n t = Math.imul(t ^ (t >>> 15), t | 1)\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n }\n}\n\n/** Seed of the details draw: `M.seed` (number or string) or a hash of the position. */\nexport const seedOf = (M: PropItem): number =>\n (M.seed != null\n ? typeof M.seed === 'number'\n ? M.seed\n : [...String(M.seed)].reduce((a, c) => (a * 31 + c.charCodeAt(0)) | 0, 7)\n : Math.round((M.x || 0) * 37 + (M.z || 0) * 91 + 1000)) >>> 0\n\n/** `'#rrggbb'` or number to number. */\nexport const hex = (v: Color): number => (typeof v === 'string' ? parseInt(v.replace('#', ''), 16) : v)\n\n/** Scales the brightness of a colour by factor `f` (clamped per channel). */\nexport const shade = (c: Color, f: number): number => {\n const n = hex(c)\n const r = Math.min(255, Math.round(((n >> 16) & 255) * f))\n const g = Math.min(255, Math.round(((n >> 8) & 255) * f))\n const b = Math.min(255, Math.round((n & 255) * f))\n return (r << 16) | (g << 8) | b\n}\n\n/** Linear mix of two colours. */\nexport const mix = (a: Color, b: Color, t: number): number => {\n const x = hex(a)\n const y = hex(b)\n const m = (s: number) => Math.round(((x >> s) & 255) * (1 - t) + ((y >> s) & 255) * t)\n return (m(16) << 16) | (m(8) << 8) | m(0)\n}\n","import { rng } from './colors'\nimport type { AtlasMap, AtlasName, DocumentLike, FloorAtlas, FloorKind, PictureAtlas, Uv, WaterAtlas } from './types'\n\ntype Ctx = CanvasRenderingContext2D\ntype Rand = () => number\n\nconst makeCanvas = (doc: DocumentLike, w: number, h: number): HTMLCanvasElement => {\n const c = doc.createElement('canvas')\n c.width = w\n c.height = h\n return c\n}\n\n// draws f() nine times (offsets of +-n) inside the clipped cell, so the pattern tiles without a seam\nfunction seamless(g: Ctx, x0: number, y0: number, n: number, f: () => void) {\n g.save()\n g.beginPath()\n g.rect(x0, y0, n, n)\n g.clip()\n for (let dy = -n; dy <= n; dy += n)\n for (let dx = -n; dx <= n; dx += n) {\n g.save()\n g.translate(x0 + dx, y0 + dy)\n f()\n g.restore()\n }\n g.restore()\n}\nconst noise = (g: Ctx, x0: number, y0: number, n: number, r: Rand, a: number) => {\n for (let i = 0; i < (n * n) / 6; i++) {\n const v = r() < 0.5 ? 0 : 255\n g.fillStyle = `rgba(${v},${v},${v},${r() * a})`\n g.fillRect(x0 + r() * n, y0 + r() * n, 1 + r() * 2, 1 + r() * 2)\n }\n}\n\n/** Floor kinds that live in the floor atlas: atlas cells and the world size (meters) one cell covers. */\nexport const FLOORS: Record<FloorKind, { cells: number[]; size: number }> = {\n lawn: { cells: [0, 1], size: 2.0 },\n tile_floor: { cells: [2], size: 1.0 },\n concrete: { cells: [3], size: 2.0 },\n deck_floor: { cells: [4], size: 1.2 },\n dirt: { cells: [5], size: 2.0 },\n pool_tile: { cells: [6], size: 1.0 },\n paving: { cells: [7], size: 1.0 },\n}\nconst PICTURE_CELLS = 8 // 0-3 abstract, 4-6 landscapes, 7 mirror\n\nfunction floorAtlas(doc: DocumentLike): FloorAtlas {\n const N = 256, cv = makeCanvas(doc, N * 4, N * 2), g = cv.getContext('2d')!, P = (c: number): [number, number] => [(c % 4) * N, Math.floor(c / 4) * N];\n const cell = (c: number, seed: number, f: (g: Ctx, x0: number, y0: number, r: Rand) => void) => { const [x0, y0] = P(c), r = rng(seed); f(g, x0, y0, r); };\n for (const [c, seed] of [[0, 11], [1, 12]] as Array<[number, number]>) cell(c, seed, (g, x0, y0, r) => { // lawn: base + blades + dry patches\n g.fillStyle = c ? '#648f3a' : '#5b8a37'; g.fillRect(x0, y0, N, N);\n seamless(g, x0, y0, N, () => { for (let i = 0; i < 40; i++) { g.fillStyle = `rgba(${r() < 0.5 ? '160,170,60' : '40,80,30'},${0.12 + r() * 0.12})`; g.beginPath(); g.arc(r() * N, r() * N, 8 + r() * 22, 0, 6.3); g.fill(); }\n for (let i = 0; i < 520; i++) { r(); /* the original drew a blade height here and never used it; the call keeps the random sequence identical */ g.strokeStyle = `hsl(${85 + r() * 30},${40 + r() * 25}%,${22 + r() * 24}%)`; g.lineWidth = 1; const x = r() * N, y = r() * N; g.beginPath(); g.moveTo(x, y); g.lineTo(x + (r() - 0.5) * 6, y - 4 - r() * 9); g.stroke(); } }); });\n cell(2, 21, (g, x0, y0, r) => { // beige 4x4 ceramic tiles with grout\n const q = N / 4; g.fillStyle = '#8c877b'; g.fillRect(x0, y0, N, N);\n for (let j = 0; j < 4; j++) for (let i = 0; i < 4; i++) { const v = 204 + (r() - 0.5) * 16; g.fillStyle = `rgb(${v},${v - 6},${v - 18})`; g.fillRect(x0 + i * q + 2, y0 + j * q + 2, q - 4, q - 4); g.fillStyle = 'rgba(255,255,255,.18)'; g.fillRect(x0 + i * q + 3, y0 + j * q + 3, q - 8, 3); }\n for (let k = 0; k < 14; k++) { const gr = g.createRadialGradient(0, 0, 0, 0, 0, 40); const x = x0 + r() * N, y = y0 + r() * N; g.save(); g.beginPath(); g.rect(x0, y0, N, N); g.clip(); g.translate(x, y); gr.addColorStop(0, 'rgba(90,80,60,.10)'); gr.addColorStop(1, 'rgba(90,80,60,0)'); g.fillStyle = gr; g.fillRect(-40, -40, 80, 80); g.restore(); } });\n cell(3, 31, (g, x0, y0, r) => { // concrete: slab, expansion joint, crack, stain\n g.fillStyle = '#a7a7a0'; g.fillRect(x0, y0, N, N);\n for (let k = 0; k < 18; k++) { const gr = g.createRadialGradient(0, 0, 0, 0, 0, 50); g.save(); g.beginPath(); g.rect(x0, y0, N, N); g.clip(); g.translate(x0 + r() * N, y0 + r() * N); gr.addColorStop(0, `rgba(${r() < 0.5 ? '70,70,60' : '200,200,190'},.12)`); gr.addColorStop(1, 'rgba(0,0,0,0)'); g.fillStyle = gr; g.fillRect(-50, -50, 100, 100); g.restore(); }\n noise(g, x0, y0, N, r, 0.25); g.fillStyle = 'rgba(40,40,36,.55)'; g.fillRect(x0, y0 + N / 2 - 1, N, 2); g.fillRect(x0 + N / 2 - 1, y0, 2, N); g.fillRect(x0, y0, N, 1); g.fillRect(x0, y0, 1, N);\n g.strokeStyle = 'rgba(50,50,46,.5)'; g.lineWidth = 1; let x = x0 + 30 + r() * 60, y = y0 + 8; g.beginPath(); g.moveTo(x, y); for (let s = 0; s < 8; s++) { x += (r() - 0.5) * 16; y += 12 + r() * 8; g.lineTo(x, y); } g.stroke(); });\n cell(4, 41, (g, x0, y0, r) => { // deck: 12 boards in staggered rows\n const th = N / 12; g.fillStyle = '#2a1c10'; g.fillRect(x0, y0, N, N);\n for (let j = 0; j < 12; j++) { const cut = r() * N; for (const [a, b] of [[0, cut], [cut, N]]) { const v = 150 + (r() - 0.5) * 36; g.fillStyle = `rgb(${v},${v * 0.68 | 0},${v * 0.42 | 0})`; g.fillRect(x0 + a + 1, y0 + j * th + 1, b - a - 2, th - 2);\n g.strokeStyle = 'rgba(50,28,12,.3)'; for (let s = 0; s < 3; s++) { const y = y0 + j * th + 3 + r() * (th - 6); g.beginPath(); g.moveTo(x0 + a, y); g.lineTo(x0 + b, y + (r() - 0.5) * 2); g.stroke(); }\n g.fillStyle = 'rgba(30,20,10,.7)'; g.fillRect(x0 + a + 4, y0 + j * th + th / 2, 2, 2); g.fillRect(x0 + b - 6, y0 + j * th + th / 2, 2, 2); } } });\n cell(5, 51, (g, x0, y0, r) => { // packed dirt\n g.fillStyle = '#7b5538'; g.fillRect(x0, y0, N, N);\n seamless(g, x0, y0, N, () => { for (let k = 0; k < 30; k++) { g.fillStyle = `rgba(${r() < 0.5 ? '60,38,24' : '150,112,80'},${0.12 + r() * 0.15})`; g.beginPath(); g.arc(r() * N, r() * N, 6 + r() * 20, 0, 6.3); g.fill(); }\n for (let k = 0; k < 90; k++) { const v = 90 + r() * 90; g.fillStyle = `rgb(${v},${v * 0.85 | 0},${v * 0.7 | 0})`; g.fillRect(r() * N, r() * N, 1 + r() * 3, 1 + r() * 3); } }); });\n cell(6, 61, (g, x0, y0, r) => { // 8x8 pool tiles\n const q = N / 8; g.fillStyle = '#d4e8ee'; g.fillRect(x0, y0, N, N);\n for (let j = 0; j < 8; j++) for (let i = 0; i < 8; i++) { const v = (r() - 0.5) * 24; g.fillStyle = `rgb(${50 + v},${150 + v},${190 + v})`; g.fillRect(x0 + i * q + 1, y0 + j * q + 1, q - 2, q - 2); g.fillStyle = 'rgba(255,255,255,.2)'; g.fillRect(x0 + i * q + 2, y0 + j * q + 2, q - 6, 2); } });\n cell(7, 71, (g, x0, y0, r) => { // reddish interlocking paving (small bricks)\n const bw = N / 4, bh = N / 8; g.fillStyle = '#7a7268'; g.fillRect(x0, y0, N, N);\n for (let j = 0; j < 8; j++) for (let i = -1; i < 4; i++) { const off = (j % 2) * bw / 2, v = 150 + (r() - 0.5) * 30; g.fillStyle = `rgb(${v + 20},${v * 0.55 | 0},${v * 0.42 | 0})`; g.fillRect(x0 + ((i * bw + off) % N + N) % N + 1, y0 + j * bh + 1, bw - 2, bh - 2); }\n noise(g, x0, y0, N, r, 0.2); });\n const cells = {} as Record<FloorKind, Uv[]>; for (const k of Object.keys(FLOORS) as FloorKind[]) cells[k] = FLOORS[k].cells.map(c => ({ u0: (c % 4) / 4, u1: (c % 4 + 1) / 4, v0: 1 - (Math.floor(c / 4) + 1) / 2, v1: 1 - Math.floor(c / 4) / 2 }));\n const e = 1.5 / (N * 4), ev = 1.5 / (N * 2); // inset of one and a half pixels: no bleeding from the neighbouring cell\n return { canvas: cv, cells, size: (k: FloorKind) => FLOORS[k].size, uv(k: FloorKind, v = 0) { const L = cells[k]; const c = L[v % L.length]; return { u0: c.u0 + e, u1: c.u1 - e, v0: c.v0 + ev, v1: c.v1 - ev }; }, variants: (k: FloorKind) => FLOORS[k].cells.length };\n}\n\nfunction pictureAtlas(doc: DocumentLike, names: string[]): PictureAtlas {\n const N = 256, cv = makeCanvas(doc, N * 4, N * 2), g = cv.getContext('2d')!;\n const cell = (c: number, seed: number, f: (r: Rand) => void) => { const x0 = (c % 4) * N, y0 = Math.floor(c / 4) * N, r = rng(seed); g.save(); g.beginPath(); g.rect(x0, y0, N, N); g.clip(); g.translate(x0, y0); f(r); g.restore(); };\n cell(0, 101, r => { g.fillStyle = '#e8dcc4'; g.fillRect(0, 0, N, N); g.fillStyle = '#c0613c'; g.beginPath(); g.arc(90 + r() * 30, 110, 60, 0, 6.3); g.fill(); g.fillStyle = '#2f7f84'; g.fillRect(120, 40 + r() * 30, 90, 150); g.fillStyle = '#d9a43a'; g.beginPath(); g.arc(150, 200, 70, Math.PI, 0); g.fill(); g.strokeStyle = '#2a2a2a'; g.lineWidth = 3; g.beginPath(); g.moveTo(20, 230); g.lineTo(236, 30); g.stroke(); });\n cell(1, 102, r => { g.fillStyle = '#1f3a5f'; g.fillRect(0, 0, N, N); for (let i = 0; i < 7; i++) { g.fillStyle = ['#f08a3c', '#f4e4c1', '#d94f4f', '#7fb5a5'][i % 4]; g.globalAlpha = 0.85; g.beginPath(); g.ellipse(r() * N, r() * N, 20 + r() * 50, 12 + r() * 30, r() * 3, 0, 6.3); g.fill(); } g.globalAlpha = 1; g.strokeStyle = '#f4e4c1'; g.lineWidth = 4; for (let i = 0; i < 3; i++) { g.beginPath(); g.moveTo(0, 50 + i * 70); g.bezierCurveTo(80, 10 + i * 70, 170, 120 + i * 40, N, 60 + i * 60); g.stroke(); } });\n cell(2, 103, r => { const cs = ['#e9e1cf', '#c8553d', '#2e5e7e', '#e4b23e', '#4b4b4b']; let x = 0; while (x < N) { const w = 30 + r() * 60; g.fillStyle = cs[(r() * cs.length) | 0]; g.fillRect(x, 0, w, N); x += w; } g.fillStyle = '#e9e1cf'; g.fillRect(40, 60, 176, 136); g.fillStyle = cs[1]; g.fillRect(70, 90, 60, 76); g.fillStyle = cs[2]; g.fillRect(140, 90, 50, 36); });\n cell(3, 104, r => { g.fillStyle = '#f2f0ea'; g.fillRect(0, 0, N, N); g.fillStyle = '#1a1a1a'; g.fillRect(40, 190, 176, 8); g.fillStyle = '#b8352e'; g.beginPath(); g.arc(128, 100, 52, 0, 6.3); g.fill(); g.strokeStyle = '#1a1a1a'; g.lineWidth = 2; for (let i = 0; i < 5; i++) { g.beginPath(); g.moveTo(40, 30 + i * 8 + r() * 4); g.lineTo(216, 30 + i * 8); g.stroke(); } });\n cell(4, 105, r => { const gr = g.createLinearGradient(0, 0, 0, N * 0.7); gr.addColorStop(0, '#5a4a8a'); gr.addColorStop(0.5, '#e8805a'); gr.addColorStop(1, '#f7d27a'); g.fillStyle = gr; g.fillRect(0, 0, N, N); g.fillStyle = '#fff3c0'; g.beginPath(); g.arc(150, 150, 36, 0, 6.3); g.fill();\n ([['#7a5a6a', 150], ['#4a4a5e', 175], ['#2a3a3a', 205]] as Array<[string, number]>).forEach(([c, y], k) => { g.fillStyle = c; g.beginPath(); g.moveTo(0, N); g.lineTo(0, y); for (let x = 0; x <= N; x += 32) g.lineTo(x, y - 10 + r() * 24 - k * 3); g.lineTo(N, N); g.fill(); }); });\n cell(5, 106, r => { const gr = g.createLinearGradient(0, 0, 0, N); gr.addColorStop(0, '#9fc8e8'); gr.addColorStop(0.55, '#e8f0f4'); gr.addColorStop(0.56, '#3d7a9a'); gr.addColorStop(1, '#22506a'); g.fillStyle = gr; g.fillRect(0, 0, N, N);\n g.fillStyle = '#5a6a78'; g.beginPath(); g.moveTo(0, 140); for (let x = 0; x <= N; x += 28) g.lineTo(x, 70 + r() * 50); g.lineTo(N, 140); g.fill(); g.fillStyle = '#3a5a4a'; g.beginPath(); g.moveTo(150, 140); g.quadraticCurveTo(200, 60, 230, 140); g.fill(); });\n cell(6, 107, () => { const gr = g.createLinearGradient(0, 0, 0, N); gr.addColorStop(0, '#f6e0a8'); gr.addColorStop(0.5, '#fbeed0'); gr.addColorStop(0.5, '#4bb0c4'); gr.addColorStop(0.72, '#2e8aa8'); gr.addColorStop(0.72, '#ecd8a2'); gr.addColorStop(1, '#d9bd7e'); g.fillStyle = gr; g.fillRect(0, 0, N, N);\n g.strokeStyle = '#4a3a22'; g.lineWidth = 5; g.beginPath(); g.moveTo(60, 255); g.quadraticCurveTo(70, 160, 100, 70); g.stroke(); g.fillStyle = '#2f6a3a'; for (let i = 0; i < 6; i++) { g.beginPath(); g.moveTo(100, 70); g.quadraticCurveTo(100 + Math.cos(i) * 40, 40 + Math.sin(i * 2) * 20, 100 + Math.cos(i * 1.1) * 60, 80 + i * 6); g.quadraticCurveTo(100 + Math.cos(i) * 30, 70, 100, 70); g.fill(); } });\n cell(7, 108, () => { const gr = g.createLinearGradient(0, 0, N, N); gr.addColorStop(0, '#e4e9eb'); gr.addColorStop(0.5, '#c4ccd0'); gr.addColorStop(1, '#a8b2b8'); g.fillStyle = gr; g.fillRect(0, 0, N, N);\n g.fillStyle = 'rgba(255,255,255,.35)'; g.beginPath(); g.moveTo(30, 0); g.lineTo(90, 0); g.lineTo(0, 120); g.lineTo(0, 60); g.fill(); g.beginPath(); g.moveTo(150, 0); g.lineTo(175, 0); g.lineTo(0, 256); g.lineTo(0, 232); g.fill();\n const v = g.createRadialGradient(128, 128, 90, 128, 128, 190); v.addColorStop(0, 'rgba(0,0,0,0)'); v.addColorStop(1, 'rgba(0,0,0,.18)'); g.fillStyle = v; g.fillRect(0, 0, N, N); });\n const uvc = (c: number): Uv => ({ u0: (c % 4) / 4 + 1.5 / (N * 4), u1: (c % 4 + 1) / 4 - 1.5 / (N * 4), v0: 1 - (Math.floor(c / 4) + 1) / 2 + 1.5 / (N * 2), v1: 1 - Math.floor(c / 4) / 2 - 1.5 / (N * 2) });\n return { canvas: cv, n: PICTURE_CELLS - 1, uv: c => uvc(c), mirror: () => uvc(7), names };\n}\n\n// light/dark ripple that tiles; the host scrolls texture.offset for the shimmer\nfunction waterAtlas(doc: DocumentLike): WaterAtlas {\n const N = 128, cv = makeCanvas(doc, N, N), g = cv.getContext('2d')!, r = rng(901);\n g.fillStyle = '#e6f2f8'; g.fillRect(0, 0, N, N);\n seamless(g, 0, 0, N, () => { for (let i = 0; i < 26; i++) { const x = r() * N, y = r() * N, w = 14 + r() * 26; g.strokeStyle = r() < 0.55 ? 'rgba(255,255,255,.9)' : 'rgba(110,170,200,.5)'; g.lineWidth = 1 + r() * 1.5; g.beginPath(); g.moveTo(x, y); g.quadraticCurveTo(x + w / 2, y - 5 - r() * 4, x + w, y); g.stroke(); } });\n return { canvas: cv };\n}\n\n/** Lazily builds and caches the procedural canvas atlases (one canvas per atlas; the host makes one texture per canvas). */\nexport function createAtlasProvider(doc: DocumentLike | null, pictureNames: string[]) {\n const cache: { [K in AtlasName]?: AtlasMap[AtlasName] } = {}\n return function atlas<K extends AtlasName>(name: K): AtlasMap[K] {\n if (!doc) throw new Error('game-props3d: a document is needed to build the atlases (pass options.document)')\n const made = cache[name]\n if (made) return made as AtlasMap[K]\n const build = { floor: () => floorAtlas(doc), picture: () => pictureAtlas(doc, pictureNames), water: () => waterAtlas(doc) }[name]\n return (cache[name] = build()) as AtlasMap[K]\n }\n}\n","import type { PropGroup, TypeDef } from './types'\n\nconst T = <I extends string>(id: I, interior: boolean, w: number, d: number, h: number, y: number, solid: boolean, group: PropGroup): TypeDef<I> => ({ id, interior, w, d, h, y, solid, group })\n\n/** id, interior, w, d, h, y (mounting height), solid (false = no collision), group. Names live in the locale table. */\nexport const TYPE_DEFS = [\n T('ceiling_light', true, 0.4, 0.4, 0.07, 0, false, 'ceiling'),\n T('pendant', true, 0.34, 0.34, 1.0, 0, false, 'ceiling'),\n T('track_spot', true, 1.2, 0.1, 0.2, 0, false, 'ceiling'),\n T('chandelier', true, 0.7, 0.7, 0.7, 0, false, 'ceiling'),\n T('wall_ac', true, 0.9, 0.2, 0.28, 2.15, false, 'wall'),\n T('picture_small', true, 0.35, 0.03, 0.45, 1.3, false, 'wall'),\n T('picture_large', true, 1.0, 0.04, 0.7, 1.2, false, 'wall'),\n T('mirror', true, 0.6, 0.03, 1.2, 0.9, false, 'wall'),\n T('curtain', true, 1.6, 0.12, 2.3, 0.1, false, 'wall'),\n T('blinds', true, 1.2, 0.08, 1.3, 1.0, false, 'wall'),\n T('flat_tv', true, 1.2, 0.06, 0.7, 1.0, false, 'wall'),\n T('floor_plant', true, 0.5, 0.5, 1.1, 0, true, 'plants'),\n T('table_plant', true, 0.2, 0.2, 0.3, 0, false, 'plants'),\n T('hanging_plant', true, 0.5, 0.5, 1.0, 0, false, 'plants'),\n T('tv_stand', true, 1.6, 0.42, 1.05, 0, true, 'furniture'),\n T('breakfast_bar', true, 2.0, 1.6, 0.95, 0, true, 'furniture'),\n T('sofa_l', true, 2.8, 1.7, 0.85, 0, true, 'furniture'),\n T('double_bed_modern', true, 1.6, 2.1, 1.2, 0, true, 'furniture'),\n T('home_office', true, 1.4, 0.75, 1.2, 0, true, 'furniture'),\n T('closet', true, 2.2, 0.6, 2.3, 0, true, 'furniture'),\n T('vanity', true, 0.6, 0.45, 1.5, 0, true, 'furniture'),\n T('ac_condenser', false, 0.8, 0.3, 0.55, 0.5, true, 'yard'),\n T('lawn', false, 4, 4, 0.02, 0, false, 'floor'),\n T('tile_floor', false, 4, 3, 0.03, 0, false, 'floor'),\n T('concrete', false, 4, 4, 0.03, 0, false, 'floor'),\n T('deck_floor', false, 4, 3, 0.04, 0, false, 'floor'),\n T('dirt', false, 4, 4, 0.02, 0, false, 'floor'),\n T('gourmet_counter', false, 4.2, 3.0, 2.9, 0, true, 'yard'),\n T('sauna', false, 2.0, 1.8, 2.1, 0, true, 'yard'),\n T('soaking_tub', false, 1.3, 1.3, 0.8, 0, true, 'yard'),\n T('pool', false, 4.5, 2.8, 0.1, 0, true, 'yard'),\n T('deck', false, 3.5, 3.0, 0.22, 0, true, 'yard'),\n T('sun_lounger', false, 0.62, 1.9, 0.5, 0, true, 'yard'),\n T('garden_bed', false, 2.0, 1.0, 0.9, 0, true, 'yard'),\n T('fruit_tree', false, 2.4, 2.4, 3.2, 0, true, 'yard'),\n T('palm_tree', false, 2.0, 2.0, 4.5, 0, true, 'yard'),\n T('pergola', false, 3.5, 3.0, 2.4, 0, true, 'yard'),\n T('slide', false, 0.8, 2.6, 1.5, 0, true, 'yard'),\n T('swing', false, 2.2, 1.7, 2.2, 0, true, 'yard'),\n T('bbq_simple', false, 0.9, 0.6, 1.0, 0, true, 'yard'),\n T('garage_car', false, 1.8, 4.2, 1.45, 0, true, 'yard'),\n T('window_boards', true, 1.2, 0.06, 1.15, 1.0, false, 'house_state'),\n T('barricade', true, 1.5, 0.7, 1.8, 0, true, 'house_state'),\n T('sandbags', false, 1.6, 0.5, 0.6, 0, true, 'house_state'),\n T('barbed_wire', false, 4.0, 0.2, 0.6, 0, false, 'house_state'),\n T('razor_wire', false, 4.0, 0.6, 0.6, 0, false, 'house_state'),\n T('water_bottle_crates', true, 0.9, 0.6, 0.65, 0, true, 'house_state'),\n T('canned_food', true, 0.4, 0.3, 0.3, 0, false, 'house_state'),\n T('water_jug', true, 0.3, 0.3, 0.5, 0, true, 'house_state'),\n T('floor_mattress', true, 0.9, 1.9, 0.3, 0, false, 'house_state'),\n T('lantern', true, 0.16, 0.16, 0.3, 0, false, 'house_state'),\n T('candles', true, 0.25, 0.25, 0.2, 0, false, 'house_state'),\n T('open_suitcases', true, 1.5, 0.7, 0.6, 0, false, 'house_state'),\n T('clothes_on_floor', true, 1.2, 1.0, 0.05, 0, false, 'house_state'),\n T('fallen_chair', true, 0.5, 0.5, 0.5, 0, false, 'house_state'),\n T('blood_stains', true, 1.2, 1.2, 0.01, 0, false, 'house_state'),\n T('open_door', true, 0.9, 0.15, 2.15, 0, false, 'house_state'),\n]\n\nexport type PropType = (typeof TYPE_DEFS)[number]['id']\n\nexport const DEFS = Object.fromEntries(TYPE_DEFS.map((t) => [t.id, t])) as Record<PropType, (typeof TYPE_DEFS)[number]>\n\nexport const MATERIALS = ['plain', 'metal', 'wood', 'glass', 'fabric', 'plant', 'water', 'emissive', 'atlas_picture', 'atlas_floor'] as const\n","import type { BuilderContext, BuilderMap } from '../types'\n\nexport function ceilingBuilders({ kit }: BuilderContext): Partial<BuilderMap> {\n const B: Partial<BuilderMap> = {}\n B.ceiling_light = (M, a) => { const K = kit(M, a), t = K.ceiling(), on = !!M.on;\n K.cyl('plain', 0, t - 0.05, 0, 0.2, 0.05, 0xece8e0); K.cyl('plain', 0, t - 0.07, 0, 0.15, 0.02, 0xd8d4c8); K.cyl(on ? 'emissive' : 'plain', 0, t - 0.078, 0, 0.12, 0.012, on ? 0xfff0c8 : 0xf4f0e6);\n if (on) K.light(0, t - 0.15, 0, 0xffe8b8, 6, 'ceiling_light'); };\n B.pendant = (M, a) => { const K = kit(M, a), t = K.ceiling(), { h } = K.dim('pendant'), ys = t - h, color = K.main(0x2a2a2a);\n K.box('plain', 0, ys + 0.1, 0, 0.012, h - 0.1, 0.012, 0x1a1a1a); K.cyl('plain', 0, t - 0.03, 0, 0.05, 0.03, 0xe8e4dc);\n K.cyl('plain', 0, ys, 0, 0.17, 0.14, color, 0, 0, 'sphere'); K.cyl('emissive', 0, ys - 0.035, 0, 0.045, 0.09, 0xffe0a0, 0, 0, 'sphere');\n if (M.light) K.light(0, ys - 0.1, 0, 0xffc880, 6, 'pendant'); };\n B.track_spot = (M, a) => { const K = kit(M, a), t = K.ceiling(), { w } = K.dim('track_spot'), on = !!M.on;\n K.box('metal', 0, t - 0.025, 0, w, 0.025, 0.035, 0x2a2a2a);\n for (let i = -1; i <= 1; i++) { const u = i * w / 3; K.box('metal', u, t - 0.1, 0, 0.02, 0.075, 0.02, 0x2a2a2a); K.cyl('metal', u, t - 0.16, 0.04, 0.035, 0.11, 0x222222, 2.6, 0); K.cyl(on ? 'emissive' : 'plain', u, t - 0.208, 0.069, 0.028, 0.006, on ? 0xfff0c8 : 0xe8e8e8, 2.6, 0); } };\n B.chandelier = (M, a) => { const K = kit(M, a), t = K.ceiling(), on = !!M.on, y = t - 0.5, gold = K.metal(0x8a7a5a);\n K.box('metal', 0, y, 0, 0.012, 0.5, 0.012, gold); K.cyl('metal', 0, y - 0.04, 0, 0.06, 0.08, gold); K.cyl('metal', 0, y - 0.06, 0, 0.14, 0.015, gold);\n for (let i = 0; i < 6; i++) { const an = i * Math.PI / 3, cx = Math.cos(an), cz = Math.sin(an);\n K.box('metal', cx * 0.14, y - 0.06, cz * 0.14, 0.28, 0.012, 0.012, gold, -an); K.cyl('plain', cx * 0.28, y - 0.06, cz * 0.28, 0.02, 0.09, 0xf0e6c8); K.cyl(on ? 'emissive' : 'plain', cx * 0.28, y + 0.03, cz * 0.28, 0.012, 0.03, on ? 0xffd890 : 0xe6d8b0, 0, 0, 'sphere');\n K.box('glass', cx * 0.3, y - 0.2, cz * 0.3, 0.025, 0.14, 0.025, 0xffffff, -an); K.box('glass', cx * 0.14, y - 0.18, cz * 0.14, 0.02, 0.12, 0.02, 0xffffff); }\n K.box('glass', 0, y - 0.3, 0, 0.05, 0.2, 0.05, 0xffffff, 0.7);\n if (M.light) K.light(0, y - 0.1, 0, 0xffd8a0, 6, 'chandelier'); };\n return B\n}\n","import type { BuilderContext, BuilderMap, FloorKind } from '../types'\n\nconst FLOOR_ITEMS: Array<FloorKind & keyof BuilderMap> = ['lawn', 'tile_floor', 'concrete', 'deck_floor', 'dirt']\n\n/** Ground cover items: one builder each, tiling the item's rectangle (or `M.poly`) with the floor atlas. */\nexport function floorBuilders({ cover }: BuilderContext): Partial<BuilderMap> {\n const B: Partial<BuilderMap> = {}\n for (const kind of FLOOR_ITEMS) B[kind] = (M, api) => cover(api, M, kind, { color: M.color })\n return B\n}\n","import { shade } from '../colors'\nimport type { Kit } from '../types'\n\nexport const GREENS = [0x3f7a3a, 0x2f6a30, 0x4f8a42, 0x5a9448]\n/** i-th shade of green (from the `plant` palette role or the defaults), alternating darker. */\nexport const green = (K: Kit, i: number): number => {\n const v = K.color('plant', GREENS[i % GREENS.length])\n return i % 2 ? shade(v, 0.85) : v\n}\nexport const pick = <T>(K: Kit, L: T[]): T => L[Math.floor(K.rnd() * L.length) % L.length]\n\n// flat TV: panel, screen, back box and either a stand (two feet) or a wall bracket; returns the panel height\nexport const drawTv = (K: Kit, x: number, y: number, z: number, w: number, stand: boolean): number => { const h = w * 0.5625, yb = stand ? y + 0.07 : y;\n K.box('plain', x, yb, z, w, h, 0.035, 0x101112); K.box('metal', x, yb + 0.015, z + 0.0175, w - 0.03, h - 0.03, 0.004, 0x0a0c10); K.box('plain', x, yb + h * 0.2, z - 0.035, w * 0.5, h * 0.55, 0.04, 0x1a1a1a);\n if (stand) { for (const s of [-1, 1]) K.box('metal', x + s * w * 0.33, y, z, 0.1, 0.07, 0.22, 0x1a1a1a); } else K.box('metal', x, yb + h * 0.35, z - 0.065, w * 0.3, 0.03, 0.03, 0x2a2a2a); return h; };\n","import type { BuilderContext, BuilderMap } from '../types'\nimport { shade } from '../colors'\nimport { drawTv, pick } from './shared'\n\nexport function furnitureBuilders({ kit, atlas }: BuilderContext): Partial<BuilderMap> {\n const B: Partial<BuilderMap> = {}\n B.breakfast_bar = (M, a) => { const K = kit(M, a), { w, d } = K.dim('breakfast_bar'), zb = -d / 2, cd = 0.65, body = K.color('wall_kitchen', 0xf0eee8), wood = K.wood(0xa07a50), paving = 0xf4f4f0;\n K.box('plain', 0, 0, zb + cd / 2 - 0.03, w - 0.06, 0.08, cd - 0.08, 0x2a2a2a); K.box('plain', 0, 0.08, zb + cd / 2, w, 0.8, cd, body);\n for (let i = 0; i < 4; i++) { K.box('wood', -w / 2 + (i + 0.5) * w / 4, 0.12, zb - 0.004, w / 4 - 0.025, 0.72, 0.012, wood); K.box('metal', -w / 2 + (i + 0.5) * w / 4 + (i % 2 ? -0.12 : 0.12), 0.75, zb - 0.012, 0.012, 0.14, 0.016, 0x9a9a9a); }\n K.box('plain', 0, 0.88, zb + (cd + 0.32) / 2, w + 0.04, 0.04, cd + 0.32, paving);\n for (const s of [-1, 1]) K.box('metal', s * (w / 2 - 0.05), 0, zb + cd + 0.24, 0.05, 0.88, 0.05, 0x2a2a2a);\n K.box('metal', -w * 0.22, 0.915, zb + cd / 2, 0.52, 0.006, 0.38, 0xb0b4b8); K.box('metal', -w * 0.22, 0.92, zb + cd / 2, 0.46, 0.004, 0.32, 0x70767a); K.cyl('metal', -w * 0.22, 0.92, zb + 0.1, 0.014, 0.26, 0xb8bcc0); K.box('metal', -w * 0.22, 1.16, zb + 0.15, 0.02, 0.02, 0.1, 0xb8bcc0);\n K.cyl('plain', w * 0.25, 0.92, zb + cd / 2 + 0.2, 0.13, 0.05, 0xf0f0ea); for (let i = 0; i < 3; i++) K.cyl('plain', w * 0.25 + (i - 1) * 0.07, 0.97, zb + cd / 2 + 0.2 + (i % 2) * 0.03, 0.045, 0.09, [0xe8801c, 0xc83030, 0xd8c040][i], 0, 0, 'sphere');\n const stool = K.accent(0x8a5a38); for (let i = 0; i < 3; i++) { const x = (i - 1) * w * 0.3, z = zb + cd + 0.5;\n K.cyl('plain', x, 0.62, z, 0.17, 0.05, stool); K.cyl('metal', x, 0.04, z, 0.02, 0.58, 0x2a2a2a); K.cyl('metal', x, 0, z, 0.17, 0.02, 0x2a2a2a); K.cyl('metal', x, 0.25, z, 0.14, 0.012, 0x2a2a2a); } };\n B.sofa_l = (M, a) => { const K = kit(M, a), { w, d } = K.dim('sofa_l'), c = K.fabric(pick(K, [0x6e7078, 0x8a7a68, 0x45586a])), cd = shade(c, 0.82), zb = -d / 2, pf = 0.95, ac = K.accent(0xc89a4a), cw = 0.95;\n for (const sx of [-1, 1]) for (const sz of [-1, 1]) K.box('wood', sx * (w / 2 - 0.1), 0, sz * (d / 2 - 0.1), 0.05, 0.08, 0.05, K.wood(0x2a1d14));\n K.box('fabric', -(cw / 2), 0.08, zb + pf / 2, w - cw, 0.26, pf, cd); K.box('fabric', w / 2 - cw / 2, 0.08, 0, cw, 0.26, d, cd);\n K.box('fabric', 0, 0.08, zb + 0.11, w, 0.72, 0.22, cd); K.box('fabric', -w / 2 + 0.09, 0.08, zb + pf / 2, 0.18, 0.55, pf, cd);\n const nm = 2, mw = (w - cw - 0.18) / nm; for (let i = 0; i < nm; i++) K.box('fabric', -w / 2 + 0.18 + (i + 0.5) * mw, 0.34, zb + 0.12 + 0.39, mw - 0.02, 0.16, 0.78, c);\n K.box('fabric', w / 2 - cw / 2, 0.34, 0.035, cw - 0.04, 0.16, d - 0.2, c);\n for (let i = 0; i < 4; i++) K.box('fabric', -w / 2 + 0.18 + (i + 0.5) * (w - 0.2) / 4, 0.42, zb + 0.28, (w - 0.2) / 4 - 0.03, 0.4, 0.14, c, 0, 0.15);\n K.box('fabric', -w * 0.3, 0.5, zb + 0.5, 0.4, 0.38, 0.12, ac, 0.3, 0.3); K.box('fabric', w * 0.32, 0.5, zb + 0.5, 0.38, 0.36, 0.12, shade(ac, 0.8), -0.3, 0.3); };\n B.double_bed_modern = (M, a) => { const K = kit(M, a), { w, d } = K.dim('double_bed_modern'), wood = K.wood(0x6a4c34), edr = K.fabric(0x8a8d92), accent = K.accent(0xc89a4a);\n K.box('wood', 0, 0.05, 0.02, w + 0.06, 0.2, d + 0.02, shade(wood, 0.85)); K.box('fabric', 0, 0.25, 0.05, w - 0.04, 0.24, d - 0.1, 0xf2f0ea);\n K.box('fabric', 0, 0.47, 0.35, w, 0.06, d * 0.64, edr); K.box('fabric', 0, 0.5, -0.05, w * 0.98, 0.05, 0.3, shade(edr, 1.12)); K.box('fabric', 0, 0.52, d / 2 - 0.4, w * 0.9, 0.03, 0.3, accent);\n for (const s of [-1, 1]) K.box('fabric', s * w * 0.24, 0.49, -d / 2 + 0.3, w * 0.4, 0.13, 0.36, 0xf4f2ec, s * 0.06);\n K.box('fabric', 0, 0.55, -d / 2 + 0.5, 0.4, 0.16, 0.12, accent, 0, 0.5);\n const hw = w + 0.5, n = Math.round(hw / 0.11); K.box('wood', 0, 0.2, -d / 2 - 0.05, hw, 1.0, 0.04, shade(wood, 0.7)); for (let i = 0; i < n; i++) K.box('wood', -hw / 2 + (i + 0.5) * hw / n, 0.25, -d / 2 - 0.02, hw / n - 0.025, 0.95, 0.04, i % 2 ? wood : shade(wood, 1.12));\n for (const s of [-1, 1]) { K.box('wood', s * (w / 2 + 0.3), 0.35, -d / 2 + 0.22, 0.4, 0.16, 0.36, shade(wood, 1.05)); K.cyl('plain', s * (w / 2 + 0.3), 0.51, -d / 2 + 0.2, 0.06, 0.03, 0xe8e4dc); K.cyl('plain', s * (w / 2 + 0.3), 0.54, -d / 2 + 0.2, 0.012, 0.18, 0xe8e4dc); K.cyl('plain', s * (w / 2 + 0.3), 0.72, -d / 2 + 0.2, 0.09, 0.12, 0xf0e6c8); } };\n B.home_office = (M, a) => { const K = kit(M, a), { w, d } = K.dim('home_office'), wood = K.wood(0xd2b48c), zb = -d / 2;\n K.box('wood', 0, 0.72, 0, w, 0.035, d, wood); for (const s of [-1, 1]) K.box('metal', s * (w / 2 - 0.04), 0, 0, 0.035, 0.72, d - 0.1, 0x222226); K.box('metal', 0, 0.3, zb + 0.05, w - 0.1, 0.03, 0.02, 0x222226);\n K.box('plain', w / 2 - 0.25, 0.28, 0, 0.4, 0.44, d - 0.08, 0xe8e4dc); for (let i = 0; i < 2; i++) { K.box('plain', w / 2 - 0.25, 0.3 + i * 0.2, d / 2 - 0.04, 0.37, 0.18, 0.01, 0xf2eee6); K.box('metal', w / 2 - 0.25, 0.39 + i * 0.2, d / 2 - 0.03, 0.12, 0.012, 0.012, 0x9a9a9a); }\n K.box('metal', 0, 0.755, zb + 0.2, 0.22, 0.012, 0.16, 0x1a1a1a); K.box('metal', 0, 0.76, zb + 0.14, 0.04, 0.2, 0.025, 0x1a1a1a); K.box('plain', 0, 0.88, zb + 0.15, 0.62, 0.36, 0.035, 0x101112); K.box('metal', 0, 0.895, zb + 0.1685, 0.6, 0.33, 0.004, 0x0a0c10);\n K.box('plain', 0, 0.755, zb + 0.43, 0.42, 0.014, 0.14, 0x1a1a1c); K.box('plain', 0.34, 0.755, zb + 0.43, 0.06, 0.025, 0.1, 0x1a1a1c, 0.2);\n K.cyl('metal', -w * 0.4, 0.755, zb + 0.2, 0.07, 0.015, 0x2a2a2a); K.box('metal', -w * 0.4, 0.77, zb + 0.2, 0.012, 0.3, 0.012, 0x2a2a2a, 0, 0.3); K.cyl('plain', -w * 0.4, 1.02, zb + 0.28, 0.06, 0.05, K.main(0xd8d4c8), 0.7, 0);\n K.box('fabric', -0.5, 0.755, 0.05, 0.2, 0.02, 0.28, K.accent(0x2e5e7e), 0.2); K.cyl('plain', -0.3, 0.755, 0.12, 0.04, 0.09, 0xd8d0c0);\n const zc = d / 2 + 0.3, cc = K.fabric(0x2a2a2e); K.box('plain', 0, 0.44, zc, 0.48, 0.07, 0.46, cc); K.box('plain', 0, 0.52, zc + 0.22, 0.44, 0.52, 0.07, cc, 0, 0.12); K.cyl('metal', 0, 0.06, zc, 0.025, 0.38, 0x2a2a2a);\n for (let i = 0; i < 5; i++) { const an = i * 1.2566, x = Math.cos(an) * 0.17, z = zc + Math.sin(an) * 0.17; K.box('metal', Math.cos(an) * 0.085, 0.06, zc + Math.sin(an) * 0.085, 0.18, 0.025, 0.03, 0x2a2a2a, -an); K.cyl('plain', x, 0, z, 0.03, 0.05, 0x1a1a1a); } };\n B.closet = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('closet'), wood = K.color('wall_bedroom', 0xe6e0d4), mw = K.wood(0xb89468), dv = -w * 0.1;\n K.box('plain', 0, 0, -d / 2 + 0.01, w, h, 0.02, shade(wood, 0.95)); for (const s of [-1, 1]) K.box('plain', s * (w / 2 - 0.015), 0, 0, 0.03, h, d, wood); K.box('plain', 0, h - 0.03, 0, w, 0.03, d, wood); K.box('plain', 0, 0.06, 0, w - 0.06, 0.02, d - 0.02, wood); K.box('plain', 0, 0, 0.0, w - 0.04, 0.06, d - 0.06, 0x2a2a2a); K.box('plain', dv, 0.06, 0, 0.025, h - 0.09, d - 0.02, wood);\n const rod = h * 0.76; K.cyl('metal', (-w / 2 + dv) / 2, rod, 0, 0.012, dv + w / 2 - 0.06, 0xb0b4b8, 0, Math.PI / 2); K.box('plain', (-w / 2 + dv) / 2, rod + 0.05, 0, dv + w / 2 - 0.05, 0.02, d - 0.04, wood);\n const nc = 9, L0 = -w / 2 + 0.06, L1 = dv - 0.05, cs = [0x2a2a30, 0xc8c0a8, 0x4a6a8a, 0xa8483a, 0xe0dcd0, 0x6a8a5a, 0x3a3a3a, 0x8a7a68, 0xd8b8a0];\n for (let i = 0; i < nc; i++) { const gh = 0.6 + K.rnd() * 0.5; K.box('fabric', L0 + (i + 0.5) * (L1 - L0) / nc, rod - gh - 0.02, 0, 0.045, gh, 0.4, i % 3 === 0 ? K.accent(cs[i]) : cs[i]); K.box('metal', L0 + (i + 0.5) * (L1 - L0) / nc, rod - 0.04, 0, 0.004, 0.04, 0.2, 0xb0b4b8); }\n const sx = (dv + w / 2) / 2 + dv / 2 + 0.0, rx0 = dv + 0.015, rw = w / 2 - 0.015 - rx0, rc = rx0 + rw / 2;\n for (let i = 0; i < 5; i++) { const y = 0.5 + i * 0.35; K.box('plain', rc, y, 0, rw, 0.022, d - 0.02, wood); if (i < 4) for (let j = 0; j < 3; j++) { const sh = 0.04 + K.rnd() * 0.12; K.box('fabric', rc - rw * 0.3 + j * rw * 0.3, y + 0.022, 0.0, 0.26, sh, 0.32, cs[(i * 3 + j) % cs.length]); } }\n for (let i = 0; i < 2; i++) { K.box('wood', rc, 0.08 + i * 0.2, d / 2 - 0.01, rw - 0.03, 0.18, 0.02, mw); K.box('metal', rc, 0.2 + i * 0.2, d / 2 + 0.005, 0.2, 0.012, 0.014, 0xb0b4b8); } void sx; };\n B.vanity = (M, a) => { const K = kit(M, a), { w, d } = K.dim('vanity'), zb = -d / 2, br = 0xf4f4f0;\n K.cyl('plain', 0, 0, zb + 0.12, 0.08, 0.7, br); K.cyl('plain', 0, 0, zb + 0.12, 0.1, 0.03, br); K.box('plain', 0, 0.74, 0, w, 0.12, d, br); K.box('plain', 0, 0.865, 0.02, w - 0.1, 0.012, d - 0.1, 0xd4d8da); K.cyl('metal', 0, 0.87, 0.05, 0.02, 0.01, 0x70767a);\n K.cyl('metal', 0, 0.87, zb + 0.05, 0.014, 0.1, 0xb8bcc0); K.box('metal', 0, 0.96, zb + 0.09, 0.02, 0.014, 0.09, 0xb8bcc0); K.box('metal', w * 0.3, 0.87, zb + 0.06, 0.04, 0.06, 0.04, 0x2a7a8a);\n K.box('plain', 0, 1.1, zb + 0.012, 0.52, 0.72, 0.024, K.color('metal', 0xe4e0d8)); K.plane('atlas_picture', 0, 1.46, zb + 0.026, 0.46, 0.66, 0xffffff, 0, 0, 0, atlas('picture').mirror());\n K.box('metal', -w / 2 - 0.02, 0.95, zb + 0.06, 0.03, 0.03, 0.03, 0xb0b4b8); K.box('metal', -w / 2 - 0.02, 0.95, zb + 0.06, 0.28, 0.014, 0.014, 0xb0b4b8); K.box('fabric', -w / 2 - 0.02, 0.62, zb + 0.065, 0.26, 0.34, 0.02, K.accent(0x2e6e7e)); };\n B.tv_stand = (M, a) => { const K = kit(M, a), { w, d } = K.dim('tv_stand'), wood = K.wood(0xc8a070), br = K.color('fabric_main', 0xf0ede6);\n for (const sx of [-1, 1]) for (const sz of [-1, 1]) K.box('metal', sx * (w / 2 - 0.08), 0, sz * (d / 2 - 0.06), 0.035, 0.16, 0.035, 0x1a1a1a);\n K.box('wood', 0, 0.16, 0, w, 0.32, d, wood); K.box('plain', 0, 0.48, 0, w + 0.02, 0.025, d + 0.02, br);\n for (let i = 0; i < 2; i++) K.box('plain', -w / 2 + (i + 0.5) * 0.375 * w, 0.18, d / 2 + 0.004, 0.375 * w - 0.02, 0.28, 0.01, shade(br, 0.97)); // two doors on the left\n K.box('plain', w / 4 + w / 8, 0.19, 0, w / 4 - 0.04, 0.2, d - 0.04, 0x1c1c20); K.box('plain', w / 4 + w / 8, 0.22, 0.05, 0.28, 0.06, 0.2, 0x2a2a30); K.box('fabric', w / 4 + w / 8 + 0.03, 0.29, 0.0, 0.2, 0.05, 0.14, K.accent(0x8a4a3a)); // open niche: console and books\n const th = drawTv(K, 0, 0.5, 0, Math.min(1.3, w * 0.78), true); K.box('plain', 0, 0.5, d / 2 - 0.06, Math.min(1.0, w * 0.55), 0.06, 0.06, 0x16161a); void th; };\n return B\n}\n","import { seedOf, shade } from '../colors'\nimport type { BuilderContext, BuilderMap, Material, PropItem, PropsApi } from '../types'\n\nexport function houseStateBuilders({ kit }: BuilderContext): Partial<BuilderMap> {\n const B: Partial<BuilderMap> = {}\n B.window_boards = (M, a) => { const K = kit(M, a), { w, h, d } = K.dim('window_boards'), y = M.y || 1.0, x = M.variant ? M.variant === 'x' : (seedOf(M) & 1) === 0, cs = [0x7a5634, 0x6b4a2a, 0x8a6a44, 0x5e4228], z = d / 2;\n if (x) { const L = Math.hypot(w, h) + 0.15, an = Math.atan2(h, w); for (const s of [-1, 1]) K.box('wood', 0, y + h / 2 - 0.075, z + (s > 0 ? 0 : 0.03), L, 0.15, 0.03, cs[s > 0 ? 0 : 1], 0, 0, s * an + (K.rnd() - 0.5) * 0.05); K.box('wood', 0, y + h * 0.3, z + 0.06, w + 0.3, 0.15, 0.03, cs[2], 0, 0, (K.rnd() - 0.5) * 0.06); for (const [nx, ny] of [[-w / 2, y], [w / 2, y], [-w / 2, y + h], [w / 2, y + h], [0, y + h / 2]]) K.cyl('metal', nx, ny, z + 0.09, 0.012, 0.02, 0x5a5a5e, Math.PI / 2, 0); }\n else { const n = Math.ceil(h / 0.17); for (let i = 0; i < n; i++) { const yy = y + i * (h - 0.15) / (n - 1 || 1), ww = w + 0.2 + K.rnd() * 0.15; K.box('wood', (K.rnd() - 0.5) * 0.1, yy, z + (i % 2) * 0.03, ww, 0.15, 0.03, cs[i % 4], 0, 0, (K.rnd() - 0.5) * 0.05); for (const s of [-1, 1]) K.cyl('metal', s * (w / 2 + 0.02), yy + 0.075, z + (i % 2) * 0.03 + 0.016, 0.01, 0.015, 0x5a5a5e, Math.PI / 2, 0); } } };\n B.barricade = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('barricade'), wood = K.wood(0x6a4a34), z0 = -d / 2;\n K.box('wood', -w * 0.22, 0, z0 + 0.3, w * 0.5, h * 0.8, 0.45, wood, 0.06, -0.12); K.box('plain', -w * 0.22, h * 0.3, z0 + 0.54, 0.3, 0.012, 0.01, 0x9a9a9a);\n K.box('fabric', w * 0.2, 0, z0 + 0.4, w * 0.5, h * 0.95, 0.18, K.fabric(0xc8c0b0), -0.08, -0.28); K.box('fabric', w * 0.2, 0, z0 + 0.4, w * 0.5 - 0.1, h * 0.9, 0.19, K.accent(0x5a7a9a), -0.08, -0.28);\n K.box('plain', w * 0.38, 0, z0 + 0.35, 0.5, 0.4, 0.4, 0xb89a6a, 0.2); K.box('plain', w * 0.4, 0.4, z0 + 0.33, 0.45, 0.35, 0.38, 0xa8885a, -0.3);\n for (let i = 0; i < 4; i++) { K.box('wood', 0, 0.45 + i * 0.3, z0 + 0.62, w + 0.3, 0.14, 0.04, [0x7a5634, 0x8a6a44, 0x6b4a2a, 0x7a5a38][i], 0, 0, (i % 2 ? 1 : -1) * (0.06 + K.rnd() * 0.08)); for (const s of [-1, 1]) K.cyl('metal', s * w * 0.4, 0.45 + i * 0.3, z0 + 0.65, 0.012, 0.02, 0x5a5a5e, Math.PI / 2, 0); }\n K.box('wood', -w * 0.4, 0.0, z0 + 0.9, 0.4, 0.04, 0.4, wood, 0.3, 0, 0.4); K.box('wood', -w * 0.45, 0.0, z0 + 0.7, 0.04, 0.5, 0.04, wood, 0, 0.9, 0.2); };\n B.sandbags = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('sandbags'), cs = [K.fabric(0xb8a678), 0xa89868, 0xc4b284], bw = 0.46, bh = 0.17, nl = Math.max(1, Math.min(4, Math.round(h / bh)));\n for (let l = 0; l < nl; l++) { const nb = Math.max(1, Math.floor((w - l * 0.18) / bw)); for (let i = 0; i < nb; i++) { const x = -nb * bw / 2 + (i + 0.5) * bw + (l % 2) * 0.12; K.box('fabric', x, l * bh * 0.95, (K.rnd() - 0.5) * 0.04, bw * 0.97, bh, Math.min(d, 0.3), cs[(i + l) % 3], (K.rnd() - 0.5) * 0.08, 0, (K.rnd() - 0.5) * 0.04); K.box('fabric', x + bw * 0.46, l * bh * 0.95 + 0.04, 0, 0.03, 0.09, 0.2, shade(cs[(i + l) % 3], 0.8)); } } };\n B.barbed_wire = (M, a) => { const K = kit(M, a), { w } = K.dim('barbed_wire'), np = Math.max(2, Math.round(w / 2) + 1), wire = 0x8a8c90;\n for (let i = 0; i < np; i++) { const x = -w / 2 + i * w / (np - 1); K.box('wood', x, 0, 0, 0.07, 0.6, 0.07, 0x5a4a3a); }\n for (const y of [0.25, 0.4, 0.55]) { K.box('metal', 0, y, 0.04, w, 0.008, 0.008, wire); const nb = Math.floor(w / 0.16); for (let i = 0; i < nb; i++) { const x = -w / 2 + (i + 0.5) * w / nb; K.box('metal', x, y - 0.012, 0.04, 0.05, 0.006, 0.006, wire, 0, 0, 0.8); K.box('metal', x, y - 0.012, 0.04, 0.05, 0.006, 0.006, wire, 0, 0, -0.8); } } };\n B.razor_wire = (M, a) => { const K = kit(M, a), { w } = K.dim('razor_wire'), r = 0.28, wire = 0x9a9ca0, nr = Math.max(3, Math.round(w / 0.22)), ns = 8, sl = 2 * Math.PI * r / ns * 1.05, y0 = M.y || 0;\n for (let i = 0; i < nr; i++) { const x = -w / 2 + (i + 0.5) * w / nr, lean = (i % 2 ? 0.1 : -0.1); for (let s = 0; s < ns; s++) { const th = (s + 0.5) / ns * Math.PI * 2; K.box('metal', x + Math.sin(th) * lean * 0.3, y0 + r + Math.sin(th) * r - 0.006, Math.cos(th) * r, 0.01, 0.012, sl, wire, 0, -(th + Math.PI / 2), lean); } }\n for (const s of [-1, 1]) K.box('metal', s * w / 2, y0, 0, 0.03, 0.12, 0.03, 0x5a5a5e); };\n B.water_bottle_crates = (M, a) => { const K = kit(M, a), { w, d } = K.dim('water_bottle_crates'), pw = 0.4, pd = 0.28, ph = 0.27, nx = Math.max(1, Math.floor(w / pw)), nz = Math.max(1, Math.floor(d / pd)), nl = 2;\n K.box('wood', 0, 0, 0, nx * pw, 0.05, nz * pd, 0x8a6a44);\n for (let l = 0; l < nl; l++) for (let i = 0; i < nx; i++) for (let j = 0; j < nz; j++) { const x = -nx * pw / 2 + (i + 0.5) * pw, z = -nz * pd / 2 + (j + 0.5) * pd, y = 0.05 + l * ph; K.box('plain', x, y, z, pw - 0.01, ph, pd - 0.01, 0xdceaf4); K.box('plain', x, y + 0.08, z, pw - 0.005, 0.08, pd - 0.005, K.accent(0x2a6aa8)); if (l === nl - 1) for (let u = 0; u < 3; u++) for (let v = 0; v < 2; v++) K.cyl('plain', x - 0.11 + u * 0.11, y + ph, z - 0.06 + v * 0.12, 0.028, 0.02, 0x2a6aa8); } };\n B.canned_food = (M, a) => { const K = kit(M, a), y = M.y || 0, cs = [0xc8a030, 0xb03a2a, 0x4a7a3a, 0xd8d0b0, 0x8a3a2a], rw = [4, 3, 2];\n rw.forEach((n, r) => { for (let i = 0; i < n; i++) { const x = (i - (n - 1) / 2) * 0.072, c = cs[(i + r * 2) % cs.length]; K.cyl('plain', x, y + r * 0.1, 0, 0.033, 0.1, c); K.cyl('plain', x, y + r * 0.1 + 0.02, 0, 0.0345, 0.055, 0xe8e4d8); K.cyl('metal', x, y + r * 0.1 + 0.1, 0, 0.031, 0.004, 0xb8bcc0); } });\n K.cyl('plain', 0.3, y + 0.033, 0.1, 0.033, 0.1, 0xc8a030, 0, Math.PI / 2); K.cyl('plain', -0.32, y + 0.033, 0.12, 0.033, 0.1, 0xb03a2a, 0, 1.2); };\n B.water_jug = (M, a) => { const K = kit(M, a), y = M.y || 0, az = K.accent(0x5aa0d8);\n K.cyl('plain', 0, y, 0, 0.13, 0.32, az); K.cyl('plain', 0, y + 0.32, 0, 0.1, 0.05, az); K.cyl('plain', 0, y + 0.37, 0, 0.05, 0.08, az); K.cyl('plain', 0, y + 0.45, 0, 0.055, 0.035, 0x2a5a9a); K.cyl('plain', 0, y + 0.12, 0, 0.1325, 0.1, 0xf4f4f0); K.box('plain', 0.14, y + 0.22, 0, 0.04, 0.12, 0.05, shade(az, 0.85)); };\n B.floor_mattress = (M, a) => { const K = kit(M, a), { w, d } = K.dim('floor_mattress'), h = 0.17, c = K.fabric(0xc8c0b0), ac = K.accent(0x6a4a3a);\n K.box('fabric', 0, 0, 0, w, h, d, c); for (let i = 0; i < 4; i++) K.box('fabric', -w / 2 + (i + 0.5) * w / 4, h, 0, w / 8, 0.003, d - 0.04, shade(c, 0.86)); K.box('fabric', 0, h, -d / 2 + 0.28, w * 0.7, 0.1, 0.38, 0xf0ece0, 0.05);\n K.box('fabric', 0.03, h, 0.3, w * 0.95, 0.05, d * 0.55, ac, 0.05); K.box('fabric', 0.15, h + 0.05, 0.0, w * 0.5, 0.05, 0.4, shade(ac, 1.2), 0.3); K.box('fabric', -0.2, h + 0.05, 0.5, w * 0.3, 0.06, 0.3, shade(ac, 0.8), -0.4); };\n B.lantern = (M, a) => { const K = kit(M, a), y = M.y || 0;\n K.cyl('metal', 0, y, 0, 0.07, 0.03, 0x2a4a2a); K.cyl('emissive', 0, y + 0.03, 0, 0.055, 0.18, 0xffd890); for (const sx of [-1, 1]) for (const sz of [-1, 1]) K.box('metal', sx * 0.05, y + 0.03, sz * 0.05, 0.01, 0.18, 0.01, 0x2a2a2a);\n K.cyl('metal', 0, y + 0.21, 0, 0.075, 0.025, 0x2a4a2a); K.box('metal', 0, y + 0.235, 0, 0.1, 0.012, 0.012, 0x2a2a2a); for (const s of [-1, 1]) K.box('metal', s * 0.05, y + 0.235, 0, 0.01, 0.045, 0.01, 0x2a2a2a);\n if (M.lit !== false) K.light(0, y + 0.15, 0, 0xffc880, 7, 'lantern'); };\n B.candles = (M, a) => { const K = kit(M, a), y = M.y || 0;\n K.cyl('plain', 0, y, 0, 0.09, 0.012, 0xf0f0ea); [[0, 0, 0.16], [0.06, 0.03, 0.1], [-0.05, 0.04, 0.12], [0.02, -0.06, 0.07]].forEach(([x, z, h], i) => { K.cyl('plain', x, y + 0.012, z, 0.017, h, 0xf0e6c8); K.cyl('emissive', x, y + 0.012 + h, z, 0.008, 0.03, i % 2 ? 0xffc060 : 0xffd890, 0, 0, 'sphere'); });\n K.cyl('plain', 0.02, y + 0.012, 0.0, 0.05, 0.008, 0xf0e6c8); if (M.lit !== false) K.light(0, y + 0.22, 0, 0xffb060, 5, 'candles'); };\n B.open_suitcases = (M, a) => { const K = kit(M, a), cs = [K.main(0xb03a34), 0x2a3a5a], rs = [0xc8c0a8, 0x4a6a8a, 0xe0dcd0, 0xa8483a];\n const suitcase = (x: number, z: number, ww: number, dd: number, c: number, ry: number, al: number) => { const lx = x, hz = z - dd / 2;\n K.box('plain', lx, 0, z, ww, 0.16, dd, c, ry); K.box('fabric', lx, 0.12, z, ww - 0.06, 0.05, dd - 0.06, 0xd8d0c0, ry);\n for (let i = 0; i < 5; i++) K.box('fabric', lx + (K.rnd() - 0.5) * (ww - 0.2), 0.16, z + (K.rnd() - 0.5) * (dd - 0.2), 0.14 + K.rnd() * 0.1, 0.04 + K.rnd() * 0.04, 0.12 + K.rnd() * 0.1, rs[i % 4], K.rnd() * 3);\n K.ramp('plain', lx, 0.16, hz, ww, 0.07, dd, c, al); K.box('metal', lx + ww * 0.3, 0.1, z + dd / 2, 0.04, 0.03, 0.01, 0xc0c0c0); };\n suitcase(-0.4, 0, 0.75, 0.5, cs[0], 0.15, 1.35); suitcase(0.45, 0.1, 0.6, 0.42, cs[1], -0.3, 1.2); };\n B.clothes_on_floor = (M, a) => { const K = kit(M, a), { w, d } = K.dim('clothes_on_floor'), cs = [0xc8c0a8, 0x4a6a8a, 0xa8483a, 0xe0dcd0, 0x6a8a5a, 0x2a2a30, K.accent(0xc89a4a)];\n for (let i = 0; i < 9; i++) { const x = (K.rnd() - 0.5) * (w - 0.3), z = (K.rnd() - 0.5) * (d - 0.3), r = K.rnd() * 6, c = cs[i % cs.length], y = 0.012 + i * 0.003;\n if (i % 3 === 0) { K.box('fabric', x, y, z, 0.3, 0.012, 0.36, c, r); for (const s of [-1, 1]) K.box('fabric', x + Math.cos(r) * s * 0.2, y, z - Math.sin(r) * s * 0.2, 0.2, 0.012, 0.09, shade(c, 0.9), r + s * 0.5); }\n else if (i % 3 === 1) { for (const s of [-1, 1]) K.box('fabric', x + s * 0.06, y, z, 0.13, 0.014, 0.5, 0x3a4a6a, r + s * 0.15); } else K.box('fabric', x, y, z, 0.14, 0.01, 0.07, c, r); }\n const sx = (K.rnd() - 0.5) * (w - 0.4), sz = (K.rnd() - 0.5) * (d - 0.4); K.box('plain', sx, 0.012, sz, 0.09, 0.07, 0.27, 0x2a2a2a, 0.4); K.box('plain', sx + 0.15, 0.012, sz + 0.05, 0.09, 0.07, 0.27, 0x2a2a2a, 0.9); };\n B.fallen_chair = (M, a) => { // wooden chair knocked over on its side: the parts are axis-aligned boxes, rotated 90 degrees around z\n const g = Math.PI / 2, dy = 0.23, api = { box: (Mm: PropItem, mat: Material, x: number, y: number, z: number, w: number, h: number, d: number, color: number, ry: number, rx: number, rz?: number) => { const cy = y + h / 2; a.box(Mm, mat, -cy + 0.0, dy + x - h / 2 + 0.0 - 0.0, z, w, h, d, color, ry, rx, g + (rz || 0)); } };\n const K = kit(M, api as unknown as PropsApi), c = K.main(0x6a4a30); // center (x, y, z) becomes (-cy, x): the api y is the base, so h/2 is subtracted\n K.box('wood', 0, 0.44, 0, 0.42, 0.04, 0.42, c); for (const sx of [-1, 1]) for (const sz of [-1, 1]) K.box('wood', sx * 0.18, 0, sz * 0.18, 0.035, 0.44, 0.035, shade(c, 0.85));\n K.box('wood', 0, 0.48, -0.19, 0.42, 0.45, 0.03, c); };\n B.blood_stains = (M, a) => { const K = kit(M, a), { w, d } = K.dim('blood_stains');\n for (let k = 0; k < 7; k++) K.cyl('plain', (K.rnd() - 0.5) * w * 0.3, 0.012 + k * 0.0004, (K.rnd() - 0.5) * d * 0.3, 0.12 + K.rnd() * 0.18, 0.003, k < 2 ? 0x3a0806 : 0x5a100c);\n for (let k = 0; k < 8; k++) { const an = K.rnd() * 6.28, r = 0.3 + K.rnd() * Math.min(w, d) * 0.4; K.cyl('plain', Math.cos(an) * r, 0.013 + k * 0.0002, Math.sin(an) * r, 0.02 + K.rnd() * 0.05, 0.003, 0x4a0a08); }\n if (M.drag !== false) for (let k = 1; k < 6; k++) K.box('plain', 0.3 + k * 0.1, 0.012, 0.15 + k * 0.17, 0.16 - k * 0.015, 0.003, 0.2, 0x4a0c0a, 0.5); };\n B.open_door = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('open_door'), wood = K.wood(0x6a4a2c), angle = M.angle != null ? M.angle : 1.2, L = w - 0.06, hx = -w / 2 + 0.03;\n K.box('wood', -w / 2 - 0.03, 0, 0, 0.06, h + 0.06, d, shade(wood, 0.8)); K.box('wood', w / 2 + 0.03, 0, 0, 0.06, h + 0.06, d, shade(wood, 0.8)); K.box('wood', 0, h, 0, w + 0.12, 0.06, d, shade(wood, 0.8));\n K.box('wood', hx + Math.cos(angle) * L / 2, 0.0, Math.sin(angle) * L / 2, L, h - 0.02, 0.04, wood, -angle); K.cyl('metal', hx + Math.cos(angle) * (L - 0.08), 1.0, Math.sin(angle) * (L - 0.08) + 0.04, 0.025, 0.06, 0xc8b070, Math.PI / 2, 0);\n for (const y of [0.2, 1.0, 1.8]) K.box('metal', hx, y, 0.0, 0.02, 0.1, 0.02, 0x8a8a8a); };\n return B\n}\n","import type { BuilderContext, BuilderMap } from '../types'\nimport { shade } from '../colors'\nimport type { Kit } from '../types'\nimport { green } from './shared'\n\nexport function plantBuilders({ kit }: BuilderContext): Partial<BuilderMap> {\n const B: Partial<BuilderMap> = {}\n const foliage = (K: Kit, x: number, y: number, z: number, n: number, w: number, h0: number, h1: number, tilt0: number, tilt1: number, twist = 2.4) => { for (let i = 0; i < n; i++) { const an = i * twist + K.rnd() * 0.5, t = tilt0 + (tilt1 - tilt0) * K.rnd(); K.leaf(x + Math.cos(an) * 0.03, y, z + Math.sin(an) * 0.03, w * (0.8 + K.rnd() * 0.4), h0 + (h1 - h0) * K.rnd(), green(K, i), an, t); } };\n B.floor_plant = (M, a) => { const K = kit(M, a), { w } = K.dim('floor_plant'), r = Math.min(0.24, w * 0.4), pot = K.main(0xb5603c);\n K.cyl('plain', 0, 0, 0, r, 0.38, pot); K.cyl('plain', 0, 0.34, 0, r * 1.07, 0.05, shade(pot, 0.92)); K.cyl('plain', 0, 0.385, 0, r * 0.92, 0.012, 0x3a2a1e);\n foliage(K, 0, 0.39, 0, 11, 0.2, 0.5, 0.85, 0.2, 0.85); };\n B.table_plant = (M, a) => { const K = kit(M, a), y = M.y || 0, pot = K.main(0xf0eee8);\n K.cyl('plain', 0, y, 0, 0.05, 0.08, pot); K.cyl('plain', 0, y + 0.08, 0, 0.045, 0.008, 0x3a2a1e); foliage(K, 0, y + 0.085, 0, 7, 0.07, 0.1, 0.2, 0.2, 0.9); };\n B.hanging_plant = (M, a) => { const K = kit(M, a), t = K.ceiling(), h = K.dim('hanging_plant').h, yp = t - h, pot = K.main(0xf0eee8), rope = K.accent(0xd8ccb0);\n K.box('metal', 0, t - 0.025, 0, 0.03, 0.025, 0.03, 0x8a8a8a); for (let i = 0; i < 3; i++) { const an = i * 2.09; K.box('fabric', Math.cos(an) * 0.08, yp + 0.12, Math.sin(an) * 0.08, 0.01, t - yp - 0.15, 0.01, rope); }\n K.cyl('plain', 0, yp, 0, 0.1, 0.14, pot); K.cyl('plain', 0, yp + 0.135, 0, 0.09, 0.01, 0x3a2a1e);\n for (let i = 0; i < 11; i++) { const an = i * 2.4, up = i < 3; K.leaf(Math.cos(an) * 0.06, yp + 0.14, Math.sin(an) * 0.06, 0.09, up ? 0.22 : 0.4 + K.rnd() * 0.3, green(K, i), an, up ? 0.5 : 2.3 + K.rnd() * 0.3); } };\n return B\n}\n","import type { BuilderContext, BuilderMap } from '../types'\nimport type { PropType } from '../catalog'\nimport { hex, shade } from '../colors'\nimport type { Kit } from '../types'\nimport { drawTv, pick } from './shared'\n\nexport function wallBuilders({ kit, atlas }: BuilderContext): Partial<BuilderMap> {\n const B: Partial<BuilderMap> = {}\n B.wall_ac = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('wall_ac'), y = M.y || 2.15;\n K.box('plain', 0, y, 0, w, h, d, 0xf0f0ee); K.box('plain', 0, y + h - 0.02, d * 0.1, w - 0.04, 0.02, d * 0.8, 0xe6e6e2); // curved cover\n K.box('plain', 0, y + 0.02, d / 2 - 0.02, w - 0.1, 0.035, 0.04, 0x30343a); K.box('plain', 0, y - 0.005, d / 2 - 0.03, w - 0.12, 0.012, 0.07, 0xe6e6e2, 0, 0.45); // slot and flap\n K.box('plain', w / 2 - 0.1, y + h - 0.1, d / 2 + 0.001, 0.05, 0.02, 0.004, 0x2a3a30); K.box('plain', -w / 2 + 0.01, y + 0.02, 0.0, 0.01, h - 0.04, d - 0.02, 0xdcdcd8); };\n const frame = (K: Kit, id: PropType, def: number, large: boolean) => { const { w, d, h } = K.dim(id), y = K.M.y || def, fr = Math.min(w, h) * 0.07 + 0.008, margin = large ? 0.05 : 0.035, A = atlas('picture'), idx = K.M.art != null ? K.M.art % 7 : Math.floor(K.rnd() * 7);\n const cm = K.color('picture_accent', pick(K, [0x4a3324, 0x1c1c1c, 0xe8e2d4, 0x8a6a44])); K.box('wood', 0, y, 0, w, h, d, K.M.color != null ? hex(K.M.color) : cm);\n K.box('plain', 0, y + fr, d / 2 + 0.0005, w - 2 * fr, h - 2 * fr, 0.004, 0xf4f1e8); K.plane('atlas_picture', 0, y + h / 2, d / 2 + 0.005, w - 2 * fr - 2 * margin, h - 2 * fr - 2 * margin, 0xffffff, 0, 0, 0, A.uv(idx)); };\n B.picture_small = (M, a) => frame(kit(M, a), 'picture_small', 1.3, false);\n B.picture_large = (M, a) => frame(kit(M, a), 'picture_large', 1.2, true);\n B.mirror = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('mirror'), y = M.y || 0.9, A = atlas('picture'), cm = K.color('metal', 0x3a3a3c);\n K.box('metal', 0, y, 0, w, h, d, M.color != null ? hex(M.color) : cm); K.plane('atlas_picture', 0, y + h / 2, d / 2 + 0.003, w - 0.05, h - 0.05, 0xffffff, 0, 0, 0, A.mirror()); };\n B.curtain = (M, a) => { const K = kit(M, a), { w, h } = K.dim('curtain'), y = M.y != null && M.y > 0 ? M.y : 0.1, color = K.fabric(pick(K, [0xd8cdb4, 0x9fb0be, 0xc5a88c, 0xe0dcd0])), isOpen = M.open !== false, top = y + h;\n K.box('metal', 0, top, 0, w + 0.1, 0.025, 0.025, K.metal(0x5a5048)); for (const s of [-1, 1]) K.cyl('metal', s * (w / 2 + 0.07), top - 0.02, 0, 0.03, 0.07, 0x5a5048, 0, 0, 'sphere');\n const pw = isOpen ? Math.min(0.5, w * 0.3) : w / 2 + 0.03, n = Math.max(4, Math.round(pw / 0.08));\n for (const s of [-1, 1]) for (let i = 0; i < n; i++) { const ux = isOpen ? s * (w / 2 + 0.03 - pw + (i + 0.5) * pw / n) : s * ((i + 0.5) * pw / n); K.box('fabric', ux, y, (i % 2 ? 0.025 : -0.01), pw / n * 1.15, h - 0.02, 0.02, i % 2 ? shade(color, 0.9) : color); }\n if (isOpen) for (const s of [-1, 1]) K.box('fabric', s * (w / 2 - pw * 0.25), y + h * 0.4, 0.04, pw * 0.5, 0.05, 0.02, shade(color, 0.7)); }; // tie-back\n B.blinds = (M, a) => { const K = kit(M, a), { w, h } = K.dim('blinds'), y = M.y || 1.0, retract = M.retracted != null ? M.retracted : 0.15, isWood = M.variant === 'wood', color = isWood ? K.wood(0x9a7a52) : K.color('wall_living', 0xe8e2d4), mat = isWood ? 'wood' : 'plain', yb = y + h * retract, n = Math.floor((y + h - yb) / 0.055);\n K.box('plain', 0, y + h - 0.05, 0, w, 0.05, 0.06, 0xdcd8d0); for (let i = 0; i < n; i++) K.box(mat, 0, y + h - 0.06 - (i + 1) * 0.055, 0.0, w - 0.05, 0.01, 0.035, color, 0, 0.3);\n for (const s of [-1, 1]) K.box('plain', s * (w / 2 - 0.05), yb, 0.02, 0.008, y + h - 0.05 - yb, 0.008, 0xb8b4ac); K.box('plain', 0, yb - 0.02, 0, w - 0.03, 0.025, 0.04, 0xdcd8d0); };\n B.flat_tv = (M, a) => { const K = kit(M, a), { w } = K.dim('flat_tv'); drawTv(K, 0, M.y != null && M.y > 0 ? M.y : 1.0, 0, w, false); };\n return B\n}\n","import type { BuilderContext, BuilderMap } from '../types'\nimport { hex, shade } from '../colors'\nimport { green, pick } from './shared'\n\nexport function yardBuilders({ kit, cover }: BuilderContext): Partial<BuilderMap> {\n const B: Partial<BuilderMap> = {}\n B.ac_condenser = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('ac_condenser'), y = M.y != null && M.y > 0 ? M.y : 0.5;\n for (const s of [-1, 1]) { K.box('metal', s * w * 0.32, y - 0.06, -d / 2 - 0.2, 0.04, 0.04, 0.3, 0x6a6a6a); K.box('metal', s * w * 0.32, y - 0.5 + 0.06, -d / 2 - 0.2 - 0.0, 0.035, 0.46, 0.035, 0x6a6a6a, 0, 0.0); }\n K.box('plain', 0, y, 0, w, h, d, 0xe8e8e4); K.box('plain', 0, y + h - 0.02, 0, w + 0.02, 0.025, d + 0.02, 0xd8d8d4);\n K.cyl('metal', -w * 0.12, y + h / 2, d / 2 + 0.004, 0.2, 0.012, 0x2a2a2e, Math.PI / 2, 0); K.cyl('metal', -w * 0.12, y + h / 2, d / 2 + 0.012, 0.085, 0.02, 0x4a4a4e, Math.PI / 2, 0);\n for (let i = 0; i < 4; i++) K.box('metal', -w * 0.12, y + h / 2 - 0.2, d / 2 + 0.018, 0.4, 0.008, 0.008, 0x8a8a8a, 0, 0, i * 0.785);\n for (let i = 0; i < 3; i++) K.cyl('metal', -w * 0.12, y + h / 2, d / 2 + 0.02, 0.07 + i * 0.043, 0.006, 0x7a7a7e, Math.PI / 2, 0);\n for (let i = 0; i < 6; i++) K.box('plain', w * 0.33, y + 0.06 + i * 0.075, d / 2 + 0.003, 0.14, 0.03, 0.006, 0xc4c4c0);\n K.cyl('metal', w / 2 - 0.05, y + 0.1, -d / 2 - 0.08, 0.012, 0.18, 0xb8733c, Math.PI / 2, 0); K.box('plain', w / 2 - 0.05, y + 0.06, -d / 2 - 0.02, 0.05, 0.12, 0.05, 0xe0e0dc); };\n B.pool = (M, a) => { const K = kit(M, a), { w, d } = K.dim('pool'), t = 0.32, bo = K.color('exterior_floor', 0xe8e4da);\n K.box('plain', 0, 0, -d / 2 - t / 2, w + 2 * t, 0.1, t, bo); K.box('plain', 0, 0, d / 2 + t / 2, w + 2 * t, 0.1, t, bo); for (const s of [-1, 1]) K.box('plain', s * (w / 2 + t / 2), 0, 0, t, 0.1, d, bo);\n cover(a, M, 'pool_tile', { w, d, y: 0.035, size: 1.0 });\n K.plane('water', 0, 0.075, 0, w, d, 0x8fd4ee, 0, -Math.PI / 2, 0);\n for (let i = 0; i < 3; i++) K.box('plain', w / 2 - 0.35 + i * 0.0, 0.035 + i * 0.01, d / 2 - 0.2 - i * 0.28, 0.7 - i * 0.05, 0.03, 0.26, 0xdfeef2);\n for (const s of [-1, 1]) { K.box('metal', -w / 4 + s * 0.2, 0.0, d / 2 + t * 0.6, 0.03, 0.85, 0.03, 0xc8ccd0); K.box('metal', -w / 4 + s * 0.2, 0.85, d / 2 + t * 0.6 - 0.1, 0.03, 0.03, 0.22, 0xc8ccd0); K.box('metal', -w / 4 + s * 0.2, 0.0, d / 2 - 0.25, 0.03, 0.88, 0.03, 0xc8ccd0, 0, 0.0); }\n for (let i = 0; i < 3; i++) K.box('metal', -w / 4, 0.02, d / 2 - 0.08 - i * 0.07, 0.4, 0.015, 0.05, 0xc8ccd0); };\n B.deck = (M, a) => { const K = kit(M, a), { w, d } = K.dim('deck'), h = 0.2, wood = K.wood(0x6a4a30);\n K.box('wood', 0, 0, 0, w - 0.05, h, d - 0.05, shade(wood, 0.7)); cover(a, M, 'deck_floor', { w, d, y: h + 0.012, size: 1.2, color: M.color });\n for (const [x, z, ww, dd] of [[0, -d / 2, w, 0.05], [0, d / 2, w, 0.05], [-w / 2, 0, 0.05, d], [w / 2, 0, 0.05, d]]) K.box('wood', x, 0, z, ww, h + 0.02, dd, wood);\n K.box('wood', 0, 0, d / 2 + 0.17, 1.4, 0.1, 0.34, wood); K.box('wood', 0, 0.1, d / 2 + 0.17, 1.4, 0.012, 0.34, shade(wood, 1.2)); };\n B.sun_lounger = (M, a) => { const K = kit(M, a), { w } = K.dim('sun_lounger'), fr = K.metal(0xe8e8e4), al = 0.95, cu = K.fabric(0x3a6a9a), stripe = K.accent(0xf0ece0);\n for (const s of [-1, 1]) { K.box('metal', s * (w / 2 - 0.03), 0.17, -0.3, 0.035, 0.035, 1.35, fr); K.box('metal', s * (w / 2 - 0.03), 0.0, 0.95, 0.035, 0.2, 0.035, fr); K.box('metal', s * (w / 2 - 0.03), 0.0, -0.3, 0.035, 0.2, 0.035, fr); K.cyl('plain', s * (w / 2 - 0.03), 0.07, -0.3, 0.07, 0.03, 0x2a2a2a, 0, Math.PI / 2); }\n K.box('fabric', 0, 0.21, 0.35, w - 0.1, 0.06, 1.3, cu); K.ramp('fabric', 0, 0.2, -0.3, w - 0.1, 0.06, 0.8, cu, al);\n for (let i = 0; i < 4; i++) { K.box('fabric', 0, 0.265, 0.0 + i * 0.3, w - 0.098, 0.004, 0.08, stripe); } K.ramp('metal', w / 2 - 0.03, 0.18, -0.3, 0.03, 0.03, 0.5, fr, al - 0.3); K.ramp('metal', -w / 2 + 0.03, 0.18, -0.3, 0.03, 0.03, 0.5, fr, al - 0.3); };\n B.garden_bed = (M, a) => { const K = kit(M, a), { w, d } = K.dim('garden_bed'), wood = K.wood(0x9a7048), hb = 0.3;\n for (const s of [-1, 1]) { K.box('wood', 0, 0, s * (d / 2 - 0.015), w, hb, 0.03, wood); K.box('wood', s * (w / 2 - 0.015), 0, 0, 0.03, hb, d - 0.06, shade(wood, 0.9)); }\n K.box('plain', 0, 0.02, 0, w - 0.06, hb - 0.05, d - 0.06, 0x3a2a1e);\n const lin = d > 0.8 ? 3 : 2, n = Math.max(3, Math.round(w / 0.4));\n for (let r = 0; r < lin; r++) for (let i = 0; i < n; i++) { const x = -w / 2 + (i + 0.5) * w / n, z = (r - (lin - 1) / 2) * 0.3;\n if (r === 0) { for (let j = 0; j < 6; j++) K.leaf(x, hb - 0.02, z, 0.11, 0.14, green(K, j) , j * 1.05, 0.9); }\n else if (r === 1) { K.box('wood', x, hb - 0.03, z, 0.015, 0.75, 0.015, 0x8a7048); K.cyl('plant', x, hb + 0.05, z, 0.1, 0.28, green(K, i), 0, 0, 'sphere'); K.cyl('plain', x + 0.05, hb + 0.18, z + 0.07, 0.03, 0.06, 0xc83030, 0, 0, 'sphere'); K.cyl('plain', x - 0.06, hb + 0.3, z + 0.05, 0.03, 0.06, 0xd04030, 0, 0, 'sphere'); }\n else { K.cyl('plant', x, hb - 0.02, z, 0.1, 0.17, 0x8ab05a, 0, 0, 'sphere'); for (let j = 0; j < 5; j++) K.leaf(x, hb, z, 0.14, 0.2, green(K, j + 1), j * 1.3, 1.0); } } };\n B.fruit_tree = (M, a) => { const K = kit(M, a), { w, h } = K.dim('fruit_tree'), fruit = M.fruit || pick(K, [0xe8801c, 0xe8a030, 0xb8c050, 0xc83030, 0xd8d040]), tr = K.wood(0x6a4a30);\n K.cyl('wood', 0, 0, 0, 0.13, h * 0.5, tr); K.box('wood', 0.2, h * 0.35, 0, 0.08, 0.6, 0.08, tr, 0, 0, -0.7); K.box('wood', -0.2, h * 0.35, 0.05, 0.08, 0.55, 0.08, tr, 0, 0, 0.7);\n const bl = [[0, h * 0.72, 0, 1.0], [0.55, h * 0.62, 0.2, 0.7], [-0.55, h * 0.64, -0.1, 0.72], [0.1, h * 0.6, -0.55, 0.65], [0.1, h * 0.9, 0.1, 0.6]];\n bl.forEach(([x, y, z, r], i) => K.cyl('plant', x * w / 2.4, y - r * 0.4, z * w / 2.4, r * w / 2.4, r * 1.5 * w / 2.4, green(K, i), 0, 0, 'sphere'));\n for (let i = 0; i < 16; i++) { const b = bl[i % 4], an = K.rnd() * 6.28, el = K.rnd() * 0.9, r = b[3] * w / 2.4 * 0.95; K.cyl('plain', b[0] * w / 2.4 + Math.cos(an) * r * Math.cos(el), b[1] * 1.0 - b[3] * 0.4 + Math.sin(el) * r * 0.5, b[2] * w / 2.4 + Math.sin(an) * r * Math.cos(el), 0.055, 0.11, fruit, 0, 0, 'sphere'); } };\n B.palm_tree = (M, a) => { const K = kit(M, a), { h } = K.dim('palm_tree'), tr = K.wood(0x8a7a5a), lean = 0.35, n = 9, sh = h * 0.78 / n; let x = 0, y = 0;\n for (let i = 0; i < n; i++) { const r = 0.14 - i * 0.006; K.cyl('wood', x, y, 0, r, sh, i % 2 ? tr : shade(tr, 0.82)); y += sh; x += lean * (2 * i + 1) / (n * n) * 1.2; }\n const top = y;\n for (let i = 0; i < 10; i++) { const an = i * 0.628 + K.rnd() * 0.2, t1 = 0.7 + K.rnd() * 0.4, L = 1.5, w = 0.5; K.leaf(x, top, 0, w, L, green(K, i), an, t1);\n const ex = x + Math.sin(t1) * Math.sin(an) * L, ey = top + Math.cos(t1) * L, ez = Math.sin(t1) * Math.cos(an) * L; K.leaf(ex, ey, ez, w * 0.7, 0.9, green(K, i + 1), an, t1 + 1.0); }\n for (let i = 0; i < 3; i++) K.cyl('wood', x + Math.cos(i * 2.1) * 0.14, top - 0.2, Math.sin(i * 2.1) * 0.14, 0.09, 0.2, 0x6a5a3a, 0, 0, 'sphere'); };\n B.pergola = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('pergola'), wood = M.variant === 'dark' ? shade(K.wood(0x6a4a30), 0.8) : K.color('exterior_floor', 0xe8e2d4), pc = M.color != null ? hex(M.color) : wood;\n for (const sx of [-1, 1]) for (const sz of [-1, 1]) K.box('wood', sx * (w / 2 - 0.1), 0, sz * (d / 2 - 0.1), 0.14, h, 0.14, pc);\n for (const sz of [-1, 1]) K.box('wood', 0, h - 0.04, sz * (d / 2 - 0.1), w + 0.2, 0.16, 0.08, pc); const n = Math.round(w / 0.4);\n for (let i = 0; i <= n; i++) K.box('wood', -w / 2 - 0.1 + i * (w + 0.2) / n, h + 0.12, 0, 0.06, 0.08, d + 0.3, shade(pc, 0.92));\n for (let i = 0; i < 9; i++) K.leaf(-w / 2 + 0.1 + (i % 2) * 0.06, 0.2 + i * 0.25, -d / 2 + 0.1, 0.2, 0.28, green(K, i), i * 1.7, 0.7); for (let i = 0; i < 7; i++) K.leaf(-w / 2 + 0.2 + i * 0.35, h + 0.2, -d / 4 + K.rnd() * 0.3, 0.22, 0.3, green(K, i), i * 2.0, 1.3); };\n B.slide = (M, a) => { const K = kit(M, a), { d, h } = K.dim('slide'), py = h * 0.8, pz = -d / 2 + 0.55, az = K.accent(0xe8c030), vm = K.main(0xc8403a), az2 = 0x3a6ab0;\n for (const sx of [-1, 1]) for (const sz of [-1, 1]) K.box('metal', sx * 0.3, 0, pz + sz * 0.3, 0.05, py, 0.05, az2);\n K.box('plain', 0, py, pz, 0.74, 0.05, 0.74, vm); for (const s of [-1, 1]) { K.box('plain', s * 0.35, py + 0.05, pz, 0.04, 0.42, 0.74, az); }\n K.box('plain', 0, py + 0.05, pz - 0.35, 0.74, 0.42, 0.04, az); const run = d / 2 - pz - 0.4, al = Math.atan2(py - 0.1, run);\n K.ramp('plain', 0, py, pz + 0.37, 0.5, 0.04, Math.hypot(run, py - 0.1) + 0.1, 0xd8d0c0, -al + 0.0 + 0); for (const s of [-1, 1]) K.ramp('plain', s * 0.27, py + 0.03, pz + 0.37, 0.04, 0.12, Math.hypot(run, py - 0.1) + 0.1, az, -al);\n for (const s of [-1, 1]) K.box('metal', s * 0.28, 0, pz - 0.45, 0.04, py + 0.1, 0.04, az2, 0, -0.1 + 0); for (let i = 0; i < 5; i++) K.box('metal', 0, 0.2 + i * (py - 0.2) / 5, pz - 0.46, 0.56, 0.03, 0.03, 0xc8ccd0); K.box('plain', 0, py + 0.4, pz, 0.8, 0.04, 0.8, vm); };\n B.swing = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('swing'), mt = K.color('metal', 0x3a6ab0), L = Math.hypot(h, d / 2), an = Math.atan2(d / 2, h);\n for (const sx of [-1, 1]) for (const sz of [-1, 1]) K.box('metal', sx * w / 2, h / 2 - L / 2 + 0.0, sz * d / 4, 0.07, L, 0.07, mt, 0, -sz * an, 0);\n K.box('metal', 0, h - 0.04, 0, w + 0.1, 0.1, 0.1, shade(mt, 0.85)); const cs = [K.accent(0xc8403a), 0x2a2a2a];\n for (let i = 0; i < 2; i++) { const x = (i - 0.5) * w * 0.45, sy = 0.5, sw = 0.46; for (const s of [-1, 1]) K.box('metal', x + s * (sw / 2 - 0.02), sy, 0, 0.012, h - 0.05 - sy, 0.012, 0xa0a4a8); K.box('plain', x, sy - 0.04, 0, sw, 0.045, 0.17, cs[i]); } };\n B.bbq_simple = (M, a) => { const K = kit(M, a), { w, d } = K.dim('bbq_simple'), br = K.color('exterior_floor', 0xb25a40), hb = 0.78;\n K.box('plain', 0, 0, 0, w, 0.45, d, br); for (const [x, z, ww, dd] of [[0, -d / 2 + 0.06, w, 0.12], [0, d / 2 - 0.06, w, 0.12], [-w / 2 + 0.06, 0, 0.12, d - 0.2], [w / 2 - 0.06, 0, 0.12, d - 0.2]]) K.box('plain', x, 0.45, z, ww, hb - 0.45, dd, br);\n K.box('plain', 0, 0.45, 0, w - 0.22, 0.04, d - 0.22, 0x2a2624); for (let i = 0; i < 10; i++) K.box('plain', (K.rnd() - 0.5) * (w - 0.3), 0.49, (K.rnd() - 0.5) * (d - 0.3), 0.08, 0.02, 0.07, i % 3 ? 0xc84a1a : 0x3a2a24, K.rnd() * 3);\n for (let i = 0; i < 7; i++) K.box('metal', -w / 2 + 0.16 + i * (w - 0.32) / 6, hb + 0.01, 0, 0.012, 0.012, d - 0.1, 0x5a5a5e); K.box('metal', 0, hb + 0.01, -d / 2 + 0.2, w - 0.1, 0.012, 0.02, 0x5a5a5e);\n for (let i = 0; i < 3; i++) { const z = (i - 1) * 0.15; K.cyl('metal', 0, hb + 0.05, z, 0.006, w + 0.12, 0xb0b4b8, 0, Math.PI / 2); for (let j = 0; j < 3; j++) K.cyl('plain', -w * 0.25 + j * w * 0.22, hb + 0.05, z, 0.035, 0.07, [0x8a3a2a, 0xa8483a, 0xd8b090][j], 0, Math.PI / 2); }\n for (let i = 0; i < 4; i++) K.cyl('wood', w / 2 + 0.2, 0.05 + (i > 2 ? 0.1 : 0), -0.1 + (i % 3) * 0.1, 0.04, 0.3, 0x8a6a44, 0, Math.PI / 2); };\n B.garage_car = (M, a) => { const K = kit(M, a), { w, d } = K.dim('garage_car'), c = K.main(pick(K, [0xa8aaac, 0xe8e8e6, 0x2a3a5a, 0xa83a34, 0x222426])), zf = d / 2;\n K.box('plain', 0, 0.26, 0, w, 0.5, d, c); K.box('plain', 0, 0.5, zf - 0.7, w - 0.04, 0.1, 1.4, shade(c, 1.04), 0, 0.04); K.box('plain', 0, 0.76, -zf + 0.3, w - 0.04, 0.1, 0.6, c);\n const cz = -0.35; for (const sx of [-1, 1]) for (const sz of [-1, 1]) K.box('plain', sx * (w / 2 - 0.1), 0.76, cz + sz * 0.8, 0.07, 0.5, 0.07, shade(c, 0.9)); K.box('plain', 0, 1.26, cz, w - 0.1, 0.05, 1.75, shade(c, 1.04));\n K.box('glass', 0, 0.76, cz, w - 0.06, 0.5, 1.6, 0xffffff); K.box('glass', 0, 0.76, cz + 0.8, w - 0.12, 0.5, 0.04, 0xffffff, 0, 0.5); for (const s of [-1, 1]) { K.box('plain', s * 0.4, 0.55, cz + 0.4, 0.4, 0.3, 0.4, 0x2a2a2e); K.box('plain', s * 0.4, 0.55, cz - 0.45, 0.4, 0.3, 0.4, 0x2a2a2e); }\n for (const sx of [-1, 1]) for (const sz of [-1, 1]) { K.cyl('plain', sx * (w / 2 - 0.1), 0.3, sz * 1.3, 0.3, 0.2, 0x1a1a1a, 0, Math.PI / 2); K.cyl('metal', sx * (w / 2 - 0.0), 0.3, sz * 1.3, 0.17, 0.02, 0xb8bcc0, 0, Math.PI / 2); }\n for (const s of [-1, 1]) { K.box('plain', s * 0.62, 0.5, zf - 0.02, 0.32, 0.12, 0.05, 0xf4f0d8); K.box('plain', s * 0.62, 0.52, -zf + 0.02, 0.3, 0.1, 0.05, 0xb02a24); K.box('plain', s * (w / 2 + 0.03), 0.9, cz + 0.55, 0.1, 0.1, 0.14, shade(c, 0.9)); }\n K.box('plain', 0, 0.2, zf - 0.01, w, 0.16, 0.06, 0x2a2a2a); K.box('plain', 0, 0.2, -zf + 0.01, w, 0.16, 0.06, 0x2a2a2a); K.box('plain', 0, 0.32, zf + 0.02, 0.5, 0.16, 0.02, 0x1c1c1c); K.box('plain', 0, 0.28, -zf - 0.015, 0.4, 0.12, 0.01, 0xf0e8c0); };\n B.gourmet_counter = (M, a) => { const K = kit(M, a), { w, d } = K.dim('gourmet_counter'), br = K.color('exterior_floor', 0xa8553c), zc = -d / 2 + 0.35, xa = -w / 2 + 0.8, xb = xa + 0.8 + 0.72, xc = xb + 0.72 + 0.65, wood = K.wood(0x8a5a38), gr = 0x4a4844, cab = K.color('wall_kitchen', 0xe8e4dc);\n K.box('plain', xa, 0, zc, 1.6, 0.85, 0.7, br); for (let i = 1; i < 4; i++) K.box('plain', xa, i * 0.2, zc + 0.351, 1.6, 0.012, 0.004, shade(br, 0.7)); K.box('plain', xa, 0.12, zc + 0.352, 0.7, 0.4, 0.004, 0x1a1612);\n for (const [x, z, ww, dd] of [[xa, zc - 0.25, 1.6, 0.2], [xa, zc + 0.3, 1.6, 0.1], [xa - 0.75, zc, 0.1, 0.7], [xa + 0.75, zc, 0.1, 0.7]]) K.box('plain', x, 0.85, z, ww, 0.12, dd, br);\n K.box('plain', xa, 0.85, zc + 0.02, 1.4, 0.025, 0.4, 0x2a2624); for (let i = 0; i < 8; i++) K.box('metal', xa - 0.62 + i * 0.18, 0.97, zc + 0.02, 0.012, 0.012, 0.4, 0x5a5a5e); for (let i = 0; i < 6; i++) K.box('plain', xa + (K.rnd() - 0.5) * 1.2, 0.88, zc + (K.rnd() - 0.5) * 0.3, 0.1, 0.02, 0.08, i % 2 ? 0xc84a1a : 0x3a2a24, K.rnd() * 3);\n K.box('plain', xa, 1.5, zc - 0.07, 1.5, 0.4, 0.56, br); K.box('plain', xa, 1.5, zc + 0.215, 1.5, 0.4, 0.01, 0x1a1612); K.box('plain', xa, 1.9, zc - 0.12, 0.62, 1.0, 0.46, br); K.box('plain', xa, 2.9, zc - 0.12, 0.76, 0.08, 0.6, shade(br, 0.8)); K.box('plain', xa, 2.98, zc - 0.12, 0.5, 0.06, 0.4, shade(br, 0.75));\n K.box('plain', xb, 0, zc, 1.44, 0.85, 0.7, cab); for (let i = 0; i < 3; i++) K.box('wood', xb - 0.48 + i * 0.48, 0.08, zc + 0.355, 0.44, 0.7, 0.012, wood); K.box('plain', (xb + xc) / 2, 0.85, zc, 0.72 * 2 + 0.65 + 0.04, 0.04, 0.74, gr);\n K.box('metal', xb, 0.89, zc, 0.5, 0.01, 0.38, 0xb0b4b8); K.box('metal', xb, 0.895, zc, 0.44, 0.006, 0.32, 0x70767a); K.cyl('metal', xb, 0.89, zc - 0.28, 0.014, 0.3, 0xb8bcc0); K.box('metal', xb, 1.17, zc - 0.2, 0.02, 0.02, 0.11, 0xb8bcc0);\n K.box('plain', xc, 0, zc, 1.3, 0.85, 0.7, cab); for (let i = 0; i < 2; i++) K.box('wood', xc - 0.32 + i * 0.64, 0.08, zc + 0.355, 0.6, 0.7, 0.012, wood);\n K.cyl('plain', xc, 0.89, zc, 0.1, 0.05, 0xf0f0ea); K.cyl('plain', xc + 0.4, 0.89, zc, 0.04, 0.26, 0x2a6a3a);\n const tz = 0.55, tl = Math.min(2.6, w * 0.65); K.box('wood', 0, 0.74, tz, tl, 0.05, 0.85, wood); for (const s of [-1, 1]) { K.box('wood', s * (tl / 2 - 0.2), 0, tz, 0.08, 0.74, 0.7, shade(wood, 0.8)); K.box('wood', s * (tl / 2 - 0.2), 0.3, tz - 0.001, 0.0 + 0.6, 0.04, 0.05, shade(wood, 0.75)); }\n for (const s of [-1, 1]) { K.box('wood', 0, 0.42, tz + s * 0.72, tl - 0.2, 0.04, 0.3, shade(wood, 1.1)); for (const sx of [-1, 1]) K.box('wood', sx * (tl / 2 - 0.4), 0, tz + s * 0.72, 0.06, 0.42, 0.26, shade(wood, 0.8)); }\n K.cyl('plain', 0.3, 0.79, tz, 0.15, 0.08, 0xd8d0c0); K.cyl('plain', -0.5, 0.79, tz + 0.1, 0.035, 0.26, 0x2a6a3a); for (let i = 0; i < 3; i++) K.cyl('plain', -0.9 + i * 0.2, 0.79, tz - 0.2, 0.035, 0.1, 0xe8e4d8); };\n B.sauna = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('sauna'), t = 0.08, wood = K.wood(0xc08a54), shade1 = shade(wood, 0.88), dw = 0.7, dx = w / 2 - 0.55;\n K.box('wood', 0, 0, 0, w, 0.05, d, shade(wood, 0.75)); K.box('wood', 0, 0.05, -d / 2 + t / 2, w, h - 0.11, t, wood); for (const s of [-1, 1]) K.box('wood', s * (w / 2 - t / 2), 0.05, 0, t, h - 0.11, d - 2 * t * 0 , wood);\n K.box('wood', 0, h - 0.06, 0, w + 0.12, 0.06, d + 0.12, shade(wood, 0.75));\n const lx0 = -w / 2, lx1 = dx - dw / 2; K.box('wood', (lx0 + lx1) / 2, 0.05, d / 2 - t / 2, lx1 - lx0, h - 0.11, t, wood); K.box('wood', (dx + dw / 2 + w / 2) / 2, 0.05, d / 2 - t / 2, w / 2 - (dx + dw / 2), h - 0.11, t, wood); K.box('wood', dx, 1.85, d / 2 - t / 2, dw, h - 0.11 - 1.8, t, wood);\n for (let i = 0; i < 12; i++) { const x = lx0 + 0.1 + i * (lx1 - lx0) / 12; K.box('wood', x, 0.05, d / 2 + 0.003, 0.012, h - 0.11, 0.005, shade(wood, 0.6)); }\n K.box('wood', dx - dw / 2 - 0.025, 0.05, d / 2 - 0.02, 0.05, 1.82, 0.06, shade1); K.box('wood', dx + dw / 2 + 0.025, 0.05, d / 2 - 0.02, 0.05, 1.82, 0.06, shade1); K.box('glass', dx, 0.06, d / 2 - 0.02, dw, 1.75, 0.02, 0xffffff); K.box('metal', dx - dw / 2 + 0.08, 0.7, d / 2 + 0.03, 0.02, 0.5, 0.03, 0xc8ccd0);\n K.box('wood', 0, 0.85, -d / 2 + t + 0.25, w - 2 * t, 0.05, 0.5, shade(wood, 1.1)); K.box('wood', 0, 0.45, -d / 2 + t + 0.65, w - 2 * t, 0.05, 0.4, shade(wood, 1.1)); K.box('wood', 0, 0.05, -d / 2 + t + 0.1, w - 2 * t, 0.8, 0.04, shade(wood, 0.8));\n K.cyl('metal', -w / 2 + 0.3, 0.05, d / 2 - 0.4, 0.14, 0.55, 0x2a2a2e); for (let i = 0; i < 5; i++) K.cyl('plain', -w / 2 + 0.3 + Math.cos(i * 1.3) * 0.06, 0.6, d / 2 - 0.4 + Math.sin(i * 1.3) * 0.06, 0.04, 0.07, 0x6a6a6a, 0, 0, 'sphere');\n K.box('plain', 0, h - 0.2, -d / 2 + t + 0.02, 0.4, 0.06, 0.04, 0xfff0c8); K.box('emissive', 0, h - 0.19, -d / 2 + t + 0.045, 0.34, 0.01, 0.006, 0xffe0a0); };\n B.soaking_tub = (M, a) => { const K = kit(M, a), { w } = K.dim('soaking_tub'), r = Math.min(0.62, w * 0.48), wood = K.wood(0xa8784a);\n K.cyl('wood', 0, 0, 0, r, 0.78, wood, 0, 0, 'cyl'); K.cyl('wood', 0, 0.76, 0, r + 0.025, 0.04, shade(wood, 1.15)); for (const y of [0.15, 0.5]) K.cyl('metal', 0, y, 0, r + 0.012, 0.03, 0x2a2a2e);\n K.cyl('water', 0, 0.74, 0, r - 0.04, 0.01, 0x9ad4e8); for (let i = 0; i < 8; i++) K.box('wood', Math.cos(i * 0.785) * (r + 0.001), 0.02, Math.sin(i * 0.785) * (r + 0.001), 0.012, 0.74, 0.12, shade(wood, 0.7), -i * 0.785 + 0);\n K.box('wood', 0, 0, r + 0.4, 0.5, 0.3, 0.34, shade(wood, 0.9)); K.cyl('wood', 0.2, 0.3, r + 0.4, 0.1, 0.14, shade(wood, 1.1)); K.box('wood', -0.1, 0.34, r + 0.4, 0.3, 0.02, 0.025, shade(wood, 1.1), 0.5); K.box('fabric', -0.1, 0.3, r + 0.42, 0.26, 0.03, 0.2, K.accent(0xf0ece0)); };\n return B\n}\n","import { hex, mix, rng, seedOf } from './colors'\nimport type { AtlasMap, CoverOptions, FloorKind, PropItem, PropsApi, Uv } from './types'\n\n/** Default height of each floor kind above the ground. */\nconst HEIGHTS: Record<FloorKind, number> = { lawn: 0.02, dirt: 0.025, concrete: 0.03, tile_floor: 0.035, deck_floor: 0.04, pool_tile: 0.04, paving: 0.035 }\n\nconst inside = (poly: Array<[number, number]>, x: number, z: number): boolean => {\n let in_ = false\n for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {\n const [xi, zi] = poly[i]\n const [xj, zj] = poly[j]\n if (zi > z !== zj > z && x < ((xj - xi) * (z - zi)) / (zj - zi) + xi) in_ = !in_\n }\n return in_\n}\n\n/**\n * Covers a rectangle centered on M (w x d) or a polygon `[[x, z], ...]` in local coordinates with floor tiles:\n * one atlas square per cell (repeats and tiles); the polygon uses cells of a quarter of the size.\n */\nexport function createCover(atlas: <K extends 'floor'>(name: K) => AtlasMap[K]) {\n return function cover(api: PropsApi, M: PropItem, kind: FloorKind, o: CoverOptions = {}): void {\n const A = atlas('floor')\n const size = o.size || A.size(kind)\n const nv = A.variants(kind)\n const y = o.y != null ? o.y : HEIGHTS[kind] || 0.03\n const color = o.color != null ? hex(o.color) : 0xffffff\n const poly = o.poly || M.poly\n const sd = seedOf(M)\n const rnd = rng(sd)\n const tint = () => {\n const v = 0.93 + rnd() * 0.07\n return mix(0x000000, color, v)\n }\n const quad = (cx: number, cz: number, qw: number, qd: number, uv: Uv) => api.plane(M, 'atlas_floor', cx, y, cz, qw, qd, tint(), 0, -Math.PI / 2, 0, uv)\n if (!poly) {\n const w = o.w || M.w || 4\n const d = o.d || M.d || 4\n for (let j = 0, v = 0; v < d - 1e-6; j++, v += size)\n for (let i = 0, u = 0; u < w - 1e-6; i++, u += size) {\n const qw = Math.min(size, w - u)\n const qd = Math.min(size, d - v)\n const b = A.uv(kind, (i * 7 + j * 13 + sd) % nv)\n const uv = { u0: b.u0, u1: b.u0 + ((b.u1 - b.u0) * qw) / size, v1: b.v1, v0: b.v1 - ((b.v1 - b.v0) * qd) / size }\n quad(-w / 2 + u + qw / 2, -d / 2 + v + qd / 2, qw, qd, uv)\n }\n return\n }\n let x0 = 1e9\n let z0 = 1e9\n let x1 = -1e9\n let z1 = -1e9\n for (const [x, z] of poly) {\n x0 = Math.min(x0, x)\n x1 = Math.max(x1, x)\n z0 = Math.min(z0, z)\n z1 = Math.max(z1, z)\n }\n const st = size / 4\n for (let j = 0; z0 + j * st < z1; j++)\n for (let i = 0; x0 + i * st < x1; i++) {\n const cx = x0 + (i + 0.5) * st\n const cz = z0 + (j + 0.5) * st\n if (!inside(poly, cx, cz)) continue\n const b = A.uv(kind, (((i >> 2) * 7 + (j >> 2) * 13 + sd) % nv))\n const du = (b.u1 - b.u0) / 4\n const dv = (b.v1 - b.v0) / 4\n const ui = i & 3\n const vj = j & 3\n quad(cx, cz, st, st, { u0: b.u0 + ui * du, u1: b.u0 + (ui + 1) * du, v1: b.v1 - vj * dv, v0: b.v1 - (vj + 1) * dv })\n }\n }\n}\n","import { DEFS } from './catalog'\nimport { hex, rng, seedOf } from './colors'\nimport type { EmitFn, Kit, PropItem, PropsApi } from './types'\n\n/** Shortcuts that fix `M` and the api, so builders read as a list of primitives. */\nexport function makeKit(M: PropItem, api: PropsApi, emit: EmitFn): Kit {\n const role = (name: string): number | null => {\n const v = M.palette && M.palette[name]\n return v != null ? hex(v) : null\n }\n const K: Kit = {\n M,\n api,\n rnd: rng(seedOf(M)),\n box: (material, x, y, z, w, h, d, color, ry, rx, rz) => api.box(M, material, x, y, z, w, h, d, hex(color), ry || 0, rx || 0, rz || 0),\n cyl: (material, x, y, z, r, h, color, rx, rz, shape) => api.cylinder(M, material, x, y, z, r, h, hex(color), rx || 0, rz || 0, shape),\n leaf: (x, y, z, w, h, color, ry, rx) => api.leaf(M, x, y, z, w, h, hex(color), ry || 0, rx || 0),\n plane: (material, x, y, z, w, h, color, ry, rx, rz, uv) => api.plane(M, material, x, y, z, w, h, hex(color), ry || 0, rx || 0, rz || 0, uv),\n light: (x, y, z, color, range, kind) => {\n if (api.light) api.light(M, x, y, z, hex(color), range, kind)\n else emit('light', { M, x, y, z, color: hex(color), range, kind })\n },\n // colours: role in the house palette (M.palette) or the default; main = M.color\n color: (name, fallback) => role(name) ?? hex(fallback),\n main: (fallback) => (M.color != null ? hex(M.color) : hex(fallback)),\n fabric: (fallback) => (M.color != null ? hex(M.color) : (role('fabric_main') ?? hex(fallback))),\n accent: (fallback) => role('fabric_accent') ?? hex(fallback),\n wood: (fallback) => role('wood') ?? hex(fallback),\n metal: (fallback) => role('metal') ?? hex(fallback),\n dim: (id) => {\n const t = DEFS[id]\n return { w: M.w || t.w, d: M.d || t.d, h: M.h || t.h }\n },\n ceiling: () => M.ceiling || 2.9,\n // long tilted box: starts at (x, y, z) and follows u = (0, sin a, -cos a) for L meters\n ramp(material, x, y, z, w, t, L, color, alpha) {\n const cy = y + (Math.sin(alpha) * L) / 2\n const cz = z - (Math.cos(alpha) * L) / 2\n K.box(material, x, cy - t / 2, cz, w, t, L, color, 0, alpha + Math.PI, 0)\n },\n }\n return K\n}\n","import type { PropType } from './catalog'\n\n/** Locales shipped with the package. Unknown locales fall back to `en`. */\nexport type PropsLocale = 'en' | 'pt-BR'\n\nexport interface PropsStrings {\n /** Display name of each prop type. */\n typeNames: Record<PropType, string>\n /** Names of the picture atlas cells (4 abstract, 3 landscapes, the mirror). */\n pictureNames: string[]\n}\n\nexport const locales: Record<PropsLocale, PropsStrings> = {\n en: {\n typeNames: {\n ceiling_light: \"Ceiling light\",\n pendant: \"Pendant lamp\",\n track_spot: \"Track spot\",\n chandelier: \"Chandelier\",\n wall_ac: \"Indoor AC unit (split)\",\n picture_small: \"Small picture\",\n picture_large: \"Large picture\",\n mirror: \"Mirror\",\n curtain: \"Curtain\",\n blinds: \"Blinds\",\n flat_tv: \"Flat TV\",\n floor_plant: \"Floor plant\",\n table_plant: \"Table plant\",\n hanging_plant: \"Hanging plant\",\n tv_stand: \"TV stand with flat TV\",\n breakfast_bar: \"Breakfast bar\",\n sofa_l: \"L-shaped sofa\",\n double_bed_modern: \"Modern double bed\",\n home_office: \"Home office\",\n closet: \"Closet\",\n vanity: \"Bathroom vanity\",\n ac_condenser: \"AC condenser\",\n lawn: \"Lawn\",\n tile_floor: \"Ceramic tile floor\",\n concrete: \"Concrete slab\",\n deck_floor: \"Deck (flooring)\",\n dirt: \"Packed dirt\",\n gourmet_counter: \"Gourmet counter\",\n sauna: \"Sauna\",\n soaking_tub: \"Soaking tub\",\n pool: \"Pool\",\n deck: \"Raised deck\",\n sun_lounger: \"Sun lounger\",\n garden_bed: \"Vegetable garden bed\",\n fruit_tree: \"Fruit tree\",\n palm_tree: \"Palm tree\",\n pergola: \"Pergola\",\n slide: \"Slide\",\n swing: \"Swing\",\n bbq_simple: \"Simple barbecue\",\n garage_car: \"Car\",\n window_boards: \"Boards on the window\",\n barricade: \"Barricade\",\n sandbags: \"Sandbags\",\n barbed_wire: \"Barbed wire\",\n razor_wire: \"Razor wire\",\n water_bottle_crates: \"Mineral water crates\",\n canned_food: \"Canned food\",\n water_jug: \"Water jug\",\n floor_mattress: \"Mattress on the floor\",\n lantern: \"Lantern\",\n candles: \"Candles\",\n open_suitcases: \"Open suitcases\",\n clothes_on_floor: \"Clothes on the floor\",\n fallen_chair: \"Fallen chair\",\n blood_stains: \"Blood stains\",\n open_door: \"Open door\",\n },\n pictureNames: ['warm abstract', 'blue abstract', 'color blocks', 'minimal', 'sunset', 'mountains and lake', 'beach with palm tree', 'mirror'],\n },\n 'pt-BR': {\n typeNames: {\n ceiling_light: \"Plafon\",\n pendant: \"Pendente\",\n track_spot: \"Spot de trilho\",\n chandelier: \"Lustre\",\n wall_ac: \"Split interno\",\n picture_small: \"Quadro pequeno\",\n picture_large: \"Quadro grande\",\n mirror: \"Espelho\",\n curtain: \"Cortina\",\n blinds: \"Persiana\",\n flat_tv: \"TV plana\",\n floor_plant: \"Vaso de chão\",\n table_plant: \"Vaso de mesa\",\n hanging_plant: \"Planta pendente\",\n tv_stand: \"Rack com TV plana\",\n breakfast_bar: \"Bancada americana\",\n sofa_l: \"Sofá em L\",\n double_bed_modern: \"Cama de casal moderna\",\n home_office: \"Home office\",\n closet: \"Closet\",\n vanity: \"Lavabo\",\n ac_condenser: \"Condensadora\",\n lawn: \"Gramado\",\n tile_floor: \"Piso cerâmico\",\n concrete: \"Cimentado\",\n deck_floor: \"Deck (piso)\",\n dirt: \"Terra batida\",\n gourmet_counter: \"Bancada gourmet\",\n sauna: \"Sauna\",\n soaking_tub: \"Ofurô\",\n pool: \"Piscina\",\n deck: \"Deck elevado\",\n sun_lounger: \"Espreguiçadeira\",\n garden_bed: \"Canteiro de horta\",\n fruit_tree: \"Árvore frutífera\",\n palm_tree: \"Palmeira\",\n pergola: \"Pergolado\",\n slide: \"Escorregador\",\n swing: \"Balanço\",\n bbq_simple: \"Churrasqueira simples\",\n garage_car: \"Carro\",\n window_boards: \"Tábuas na janela\",\n barricade: \"Barricada\",\n sandbags: \"Sacos de areia\",\n barbed_wire: \"Arame farpado\",\n razor_wire: \"Concertina\",\n water_bottle_crates: \"Caixas de água mineral\",\n canned_food: \"Enlatados\",\n water_jug: \"Galão de água\",\n floor_mattress: \"Colchão no chão\",\n lantern: \"Lanterna\",\n candles: \"Velas\",\n open_suitcases: \"Malas abertas\",\n clothes_on_floor: \"Roupas no chão\",\n fallen_chair: \"Cadeira caída\",\n blood_stains: \"Manchas de sangue\",\n open_door: \"Porta aberta\",\n },\n pictureNames: ['abstrato quente', 'abstrato azul', 'blocos de cor', 'minimal', 'pôr do sol', 'montanhas e lago', 'praia com coqueiro', 'espelho'],\n },\n}\n\nexport const resolveStrings = (locale?: string): PropsStrings => locales[locale as PropsLocale] ?? locales.en\n","import { createAtlasProvider } from './atlas'\nimport { DEFS, MATERIALS, TYPE_DEFS, type PropType } from './catalog'\nimport { ceilingBuilders } from './builders/ceiling'\nimport { floorBuilders } from './builders/floors'\nimport { furnitureBuilders } from './builders/furniture'\nimport { houseStateBuilders } from './builders/house-state'\nimport { plantBuilders } from './builders/plants'\nimport { wallBuilders } from './builders/wall'\nimport { yardBuilders } from './builders/yard'\nimport { createCover } from './cover'\nimport { makeKit } from './kit'\nimport { resolveStrings } from './locale'\nimport type { BuilderContext, BuilderMap, DocumentLike, EmitFn, Props3d, Props3dOptions, TypeInfo } from './types'\n\n/** Type metadata with display names for a locale (`en` by default). */\nexport function listTypes(locale?: string): TypeInfo[] {\n const names = resolveStrings(locale).typeNames\n return TYPE_DEFS.map((t) => ({ ...t, name: names[t.id] }))\n}\n\n/**\n * Creates the props kit. Builders only describe primitives through the `api` you pass to them; nothing here touches a renderer.\n *\n * `three` is accepted so the call looks like the other game-kit packages, but the props never use it.\n *\n * ```ts\n * const props = create(THREE, { document })\n * props.builders.sofa_l({ w: 2.8, d: 1.7, h: 0.85, seed: 'living' }, api)\n * ```\n */\nexport function create(_three?: unknown, options: Props3dOptions = {}): Props3d {\n const emit: EmitFn = options.emit || (() => {})\n const doc: DocumentLike | null = options.document || (typeof document !== 'undefined' ? document : null)\n const strings = resolveStrings(options.locale)\n const atlas = createAtlasProvider(doc, strings.pictureNames)\n const cover = createCover(atlas)\n const ctx: BuilderContext = { kit: (M, api) => makeKit(M, api, emit), atlas, cover }\n const builders = Object.assign(\n {},\n ceilingBuilders(ctx),\n wallBuilders(ctx),\n plantBuilders(ctx),\n furnitureBuilders(ctx),\n floorBuilders(ctx),\n yardBuilders(ctx),\n houseStateBuilders(ctx),\n ) as BuilderMap\n return {\n builders,\n types: listTypes(options.locale),\n names: Object.keys(builders) as PropType[],\n atlas,\n cover,\n materials: MATERIALS,\n }\n}\n\nexport { DEFS, MATERIALS, TYPE_DEFS }\n","import type * as ThreeNamespace from 'three'\n\ntype Three = typeof ThreeNamespace\n\n/** What `createNodeSkin` makes: one mesh that stands for a whole tree of meshes, each vertex tied to the node of the tree that moves it. */\nexport type NodeSkin = {\n readonly mesh: ThreeNamespace.Mesh\n /** The nodes the animation moves, in the order of the skin's `aNodeIndex`. */\n readonly nodes: readonly ThreeNamespace.Object3D[]\n /** The matrix of each node, uploaded as the `uNodes` uniform. */\n readonly matrices: { readonly value: ThreeNamespace.Matrix4[] }\n /** Copies the world matrix of every node into `matrices`: call it once per frame, after the tree was animated. */\n update(): void\n}\n\nexport type NodeSkinOptions = {\n /** Meshes the animation moves and that therefore stay nodes of their own (a head ball, an eye). Groups and meshes with children are always nodes. */\n readonly nodes?: Iterable<ThreeNamespace.Object3D>\n}\n\nconst MAX_WALK_DEPTH = 3\n\n/**\n * Every Object3D reachable from the fields of `owner` (up to 3 levels, skipping the keys in `skip`): what the animation of a tree keeps in its own\n * state (head, jaw, wings, legs). Pass the result as `nodes` so those meshes are not merged into their parents.\n */\nexport const collectReferencedNodes = (owner: object, skip: readonly string[] = []): Set<ThreeNamespace.Object3D> => {\n const found = new Set<ThreeNamespace.Object3D>()\n const seen = new Set<unknown>()\n const walk = (value: unknown, depth: number): void => {\n if (!value || typeof value !== 'object' || depth > MAX_WALK_DEPTH || seen.has(value)) return\n seen.add(value)\n const candidate = value as { isObject3D?: boolean; isMaterial?: boolean; isBufferGeometry?: boolean }\n if (candidate.isObject3D) {\n found.add(value as ThreeNamespace.Object3D)\n return\n }\n if (candidate.isMaterial || candidate.isBufferGeometry) return\n for (const key in value) walk((value as Record<string, unknown>)[key], depth + 1)\n }\n for (const key in owner) if (!skip.includes(key)) walk((owner as Record<string, unknown>)[key], 0)\n return found\n}\n\ntype TextureSample = { readonly data: Uint8ClampedArray; readonly width: number; readonly height: number }\n\nconst isVisibleInTree = (object: ThreeNamespace.Object3D): boolean => {\n for (let node: ThreeNamespace.Object3D | null = object; node; node = node.parent) if (!node.visible) return false\n return true\n}\n\n/**\n * A GPU \"node skin\": a tree of about fifty meshes (an animal, a character prop) merged into one mesh, every vertex tied to the node that moves it.\n * The tree stays out of the scene and keeps animating; `update()` copies its node matrices each frame. Colour (with the texture sampled at the uv),\n * roughness, metalness and emissive are baked per vertex, so the 40 animals of a scene cost 40 draw calls instead of 2,000.\n * Generalised from the game's `bichoPele` (G 66536); the colours are read the way r160 does (linear converted to sRGB), as the game does.\n */\nexport const createNodeSkin = (THREE: Three, root: ThreeNamespace.Object3D, options: NodeSkinOptions = {}): NodeSkin => {\n const referenced = new Set(options.nodes ?? [])\n const nodes: ThreeNamespace.Object3D[] = []\n const indexOf = new Map<ThreeNamespace.Object3D, number>()\n root.traverse((object) => {\n const mesh = object as ThreeNamespace.Mesh\n if (!mesh.isMesh || object.children.length > 0 || referenced.has(object)) {\n indexOf.set(object, nodes.length)\n nodes.push(object)\n }\n })\n\n const positions: number[] = []\n const normals: number[] = []\n const tints: number[] = []\n const surfaces: number[] = []\n const nodeIndices: number[] = []\n const samples = new Map<ThreeNamespace.Texture, TextureSample>()\n const color = new THREE.Color()\n const vertex = new THREE.Vector3()\n const normal = new THREE.Vector3()\n\n const sampleOf = (texture: ThreeNamespace.Texture): TextureSample | undefined => {\n const image = texture.image as { getContext?: (kind: string) => CanvasRenderingContext2D | null; width: number; height: number } | undefined\n if (!image?.getContext) return undefined\n const cached = samples.get(texture)\n if (cached) return cached\n const context = image.getContext('2d')\n if (!context) return undefined\n const sample = { data: context.getImageData(0, 0, image.width, image.height).data, width: image.width, height: image.height }\n samples.set(texture, sample)\n return sample\n }\n\n root.traverse((object) => {\n const mesh = object as ThreeNamespace.Mesh\n if (!mesh.isMesh || !isVisibleInTree(object)) return\n const material = (Array.isArray(mesh.material) ? mesh.material[0] : mesh.material) as ThreeNamespace.MeshStandardMaterial\n const geometry = mesh.geometry\n const position = geometry.attributes.position as ThreeNamespace.BufferAttribute\n const normalAttribute = geometry.attributes.normal as ThreeNamespace.BufferAttribute | undefined\n const uv = geometry.attributes.uv as ThreeNamespace.BufferAttribute | undefined\n const matrix = new THREE.Matrix4()\n let nodeIndex = indexOf.get(object)\n if (nodeIndex === undefined) {\n nodeIndex = indexOf.get(object.parent as ThreeNamespace.Object3D) ?? 0\n object.updateMatrix()\n matrix.copy(object.matrix)\n }\n const normalMatrix = new THREE.Matrix3().getNormalMatrix(matrix)\n const sample = material.map ? sampleOf(material.map) : undefined\n const emissive = material.emissive ? (material.emissive.r + material.emissive.g + material.emissive.b) * (material.emissiveIntensity ?? 1) : 0\n const index = geometry.index ? geometry.index.array : null\n const count = index ? index.length : position.count\n for (let item = 0; item < count; item++) {\n const at = index ? (index[item] as number) : item\n vertex.fromBufferAttribute(position, at).applyMatrix4(matrix)\n positions.push(vertex.x, vertex.y, vertex.z)\n if (normalAttribute) normal.fromBufferAttribute(normalAttribute, at).applyMatrix3(normalMatrix).normalize()\n else normal.set(0, 1, 0)\n normals.push(normal.x, normal.y, normal.z)\n color.copy(material.color || color.set(1, 1, 1)).convertLinearToSRGB()\n if (sample && uv && material.map) {\n const repeat = material.map.repeat\n const u = (((uv.getX(at) * repeat.x) % 1) + 1) % 1\n const v = (((uv.getY(at) * repeat.y) % 1) + 1) % 1\n const pixelX = Math.min(sample.width - 1, (u * sample.width) | 0)\n const pixelY = Math.min(sample.height - 1, ((1 - v) * sample.height) | 0)\n const offset = (pixelY * sample.width + pixelX) * 4\n color.multiply(new THREE.Color().setRGB((sample.data[offset] as number) / 255, (sample.data[offset + 1] as number) / 255, (sample.data[offset + 2] as number) / 255, THREE.SRGBColorSpace))\n }\n if (emissive > 0.3) color.lerp(material.emissive, 0.7)\n tints.push(color.r, color.g, color.b)\n surfaces.push(material.roughness ?? 0.8, material.metalness ?? 0, emissive > 0.3 ? 1 : 0)\n nodeIndices.push(nodeIndex)\n }\n })\n\n const geometry = new THREE.BufferGeometry()\n geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))\n geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3))\n geometry.setAttribute('aTint', new THREE.Float32BufferAttribute(tints, 3))\n geometry.setAttribute('aSurface', new THREE.Float32BufferAttribute(surfaces, 3))\n geometry.setAttribute('aNodeIndex', new THREE.Float32BufferAttribute(nodeIndices, 1))\n geometry.computeBoundingSphere()\n\n const matrices = { value: nodes.map(() => new THREE.Matrix4()) }\n const skinVertex = (shader: ThreeNamespace.WebGLProgramParametersWithUniforms): void => {\n shader.uniforms.uNodes = matrices\n shader.vertexShader = shader.vertexShader\n .replace(\n '#include <common>',\n `#include <common>\\nuniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3 aTint; attribute vec3 aSurface; varying vec3 vTint; varying vec3 vSurface;`,\n )\n .replace('#include <beginnormal_vertex>', 'vec3 objectNormal = mat3(uNodes[int(aNodeIndex + .5)]) * normal;')\n .replace('#include <begin_vertex>', 'vec3 transformed = (uNodes[int(aNodeIndex + .5)] * vec4(position, 1.)).xyz; vTint = aTint; vSurface = aSurface;')\n }\n\n const material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0 })\n material.onBeforeCompile = (shader) => {\n skinVertex(shader)\n shader.fragmentShader = shader.fragmentShader\n .replace('#include <common>', '#include <common>\\nvarying vec3 vTint; varying vec3 vSurface;')\n .replace('vec4 diffuseColor = vec4( diffuse, opacity );', 'vec4 diffuseColor = vec4( vTint, opacity );')\n .replace('#include <roughnessmap_fragment>', 'float roughnessFactor = vSurface.x;')\n .replace('#include <metalnessmap_fragment>', 'float metalnessFactor = vSurface.y;')\n .replace('#include <emissivemap_fragment>', 'totalEmissiveRadiance = vTint * vSurface.z * 1.5;')\n }\n material.customProgramCacheKey = () => `nodeSkin${nodes.length}`\n const depthMaterial = new THREE.MeshDepthMaterial({ depthPacking: THREE.RGBADepthPacking })\n depthMaterial.onBeforeCompile = skinVertex\n depthMaterial.customProgramCacheKey = () => `nodeSkinDepth${nodes.length}`\n\n const mesh = new THREE.Mesh(geometry, material)\n mesh.customDepthMaterial = depthMaterial\n mesh.frustumCulled = false\n root.updateMatrixWorld(true)\n return {\n mesh,\n nodes,\n matrices,\n update() {\n root.updateMatrixWorld(true)\n for (let index = 0; index < nodes.length; index++) (matrices.value[index] as ThreeNamespace.Matrix4).copy((nodes[index] as ThreeNamespace.Object3D).matrixWorld)\n },\n }\n}\n","import type { Primitive, PropsApi } from './types'\n\n/**\n * An api that records every primitive instead of drawing it. Handy for tests, previews and for measuring a prop.\n *\n * ```ts\n * const { api, primitives } = createRecorder()\n * props.builders.sofa_l(item, api)\n * console.log(primitives.length)\n * ```\n * Pass `{ light: false }` to leave `api.light` undefined, so builders fall back to `emit('light', ...)`.\n */\nexport function createRecorder(options: { light?: boolean } = {}): { api: PropsApi; primitives: Primitive[] } {\n const primitives: Primitive[] = []\n const api: PropsApi = {\n box: (M, material, x, y, z, w, h, d, color, ry, rx, rz) => void primitives.push({ kind: 'box', M, material, x, y, z, w, h, d, color, ry, rx, rz }),\n cylinder: (M, material, x, y, z, r, h, color, rx, rz, shape) => void primitives.push({ kind: 'cylinder', M, material, x, y, z, r, h, color, rx, rz, shape }),\n leaf: (M, x, y, z, w, h, color, ry, rx) => void primitives.push({ kind: 'leaf', M, x, y, z, w, h, color, ry, rx }),\n plane: (M, material, x, y, z, w, h, color, ry, rx, rz, uv) => void primitives.push({ kind: 'plane', M, material, x, y, z, w, h, color, ry, rx, rz, uv }),\n }\n if (options.light !== false) {\n api.light = (M, x, y, z, color, range, lightKind) => void primitives.push({ kind: 'light', M, x, y, z, color, range, lightKind })\n }\n return { api, primitives }\n}\n","/**\n * Every visible mesh under `root`, baked into `space` and joined per material. It lives in `@adatechnology/game-shapes-three` now (on the\n * game-shapes merge, the same bytes as the version this package had); this re-export keeps the old import working.\n */\nexport { mergeByMaterial } from '@adatechnology/game-shapes-three'\n","import type { GroupNode, ShapeNode, ShapeSchema, Vec3 } from '@adatechnology/game-shapes'\n\nimport type { CylinderShape, PropItem, PropsApi, Uv } from './types'\n\nexport interface ShapeRecorderOptions {\n /** Radial segments of `cyl` and `cone` (default 12). */\n readonly cylinderSegments?: number\n /** Radial segments of `cyl6` (default 6). */\n readonly hexSegments?: number\n /** Width and height segments of `sphere` (default 12 x 8). */\n readonly sphereSegments?: readonly [number, number]\n /** Name of the schema. */\n readonly name?: string\n}\n\n/** A part as the api received it: the first call's pivot and turn (later calls with the same id draw into it). */\nexport interface RecordedPart {\n readonly id: string\n readonly pivot: Vec3\n readonly ry: number\n}\n\n/** The props api plus parts (the `HouseholdApi` contract): a part draws in its own frame, at `pivot`, turned by `ry`. */\nexport interface ShapeRecorderApi extends PropsApi {\n light(M: PropItem, x: number, y: number, z: number, color: number, range: number, kind: string): void\n part(id: string, pivot: Vec3, ry: number, draw: () => void): void\n}\n\nexport interface ShapeRecorder {\n readonly api: ShapeRecorderApi\n /** The game-shapes schema of everything drawn so far. */\n schema(): ShapeSchema\n /** The parts in the order of their first call. */\n readonly parts: readonly RecordedPart[]\n}\n\n/** The double-sided diamond of `leaf`: base at the origin, tip at +y, in the xy plane (front face counter-clockwise seen from +z). */\nconst LEAF = (() => {\n const f = [0, 0, 0, -0.5, 0.4, 0, 0.5, 0.4, 0, 0, 1, 0]\n const positions: number[] = []\n const normals: number[] = []\n const uvs: number[] = []\n const indices: number[] = []\n for (const side of [1, -1]) {\n const base = positions.length / 3\n for (let i = 0; i < 4; i++) {\n positions.push(f[i * 3] as number, f[i * 3 + 1] as number, f[i * 3 + 2] as number)\n normals.push(0, 0, side)\n uvs.push((f[i * 3] as number) + 0.5, f[i * 3 + 1] as number)\n }\n if (side === 1) indices.push(base, base + 2, base + 1, base + 1, base + 2, base + 3)\n else indices.push(base, base + 1, base + 2, base + 1, base + 3, base + 2)\n }\n return { positions, normals, uvs, indices }\n})()\n\n/**\n * An api that turns what a builder draws into a `game-shapes` schema, with the arithmetic of the interactables mesh api: each primitive\n * under its item frame (`heading` = `M.ry`, at `M.x, 0, M.z`), placed by `compose(position, euler YXZ, scale)` over a unit template (a box\n * with its base at `y`, a cylinder centred as the api says, a sphere of radius `h / 2`, the double-sided leaf, a unit plane with the atlas\n * cell as its uv), parts as part nodes, lights as points (`data: { light: true, color, range, kind }`).\n *\n * ```ts\n * const recorder = createShapeRecorder()\n * props.builders.sofa_l(item, recorder.api)\n * const buffers = generate(recorder.schema(), { validate: false, key: false })\n * ```\n */\nexport function createShapeRecorder(options: ShapeRecorderOptions = {}): ShapeRecorder {\n const cyl = options.cylinderSegments ?? 12\n const hex = options.hexSegments ?? 6\n const [sphereW, sphereH] = options.sphereSegments ?? [12, 8]\n const root: ShapeNode[] = []\n const parts: RecordedPart[] = []\n const partChildren = new Map<string, ShapeNode[]>()\n let current = root\n // consecutive primitives of the same item frame share one frame group (one matrix, the same products)\n let lastFrame: { list: ShapeNode[]; group: GroupNode & { children: ShapeNode[] }; x: number; z: number; ry: number } | null = null\n\n const into = (M: PropItem | undefined): ShapeNode[] => {\n const x = M?.x ?? 0\n const z = M?.z ?? 0\n const ry = M?.ry ?? 0\n const last = lastFrame\n if (last && last.list === current && current[current.length - 1] === last.group && Object.is(last.x, x) && Object.is(last.z, z) && Object.is(last.ry, ry)) return last.group.children\n const group = { kind: 'group' as const, position: [x, 0, z] as Vec3, heading: ry, children: [] as ShapeNode[] }\n current.push(group)\n lastFrame = { list: current, group, x, z, ry }\n return group.children\n }\n\n const api: ShapeRecorderApi = {\n box(M, material, x, y, z, w, h, d, color, ry, rx, rz) {\n into(M).push({ kind: 'box', material, color, position: [x, y + h / 2, z], rotation: [rx, ry, rz], rotationOrder: 'YXZ', scale: [w, h, d] })\n },\n cylinder(M, material, x, y, z, r, h, color, rx, rz, shape?: CylinderShape) {\n const tilted = rx !== 0 || rz !== 0\n const cy = shape === 'sphere' ? y + h / 2 : tilted ? y : y + h / 2\n const sy = shape === 'sphere' ? h / 2 : h\n const place = { material, color, position: [x, cy, z] as Vec3, rotation: [rx, 0, rz] as Vec3, rotationOrder: 'YXZ' as const, scale: [r, sy, r] as Vec3 }\n const list = into(M)\n switch (shape) {\n case 'cyl6':\n list.push({ kind: 'cylinder', radiusTop: 1, radiusBottom: 1, height: 1, radialSegments: hex, ...place })\n break\n case 'cone':\n list.push({ kind: 'cylinder', radiusTop: 0, radiusBottom: 1, height: 1, radialSegments: cyl, ...place })\n break\n case 'sphere':\n list.push({ kind: 'sphere', radius: 1, widthSegments: sphereW, heightSegments: sphereH, ...place })\n break\n default:\n list.push({ kind: 'cylinder', radiusTop: 1, radiusBottom: 1, height: 1, radialSegments: cyl, ...place })\n }\n },\n leaf(M, x, y, z, w, h, color, ry, rx) {\n into(M).push({ kind: 'mesh', ...LEAF, material: 'plant', color, position: [x, y, z], rotation: [rx, ry, 0], rotationOrder: 'YXZ', scale: [w, h, 1] })\n },\n plane(M, material, x, y, z, w, h, color, ry, rx, rz, uv?: Uv) {\n into(M).push({\n kind: 'plane',\n width: 1,\n height: 1,\n material,\n color,\n position: [x, y, z],\n rotation: [rx, ry, rz],\n rotationOrder: 'YXZ',\n scale: [w, h, 1],\n ...(uv ? { uv: { u0: uv.u0, v0: uv.v0, u1: uv.u1, v1: uv.v1 } } : {}),\n })\n },\n light(M, x, y, z, color, range, kind) {\n into(M).push({ kind: 'point', position: [x, y, z], data: { light: true, color, range, kind } })\n },\n part(id, pivot, ry, draw) {\n let children = partChildren.get(id)\n if (!children) {\n children = []\n partChildren.set(id, children)\n parts.push({ id, pivot: [pivot[0], pivot[1], pivot[2]], ry })\n current.push({ kind: 'group', part: id, position: [pivot[0], pivot[1], pivot[2]], rotation: [0, ry, 0], children })\n }\n const outer = current\n current = children\n try {\n draw()\n } finally {\n current = outer\n }\n },\n }\n\n return {\n api,\n parts,\n schema: () => ({ version: 1, ...(options.name !== undefined ? { name: options.name } : {}), root: { kind: 'group', children: root } }),\n }\n}\n","import type { BirdSpec } from '@adatechnology/game-animals'\n\nimport type { AnimateParams } from './animate-types'\nimport type { AnimalRig } from './types'\n\nconst { PI } = Math\n\n/** Advances a bird's cycle (page: `animBird`): steps, flight for the species that fly, wings, peck, alarm, tail. */\nexport const animateBird = (rig: AnimalRig, params: AnimateParams): void => {\n const { time: t, delta: dt, clip } = params\n const species = rig.species\n const spec = species.body as BirdSpec\n const isVulture = species.id === 'vulture'\n const isChicken = species.id === 'chicken'\n const isLapwing = species.id === 'southern_lapwing'\n const flying = clip === 'run' && !isChicken\n const hz = clip === 'run' && !flying ? species.runHz : clip === 'walk' ? species.walkHz : 0\n rig.phase += dt * (flying ? (isVulture ? 1.1 : 4.5) : hz) * 2 * PI\n const targetHeight = flying ? rig.size * 1.2 : 0\n rig.trunk.position.y += (targetHeight - rig.trunk.position.y) * Math.min(1, dt * 3)\n rig.legs.forEach((leg, index) => {\n if (flying) {\n leg.hip.rotation.x = 1.2\n leg.knee.rotation.x = -1.6\n } else if (clip === 'idle') {\n leg.hip.rotation.x = 0\n leg.knee.rotation.x = 0\n } else {\n const phase = rig.phase + index * PI\n leg.hip.rotation.x = -Math.sin(phase) * 0.45\n leg.knee.rotation.x = -Math.max(0, Math.cos(phase)) * 0.7\n }\n leg.foot.rotation.x = -(leg.hip.rotation.x + leg.knee.rotation.x)\n })\n rig.wings.forEach((wing) => {\n const { side, shoulder, elbow } = wing\n if (flying) {\n const flap = isVulture ? 0.25 + Math.sin(rig.phase) * 0.18 : Math.sin(rig.phase) * 0.9\n shoulder.rotation.y += (0 - shoulder.rotation.y) * Math.min(1, dt * 6)\n shoulder.rotation.z = side * flap\n elbow.rotation.z = side * (isVulture ? 0.08 : Math.sin(rig.phase - 0.6) * 0.4)\n elbow.rotation.y = 0\n shoulder.scale.x += (1 - shoulder.scale.x) * Math.min(1, dt * 6)\n } else {\n const alert = isLapwing && clip === 'idle' ? Math.max(0, Math.sin(t * 1.3)) * 0.9 : 0\n const flutter = isChicken && clip === 'run' ? Math.abs(Math.sin(rig.phase * 1.5)) * 0.8 : 0\n shoulder.rotation.y += ((side * PI) / 2 * 0.97 - alert * side * 0.9 - shoulder.rotation.y) * Math.min(1, dt * 6)\n shoulder.rotation.z = side * (-0.06 + flutter + alert * 0.6)\n elbow.rotation.z = side * 0.04\n elbow.rotation.y = 0\n shoulder.scale.x += ((alert > 0.1 ? 1 : 0.58) - shoulder.scale.x) * Math.min(1, dt * 6)\n }\n })\n const peck = isChicken && clip === 'idle' ? Math.max(0, Math.sin(t * 1.6)) ** 8 * 1.2 : 0\n if (rig.neck) {\n rig.neck.rotation.x = rig.neckAngle + (clip === 'walk' && !isVulture ? Math.max(0, Math.sin(rig.phase * 2)) * 0.35 : 0) + peck + (flying ? 0.6 : 0)\n rig.neck.rotation.y = clip === 'idle' ? Math.sin(t * 0.9) * 0.5 : 0\n }\n if (rig.jaw) rig.jaw.rotation.x = isLapwing && clip === 'idle' ? Math.max(0, Math.sin(t * 1.3)) * Math.abs(Math.sin(t * 14)) * 0.5 : 0\n if (rig.tailRoot) rig.tailRoot.rotation.x = (spec.tailA || 0) + (flying ? 0.6 : 0)\n}\n","import type { GaitPhases, QuadrupedSpec } from '@adatechnology/game-animals'\n\nimport type { AnimateParams } from './animate-types'\nimport type { AnimalRig } from './types'\n\nconst { PI } = Math\n\n/** Advances a quadruped's step cycle (page: `animQuad`): legs by gait phase, trunk bob, neck, head, jaw and tail; a dead one lies down. */\nexport const animateQuadruped = (rig: AnimalRig, params: AnimateParams, gaits: { readonly walk: GaitPhases; readonly trot: GaitPhases; readonly gallop: GaitPhases }): void => {\n const { time: t, delta: dt, clip } = params\n const species = rig.species\n const spec = species.body as QuadrupedSpec\n if (rig.dead) {\n rig.root.rotation.z += (PI / 2 * 0.97 - rig.root.rotation.z) * Math.min(1, dt * 3)\n rig.root.position.y += (spec.R * (spec.sx || 1) * 0.85 - rig.root.position.y) * Math.min(1, dt * 3)\n rig.legs.forEach((leg) => {\n leg.hip.rotation.x = leg.angle1 + (leg.isFront ? -0.5 : 0.4)\n leg.knee.rotation.x = 0.05\n leg.foot.rotation.x = 0\n })\n if (rig.neck) rig.neck.rotation.x = rig.neckAngle + 0.35\n if (rig.jaw) rig.jaw.rotation.x = 0.25\n return\n }\n const hz = (clip === 'run' ? species.runHz : clip === 'walk' ? species.walkHz : 0) * (rig.infected ? 1.2 : 1)\n rig.phase += dt * hz * 2 * PI\n const gait = clip === 'run' ? (species.gallop ? gaits.gallop : gaits.trot) : gaits.walk\n const amplitude = clip === 'run' ? 0.62 : 0.34\n const flex = clip === 'run' ? 1.15 : 0.75\n rig.legs.forEach((leg, index) => {\n if (clip === 'idle') {\n leg.hip.rotation.x = leg.angle1\n leg.knee.rotation.x = leg.angle2\n } else {\n const phase = rig.phase + (gait[index] as number)\n const limit = rig.infected && index === 1 ? 0.35 : 1\n leg.hip.rotation.x = leg.angle1 - Math.sin(phase) * amplitude * limit\n leg.knee.rotation.x = leg.angle2 + Math.max(0, Math.cos(phase)) * flex * limit\n }\n leg.foot.rotation.x = -(leg.hip.rotation.x + leg.knee.rotation.x) * 0.85\n })\n const bob = clip === 'idle' ? 0 : Math.abs(Math.sin(rig.phase)) * (clip === 'run' ? 0.05 : 0.018) * rig.size\n rig.trunk.position.y = bob * 0.6\n const bodyGroup = rig.bodyGroup\n if (bodyGroup) {\n bodyGroup.rotation.x = clip === 'run' && species.gallop ? Math.sin(rig.phase + PI * 0.4) * 0.07 : 0\n bodyGroup.scale.y = 1 + (clip === 'idle' ? Math.sin(t * 2.2) * 0.012 : 0)\n }\n const low = rig.infected ? 0.35 : 0\n if (rig.neck) {\n rig.neck.rotation.x = rig.neckAngle + low + (clip === 'idle' ? Math.sin(t * 0.7) * 0.08 : Math.sin(rig.phase * 2) * 0.04)\n rig.neck.rotation.y = clip === 'idle' ? Math.sin(t * 0.45) * 0.35 : 0\n }\n if (rig.head) rig.head.rotation.z = rig.infected ? Math.sin(t * 11) * Math.sin(t * 1.7) * 0.12 : 0\n if (rig.jaw) rig.jaw.rotation.x = rig.infected ? 0.25 + Math.max(0, Math.sin(t * 6)) * 0.25 : clip === 'run' ? 0.12 : 0\n const wagging = species.id === 'dog' && !rig.infected\n rig.tail.forEach((segment, index) => {\n segment.rotation.z = Math.sin(t * (wagging ? 9 : 2.2) - index * 0.6) * (wagging ? 0.35 : 0.12)\n })\n}\n","import type * as THREE_NS from 'three'\n\nimport type { AnimateParams } from './animate-types'\nimport type { AnimalRig, Three } from './types'\n\nconst { PI } = Math\n\n/** The path of the body for a clip: crawling in a wave, or coiled with the head up in an S (and striking when `strike` rises). */\nconst bodyPath = (THREE: Three, rig: AnimalRig, params: AnimateParams, spacing: number): THREE_NS.Vector3[] => {\n const { time: t, clip } = params\n const points: THREE_NS.Vector3[] = []\n for (let index = 0; index < rig.segmentCount; index++) {\n let x: number\n let z: number\n let y = 0\n if (clip === 'walk') {\n z = 0.6 - index * spacing\n x = Math.sin(rig.phase * 2.2 - index * 0.42) * 0.08 * Math.min(1, index / 6 + 0.3)\n } else {\n const strike = clip === 'run' ? Math.max(0, Math.sin(t * 2.2)) ** 6 : 0\n if (index < 9) {\n const k = index / 9\n z = 0.08 + (1 - k) * 0.12 + strike * (1 - k) * 0.35\n y = (1 - k) * 0.13 * (1 - strike * 0.6)\n x = Math.sin(k * PI * 1.6) * 0.05 * (1 - strike)\n } else {\n const angle = (index - 9) * 0.36\n const radius = 0.16 - (index - 9) * 0.0045\n x = Math.cos(angle) * radius\n z = Math.sin(angle) * radius - 0.05\n y = (index - 9) * 0.0012\n }\n }\n points.push(new THREE.Vector3(x, y, z))\n }\n return points\n}\n\n/** Advances a snake (page: `animSnake`): lays the 34 segments along the path of the clip, then the head, jaw and tongue. */\nexport const animateSnake = (THREE: Three, rig: AnimalRig, params: AnimateParams): void => {\n const { time: t, delta: dt, clip } = params\n const spacing = rig.segmentLength * (params.split ? 1.5 : 1)\n if (clip === 'walk') rig.phase += dt * 2.4\n const points = bodyPath(THREE, rig, params, spacing)\n const at = (index: number): THREE_NS.Vector3 => points[index] as THREE_NS.Vector3\n for (let index = 0; index < rig.segmentCount; index++) {\n const group = (rig.segments[index] as AnimalRig['segments'][number]).group\n group.position.copy(at(index))\n const direction = index === 0 ? at(0).clone().sub(at(1)) : at(index - 1).clone().sub(at(index))\n group.rotation.set(0, Math.atan2(direction.x, direction.z), 0)\n group.rotateX(-Math.atan2(direction.y, Math.hypot(direction.x, direction.z)))\n }\n const head = rig.head as THREE_NS.Group\n const headDirection = at(0).clone().sub(at(1)).normalize()\n head.position.copy(at(0)).addScaledVector(headDirection, 0.02)\n head.rotation.set(0, Math.atan2(headDirection.x, headDirection.z), 0)\n head.rotateX(-Math.atan2(headDirection.y, Math.hypot(headDirection.x, headDirection.z)) * 0.5)\n const jaw = rig.jaw as THREE_NS.Group\n jaw.rotation.x = clip === 'run' ? Math.max(0, Math.sin(t * 2.2)) ** 6 * 0.9 : 0\n const tongue = rig.tongue as THREE_NS.Mesh\n tongue.visible = Math.sin(t * 5) > 0.3\n tongue.scale.z = 1 + Math.sin(t * 20) * 0.3\n}\n","import { birdSize } from '@adatechnology/game-animals'\nimport type { BirdSpec } from '@adatechnology/game-animals'\nimport type * as THREE_NS from 'three'\n\nimport { addMesh, createRigTools } from './context'\nimport type { BirdExtra } from './extras'\nimport type { AnimalRig, Three } from './types'\n\nconst buildWings = (THREE: Three, rig: AnimalRig, spec: BirdSpec, parts: { bodyGroup: THREE_NS.Group; main: THREE_NS.MeshStandardMaterial; dark: THREE_NS.MeshStandardMaterial }): void => {\n const tools = createRigTools(THREE, rig)\n const { bodyGroup, main, dark } = parts\n for (const side of [-1, 1]) {\n const shoulder = tools.pivot(bodyGroup, side * spec.r * spec.s[0] * 1.02, spec.r * 0.22, spec.r * 0.3, new THREE.Vector3(side * 0.12, 0.03, 0).multiplyScalar(rig.size))\n tools.joint(shoulder, spec.r * 0.14, main, 0, 0, 0)\n tools.ball(shoulder, 0.5, spec.wingM ? tools.material(spec.wingM) : dark, side * spec.wi * 0.5, 0, -spec.wc * 0.3, spec.wi * 1.05, spec.r * 0.16, spec.wc)\n const elbow = tools.pivot(shoulder, side * spec.wi, 0, 0, new THREE.Vector3(side * 0.06, 0, 0).multiplyScalar(rig.size))\n tools.joint(elbow, spec.r * 0.1, main, 0, 0, 0)\n tools.ball(elbow, 0.5, dark, side * spec.wo * 0.45, 0, -spec.wc * 0.32, spec.wo * 1.05, spec.r * 0.12, spec.wc * 0.75)\n if (spec.fingers) {\n for (let finger = 0; finger < 4; finger++) {\n addMesh(THREE, elbow, new THREE.BoxGeometry(spec.wo * 0.35, spec.r * 0.03, spec.wc * 0.12), dark, side * (spec.wo + spec.wo * 0.12), 0, -spec.wc * 0.05 - finger * spec.wc * 0.18, 0, side * (0.05 + finger * 0.12), 0)\n }\n }\n if (spec.spur) addMesh(THREE, elbow, new THREE.ConeGeometry(spec.r * 0.06, spec.r * 0.25, 5), tools.material(0xc0302a), 0, 0, spec.wc * 0.2, Math.PI / 2, 0, 0)\n rig.wings.push({ shoulder, elbow, side })\n }\n}\n\nconst buildLegs = (THREE: Three, rig: AnimalRig, spec: BirdSpec, parts: { bodyGroup: THREE_NS.Group; main: THREE_NS.MeshStandardMaterial; leg: THREE_NS.MeshStandardMaterial }): void => {\n const tools = createRigTools(THREE, rig)\n const { bodyGroup, main, leg } = parts\n for (const side of [-1, 1]) {\n const hip = tools.pivot(bodyGroup, side * spec.r * 0.38, -spec.r * 0.55, spec.r * 0.05, new THREE.Vector3(side * 0.06, -0.05, 0).multiplyScalar(rig.size))\n tools.joint(hip, spec.r * 0.18, main, 0, 0, 0)\n tools.limb(hip, spec.r * 0.16, spec.r * 0.09, spec.shank, spec.featherLeg ? main : leg)\n const ankle = tools.pivot(hip, 0, -spec.shank, 0, new THREE.Vector3(0, -0.04, 0).multiplyScalar(rig.size))\n tools.joint(ankle, spec.r * 0.06, leg, 0, 0, 0)\n tools.limb(ankle, spec.r * 0.055, spec.r * 0.05, spec.tars, leg)\n const foot = tools.pivot(ankle, 0, -spec.tars, 0)\n for (const angle of [-0.45, 0, 0.45]) addMesh(THREE, foot, new THREE.BoxGeometry(spec.r * 0.05, spec.r * 0.04, spec.r * 0.55).translate(0, 0, spec.r * 0.25), leg, 0, -spec.r * 0.02, 0, 0, angle, 0)\n addMesh(THREE, foot, new THREE.BoxGeometry(spec.r * 0.05, spec.r * 0.04, spec.r * 0.25).translate(0, 0, -spec.r * 0.12), leg, 0, -spec.r * 0.02, 0)\n rig.legs.push({ hip, knee: ankle, foot, isFront: false, angle1: 0.35, angle2: -0.5, side })\n }\n}\n\n/** Builds a bird: body, tail, two wings with shoulder and elbow pivots, neck, head with beak and jaw, eyes, the species' extra parts, and two legs. */\nexport const buildBird = (THREE: Three, rig: AnimalRig, options: { extra?: BirdExtra }): AnimalRig => {\n const spec = rig.species.body as BirdSpec\n const tools = createRigTools(THREE, rig)\n const colors = spec.c\n const main = tools.material(colors.main)\n const dark = tools.material(colors.dark)\n const leg = tools.material(colors.leg)\n const beak = tools.material(colors.beak, { roughness: 0.5 })\n const belly = tools.material(colors.belly || colors.main)\n rig.main = main\n const hipY = spec.tars + spec.shank * 0.9\n rig.size = birdSize(spec)\n const bodyGroup = tools.pivot(rig.trunk, 0, hipY + spec.r * 0.55, 0)\n rig.bodyGroup = bodyGroup\n bodyGroup.rotation.x = spec.pitch || 0\n tools.ball(bodyGroup, spec.r, main, 0, 0, 0, spec.s[0], spec.s[1], spec.s[2])\n tools.ball(bodyGroup, spec.r * 0.9, belly, 0, -spec.r * 0.22, spec.r * 0.15, spec.s[0] * 0.9, spec.s[1] * 0.8, spec.s[2] * 0.85)\n const tail = tools.pivot(bodyGroup, 0, spec.r * 0.15, -spec.r * spec.s[2] * 0.85, new THREE.Vector3(0, 0, -0.08).multiplyScalar(rig.size))\n tail.rotation.x = spec.tailA || -0.3\n rig.tailRoot = tail\n addMesh(THREE, tail, new THREE.BoxGeometry(spec.tailW, spec.r * 0.08, spec.tailL).translate(0, 0, -spec.tailL / 2), spec.tailM ? tools.material(spec.tailM) : dark)\n buildWings(THREE, rig, spec, { bodyGroup, main, dark })\n const neck = tools.pivot(bodyGroup, 0, spec.r * 0.45, spec.r * spec.s[2] * 0.75, new THREE.Vector3(0, 0.05, 0.08).multiplyScalar(rig.size))\n neck.rotation.x = spec.neckA\n rig.neck = neck\n rig.neckAngle = spec.neckA\n tools.joint(neck, spec.nr * 1.1, spec.neckM ? tools.material(spec.neckM) : main, 0, 0, 0)\n addMesh(THREE, neck, new THREE.CylinderGeometry(spec.nr * 0.85, spec.nr, spec.nl, 12).translate(0, spec.nl / 2, 0), spec.neckM ? tools.material(spec.neckM) : main)\n const head = tools.pivot(neck, 0, spec.nl, 0, new THREE.Vector3(0, 0.05, 0.05).multiplyScalar(rig.size))\n head.rotation.x = -spec.neckA + (spec.headA || 0)\n rig.head = head\n rig.headAngle = head.rotation.x\n const headMaterial = spec.headM ? tools.material(spec.headM) : main\n tools.ball(head, spec.hr, headMaterial, 0, 0, 0, spec.hs ? spec.hs[0] : 1, spec.hs ? spec.hs[1] : 1, spec.hs ? spec.hs[2] : 1.1)\n addMesh(THREE, head, new THREE.ConeGeometry(spec.br, spec.bl, 8).rotateX(Math.PI / 2).translate(0, 0, spec.bl / 2), beak, 0, -spec.hr * 0.1, spec.hr * 0.85, spec.hook || 0, 0, 0)\n const jaw = tools.pivot(head, 0, -spec.hr * 0.25, spec.hr * 0.8)\n rig.jaw = jaw\n addMesh(THREE, jaw, new THREE.ConeGeometry(spec.br * 0.7, spec.bl * 0.85, 8).rotateX(Math.PI / 2).translate(0, 0, spec.bl * 0.42), beak).scale.set(1, 0.5, 1)\n tools.eyes(head, spec.hr * 0.17, spec.hr * 0.68, spec.hr * 0.2, spec.hr * 0.35)\n if (spec.eyeC && rig.eyeMaterial) {\n rig.eyeMaterial.color.set(spec.eyeC)\n rig.eyeMaterial.userData.base = rig.eyeMaterial.color.clone()\n }\n options.extra?.({ THREE, rig, tools }, { bodyGroup, head, neck, main, dark })\n buildLegs(THREE, rig, spec, { bodyGroup, main, leg })\n return rig\n}\n","import { quadrupedLayout } from '@adatechnology/game-animals'\nimport type { QuadrupedSpec } from '@adatechnology/game-animals'\nimport type * as THREE_NS from 'three'\n\nimport { addMesh, createRigTools } from './context'\nimport { createCoatTexture } from './textures'\nimport { profileGeometry } from './profile'\nimport type { AnimalRig, DocumentLike, Three } from './types'\nimport type { QuadrupedExtra } from './extras'\n\nconst RIB_COUNT = 4\n\ntype LegDefinition = readonly [number, number, 'F' | 'H']\n\nconst LEG_DEFINITIONS: readonly LegDefinition[] = [\n [1, 1, 'F'],\n [-1, 1, 'F'],\n [1, -1, 'H'],\n [-1, -1, 'H'],\n]\n\n/** The body and the four legs of a quadruped (page: the first half of `quad`). */\nconst buildBodyAndLegs = (THREE: Three, rig: AnimalRig, spec: QuadrupedSpec, main: THREE_NS.MeshStandardMaterial, belly: THREE_NS.MeshStandardMaterial, dark: THREE_NS.MeshStandardMaterial) => {\n const tools = createRigTools(THREE, rig)\n const { L, R: radius } = spec\n const leg = spec.leg\n const layout = quadrupedLayout(spec)\n const { frontHipHeight, hindHipHeight, footHeight, bodyHeight, span } = layout\n const frontAngle1 = leg.fa1 ?? 0.06\n const frontAngle2 = leg.fa2 ?? -0.08\n const hindAngle1 = leg.ha1 ?? -0.32\n const hindAngle2 = leg.ha2 ?? 0.58\n rig.size = layout.size\n const bodyGroup = tools.pivot(rig.trunk, 0, bodyHeight, 0)\n rig.bodyGroup = bodyGroup\n const body = addMesh(THREE, bodyGroup, profileGeometry(THREE, L, spec.prof.map((value) => value * radius)), main)\n body.scale.set(spec.sx || 1, spec.sy || 1, 1)\n body.rotation.x = Math.atan2(hindHipHeight - frontHipHeight, span) * 0.6\n rig.body = body\n const belt = addMesh(THREE, bodyGroup, profileGeometry(THREE, L * 0.8, spec.prof.map((value) => value * 0.8 * radius)), belly)\n belt.scale.set((spec.sx || 1) * 0.92, (spec.sy || 1) * 0.85, 1)\n belt.position.y = -radius * 0.2\n LEG_DEFINITIONS.forEach(([sx, sz, kind]) => {\n const front = kind === 'F'\n const upper = front ? leg.fu : leg.hu\n const lower = front ? leg.fl : leg.hl\n const height = front ? frontHipHeight : hindHipHeight\n const hip = tools.pivot(rig.trunk, sx * radius * (spec.sx || 1) * 0.55, height, (sz * span) / 2, new THREE.Vector3(sx * 0.18, -0.06, sz * 0.12).multiplyScalar(rig.size))\n tools.ball(hip, leg.r * (front ? 1.9 : 2.3), main, 0, -upper * 0.18, front ? 0 : -leg.r * 0.3, 0.75, 1.25, 1)\n tools.joint(hip, leg.r * 1.15, main, 0, 0, 0)\n tools.limb(hip, leg.r * 1.25, leg.r * 0.95, upper, main)\n const knee = tools.pivot(hip, 0, -upper, 0, new THREE.Vector3(0, -0.07 * rig.size, 0))\n tools.joint(knee, leg.r * 0.95, main, 0, 0, 0)\n tools.limb(knee, leg.r * 0.92, leg.r * 0.68, lower, main)\n const foot = tools.pivot(knee, 0, -lower, 0, new THREE.Vector3(0, -0.05 * rig.size, 0))\n if (leg.hoof) {\n tools.joint(foot, leg.r * 0.72, dark, 0, 0, 0)\n addMesh(THREE, foot, new THREE.CylinderGeometry(leg.r * 0.8, leg.r * 1.05, footHeight, 12).translate(0, -footHeight / 2, 0), dark)\n } else {\n tools.joint(foot, leg.r * 0.66, main, 0, 0, 0)\n tools.ball(foot, leg.r * 0.95, leg.dark ? dark : main, 0, -footHeight * 0.55, leg.r * 0.5, 1, 0.62, 1.45)\n }\n rig.legs.push({ hip, knee, foot, isFront: front, angle1: front ? frontAngle1 : hindAngle1, angle2: front ? frontAngle2 : hindAngle2, side: sx })\n })\n return { bodyGroup, bodyHeight }\n}\n\nconst buildHead = (THREE: Three, rig: AnimalRig, spec: QuadrupedSpec, parts: { bodyGroup: THREE_NS.Group; main: THREE_NS.MeshStandardMaterial; belly: THREE_NS.MeshStandardMaterial; nose: THREE_NS.MeshStandardMaterial }) => {\n const tools = createRigTools(THREE, rig)\n const { bodyGroup, main, belly, nose } = parts\n const { L, R: radius } = spec\n const neckSpec = spec.neck\n const headSpec = spec.head\n const neck = tools.pivot(bodyGroup, 0, radius * (spec.sy || 1) * (neckSpec.y ?? 0.35), (L / 2) * (neckSpec.z ?? 0.78), new THREE.Vector3(0, 0.05, 0.12).multiplyScalar(rig.size))\n neck.rotation.x = neckSpec.ang\n rig.neck = neck\n rig.neckAngle = neckSpec.ang\n tools.joint(neck, neckSpec.r * 1.05, main, 0, 0, 0)\n addMesh(THREE, neck, new THREE.CylinderGeometry(neckSpec.r2 || neckSpec.r * 0.85, neckSpec.r, neckSpec.len, 14).translate(0, neckSpec.len / 2, 0), main)\n const head = tools.pivot(neck, 0, neckSpec.len, 0, new THREE.Vector3(0, 0.06, 0.08).multiplyScalar(rig.size))\n head.rotation.x = -neckSpec.ang + (headSpec.down || 0)\n rig.head = head\n rig.headAngle = head.rotation.x\n tools.joint(head, neckSpec.r2 || neckSpec.r * 0.85, main, 0, 0, 0)\n tools.ball(head, headSpec.r, main, 0, headSpec.r * 0.25, headSpec.r * 0.25, headSpec.s[0], headSpec.s[1], headSpec.s[2])\n const snoutZ = headSpec.r * 0.25 + headSpec.r * headSpec.s[2] * 0.55\n const snout = addMesh(THREE, head, new THREE.CylinderGeometry(headSpec.sr2, headSpec.sr1, headSpec.sl, 14).rotateX(Math.PI / 2), headSpec.muzzle ? tools.material(headSpec.muzzle) : main, 0, headSpec.r * (headSpec.sy ?? 0), snoutZ + headSpec.sl / 2)\n snout.scale.set(1, headSpec.sq || 1, 1)\n tools.ball(head, headSpec.sr2 * 1.02, nose, 0, headSpec.r * (headSpec.sy ?? 0) + headSpec.sr2 * 0.1, snoutZ + headSpec.sl, 1, 0.85 * (headSpec.sq || 1), 0.45)\n const jaw = tools.pivot(head, 0, headSpec.r * (headSpec.sy ?? 0) - headSpec.sr1 * 0.55 * (headSpec.sq || 1), snoutZ - headSpec.sr1 * 0.2, new THREE.Vector3(0, -0.04, 0.04).multiplyScalar(rig.size))\n rig.jaw = jaw\n addMesh(THREE, jaw, new THREE.CylinderGeometry(headSpec.sr2 * 0.7, headSpec.sr1 * 0.75, headSpec.sl * 0.92, 12).rotateX(Math.PI / 2).translate(0, 0, headSpec.sl * 0.46), belly).scale.set(1, 0.45, 1)\n const blood = tools.ball(jaw, headSpec.sr2 * 0.9, rig.woundMaterial || (rig.woundMaterial = new THREE.MeshStandardMaterial({ color: 0x6a100c, roughness: 0.3 })), 0, 0.0, headSpec.sl * 0.9, 1.1, 0.6, 0.6)\n tools.eyes(head, headSpec.r * 0.13, headSpec.r * headSpec.s[0] * 0.52, headSpec.r * 0.5, headSpec.r * 0.25 + headSpec.r * headSpec.s[2] * 0.55)\n const ears = spec.ears\n for (const side of [-1, 1]) {\n const earGroup = tools.pivot(head, side * headSpec.r * headSpec.s[0] * 0.55, headSpec.r * (0.25 + headSpec.s[1] * 0.75), headSpec.r * 0.1, new THREE.Vector3(side * 0.04, 0.04, 0).multiplyScalar(rig.size))\n rig.ears.push(earGroup)\n if (ears.t === 'cone') addMesh(THREE, earGroup, new THREE.ConeGeometry(ears.s * 0.45, ears.s, 8).translate(0, ears.s / 2, 0), main, 0, 0, 0, ears.fwd || 0, 0, side * -(ears.tilt ?? 0.3))\n else if (ears.t === 'round') tools.ball(earGroup, ears.s, main, 0, ears.s * 0.4, 0, 1, 1, 0.35)\n else if (ears.t === 'flop') {\n const ear = tools.ball(earGroup, ears.s, main, side * ears.s * 0.9, -ears.s * 0.1, 0, 1.5, 0.45, 0.7)\n ear.rotation.z = side * -0.25\n }\n }\n return { neck, head, blood, neckSpec, headSpec }\n}\n\nconst buildTail = (THREE: Three, rig: AnimalRig, spec: QuadrupedSpec, parts: { bodyGroup: THREE_NS.Group; main: THREE_NS.MeshStandardMaterial; dark: THREE_NS.MeshStandardMaterial }): void => {\n const tools = createRigTools(THREE, rig)\n const tailSpec = spec.tail\n if (!tailSpec || !tailSpec.n) return\n const { bodyGroup, main, dark } = parts\n const { L, R: radius } = spec\n let parent = tools.pivot(bodyGroup, 0, radius * (spec.sy || 1) * (tailSpec.y ?? 0.3), (-L / 2) * 0.95, new THREE.Vector3(0, 0.02, -0.12).multiplyScalar(rig.size))\n parent.rotation.x = tailSpec.ang\n rig.tailRoot = parent\n const segmentLength = tailSpec.len / tailSpec.n\n for (let index = 0; index < tailSpec.n; index++) {\n const radius1 = tailSpec.r * (1 - (index / tailSpec.n) * 0.6)\n const radius2 = tailSpec.r * (1 - ((index + 1) / tailSpec.n) * 0.6)\n tools.limb(parent, radius1, radius2, segmentLength, tailSpec.dark && index === tailSpec.n - 1 ? dark : main)\n tools.joint(parent, radius1, main, 0, 0, 0)\n rig.tail.push(parent)\n if (index < tailSpec.n - 1) {\n const next = tools.pivot(parent, 0, -segmentLength, 0)\n next.rotation.x = tailSpec.curl || 0\n parent = next\n } else if (tailSpec.tuft) tools.ball(parent, tailSpec.r * 2.2, dark, 0, -segmentLength - tailSpec.r, 0, 1, 1.8, 1)\n }\n if (tailSpec.bushy) tools.ball(rig.tail[0] as THREE_NS.Group, tailSpec.r * 2.2, dark, 0, -tailSpec.len * 0.55, 0, 1, (tailSpec.len * 2.2) / tailSpec.r / 6, 1)\n}\n\n/** Wounds, ribs and the blood of the jaw: shown only while the animal is infected (page: the end of `quad`). */\nconst buildInfectionParts = (THREE: Three, rig: AnimalRig, spec: QuadrupedSpec, parts: { bodyGroup: THREE_NS.Group; neck: THREE_NS.Group; blood: THREE_NS.Mesh; neckSpec: QuadrupedSpec['neck'] }): void => {\n const tools = createRigTools(THREE, rig)\n const { bodyGroup, neck, blood, neckSpec } = parts\n const { L, R: radius } = spec\n const sideX = radius * (spec.sx || 1)\n const sideY = radius * (spec.sy || 1)\n const list: THREE_NS.Object3D[] = [blood]\n const wound = (parent: THREE_NS.Object3D, size: number, x: number, y: number, z: number, sx: number, sy: number, sz: number): THREE_NS.Mesh =>\n tools.ball(parent, size, rig.woundMaterial || (rig.woundMaterial = new THREE.MeshStandardMaterial({ color: 0x5a120e, roughness: 0.45 })), x, y, z, sx || 1, sy || 1, sz || 1)\n list.push(wound(bodyGroup, radius * 0.28, sideX * 0.92, sideY * 0.15, L * 0.05, 0.35, 1, 1.3))\n list.push(wound(bodyGroup, radius * 0.2, -sideX * 0.9, sideY * 0.3, -L * 0.2, 0.35, 1, 1.1))\n list.push(wound(neck, neckSpec.r * 0.5, neckSpec.r * 0.85, neckSpec.len * 0.5, 0, 0.4, 1, 1))\n const bone = tools.material(0xd9cfb8, { roughness: 0.6 })\n for (let index = 0; index < RIB_COUNT; index++) {\n const rib = addMesh(THREE, bodyGroup, new THREE.TorusGeometry(radius * 0.95, radius * 0.035, 6, 16, Math.PI * 0.42), bone, sideX * 0.12, sideY * 0.05, L * 0.12 - index * L * 0.07, 0, Math.PI / 2, -0.3)\n rib.scale.set(1, 1.02 * (spec.sy || 1), spec.sx || 1)\n list.push(rib)\n }\n rig.infectionParts.push(...list)\n list.forEach((part) => (part.visible = false))\n}\n\n/** Builds a quadruped: body, four legs on hip-knee-foot pivots, neck, head with jaw and eyes, ears, tail, the species' extra parts, and the infection parts. */\nexport const buildQuadruped = (THREE: Three, rig: AnimalRig, options: { document: DocumentLike; extra?: QuadrupedExtra }): AnimalRig => {\n const spec = rig.species.body as QuadrupedSpec\n const tools = createRigTools(THREE, rig)\n const colors = spec.c\n const texture = spec.texture ? createCoatTexture(THREE, options.document, spec.texture) : undefined\n const main = tools.material(texture ? 0xffffff : colors.main, texture ? { map: texture } : undefined)\n const belly = tools.material(colors.belly)\n const dark = tools.material(colors.dark)\n const nose = tools.material(colors.nose || colors.dark, { roughness: 0.4 })\n rig.main = main\n rig.dark = dark\n const { bodyGroup, bodyHeight } = buildBodyAndLegs(THREE, rig, spec, main, belly, dark)\n const { neck, head, blood, neckSpec, headSpec } = buildHead(THREE, rig, spec, { bodyGroup, main, belly, nose })\n buildTail(THREE, rig, spec, { bodyGroup, main, dark })\n options.extra?.({ THREE, rig, tools }, { bodyGroup, head, neck, main, belly, dark, L: spec.L, Rb: spec.R, bodyY: bodyHeight, hd: headSpec, nk: neckSpec })\n if (rig.species.outcome === 'turns') buildInfectionParts(THREE, rig, spec, { bodyGroup, neck, blood, neckSpec })\n else blood.visible = false\n return rig\n}\n","import { addMesh, createRigTools } from './context'\nimport type { AnimalRig, Three } from './types'\n\nconst SEGMENT_COUNT = 34\nconst SEGMENT_LENGTH = 0.042\n\n/** Builds a snake: 34 spheres on their own groups along the trunk (the animation lays them on a path), a head with a jaw and a tongue. */\nexport const buildSnake = (THREE: Three, rig: AnimalRig): AnimalRig => {\n const tools = createRigTools(THREE, rig)\n rig.size = 0.5\n rig.segmentLength = SEGMENT_LENGTH\n rig.segmentCount = SEGMENT_COUNT\n const primary = tools.material(0x7a6a4a)\n const secondary = tools.material(0x3a2e22)\n const belly = tools.material(0xb8a888)\n rig.main = primary\n for (let index = 0; index < SEGMENT_COUNT; index++) {\n const t = index / (SEGMENT_COUNT - 1)\n const radius = 0.012 + 0.03 * Math.sin(Math.min(1, t * 1.15) * Math.PI) * (t < 0.1 ? 0.75 + t * 2.5 : 1) + (t < 0.05 ? 0.012 : 0)\n const material = index % 4 === 1 || index % 4 === 2 ? secondary : primary\n const group = new THREE.Group()\n rig.trunk.add(group)\n const sphere = tools.ball(group, radius, material, 0, radius, 0, 1, 0.78, 1.5)\n sphere.userData.joint = index % 3 === 0 ? 1 : 0\n if (index % 3 === 0) rig.joints.push(sphere)\n rig.segments.push({ group, radius })\n }\n const head = new THREE.Group()\n rig.trunk.add(head)\n rig.head = head\n tools.ball(head, 0.032, primary, 0, 0.03, 0.025, 1.25, 0.6, 1.45)\n tools.ball(head, 0.024, secondary, 0, 0.04, 0.0, 1.3, 0.4, 0.9)\n tools.ball(head, 0.02, primary, 0, 0.028, 0.06, 0.85, 0.55, 1.1)\n const jaw = new THREE.Group()\n jaw.position.set(0, 0.018, 0.0)\n head.add(jaw)\n rig.jaw = jaw\n tools.ball(jaw, 0.026, belly, 0, 0, 0.035, 1.1, 0.35, 1.5)\n tools.eyes(head, 0.006, 0.03, 0.04, 0.045)\n rig.tongue = addMesh(THREE, head, new THREE.BoxGeometry(0.004, 0.002, 0.05), tools.material(0xb02020), 0, 0.024, 0.1)\n return rig\n}\n","import { GAIT_PHASES } from '@adatechnology/game-animals'\nimport { markKitOrigin } from '@adatechnology/game-contracts'\nimport type { AnimalSpecies, AnimalVisualState, GaitPhases } from '@adatechnology/game-animals'\nimport { collectReferencedNodes, createNodeSkin } from '@adatechnology/game-props3d'\nimport { shapeGeometries } from '@adatechnology/game-shapes-three'\nimport type { NodeSkin } from '@adatechnology/game-props3d'\nimport type * as THREE_NS from 'three'\nimport { animateBird } from './animate-bird'\nimport { animateQuadruped } from './animate-quadruped'\nimport { animateSnake } from './animate-snake'\nimport type { AnimateParams } from './animate-types'\nimport { buildBird } from './bird'\nimport { createRig, createStream } from './context'\nimport { BIRD_EXTRAS, QUADRUPED_EXTRAS } from './extras'\nimport { buildQuadruped } from './quadruped'\nimport { buildSnake } from './snake'\nimport type { AnimalRig, AnimalsThreeOptions, DocumentLike, Three } from './types'\nimport { applyVisualState } from './visual-state'\nimport { setJointView, setSplit, updateSplit } from './views'\n\nexport type AnimalsThree = {\n /** Builds one animal from its species; consumes one number of the stream for the start phase unless `phase` is given. */\n build(species: AnimalSpecies, buildOptions?: { readonly phase?: number }): AnimalRig\n /** Advances the animation of one animal (steps, flight, wings, head, tail, a dead one lying down). */\n animate(rig: AnimalRig, params: AnimateParams): void\n setVisualState(rig: AnimalRig, state: AnimalVisualState): void\n setJointView(rig: AnimalRig, on: boolean): void\n setSplit(rig: AnimalRig, on: boolean): void\n updateSplit(rig: AnimalRig, delta: number): void\n /** One mesh that stands for the whole tree, for crowds: call `skin.update()` each frame after `animate`. */\n createSkin(rig: AnimalRig): NodeSkin\n readonly jointMaterial: THREE_NS.MeshStandardMaterial\n}\n\nconst countTriangles = (root: THREE_NS.Object3D): number => {\n let triangles = 0\n root.traverse((object) => {\n const mesh = object as THREE_NS.Mesh\n if (!mesh.isMesh) return\n const geometry = mesh.geometry\n triangles += geometry.index ? geometry.index.count / 3 : geometry.attributes.position.count / 3\n })\n return Math.round(triangles)\n}\n\n/**\n * The animals drawn with the host's three.js. The geometry (boxes, cylinders, cones, spheres, circles, tori, the lathe of the bodies) comes from\n * game-shapes through `shapeGeometries(host)`: the same bytes and classes as three's, from a template cache; everything else is the host's.\n */\nexport const createAnimalsThree = (host: Three, options: AnimalsThreeOptions = {}): AnimalsThree => {\n const THREE = shapeGeometries(host)\n const random = options.random ?? createStream(options.seed ?? 7)\n const documentOf = (): DocumentLike => {\n const found = options.document ?? (globalThis as { document?: DocumentLike }).document\n if (!found) throw new Error('game-animals-three: a species with a coat texture needs `options.document` (no global document found)')\n return found\n }\n const jointMaterial = new THREE.MeshStandardMaterial({ color: 0xe0a548, emissive: 0xe0a548, emissiveIntensity: 0.8, roughness: 0.4 })\n const gaits: Readonly<Record<'walk' | 'trot' | 'gallop', GaitPhases>> = GAIT_PHASES\n\n return {\n jointMaterial,\n build(species, buildOptions = {}) {\n const rig = createRig(THREE, species, { phase: buildOptions.phase ?? random() * 6 })\n if (species.body.kind === 'snake') buildSnake(THREE, rig)\n else if (species.body.kind === 'bird') buildBird(THREE, rig, { extra: BIRD_EXTRAS[species.id] })\n else buildQuadruped(THREE, rig, { document: species.body.texture ? documentOf() : (undefined as never), extra: QUADRUPED_EXTRAS[species.id] })\n rig.triangles = countTriangles(rig.root)\n markKitOrigin(rig.root, { owner: 'game-animals-three', kind: 'animal', id: species.id })\n rig.root.traverse((object) => {\n if ((object as THREE_NS.Mesh).isMesh) object.userData.speciesId = species.id\n })\n return rig\n },\n animate(rig, params) {\n const kind = rig.species.body.kind\n if (kind === 'snake') animateSnake(THREE, rig, params)\n else if (kind === 'bird') animateBird(rig, params)\n else animateQuadruped(rig, params, gaits)\n },\n setVisualState: (rig, state) => applyVisualState(THREE, rig, state),\n setJointView: (rig, on) => setJointView(rig, on, jointMaterial),\n setSplit,\n updateSplit,\n createSkin: (rig) => createNodeSkin(THREE, rig.root, { nodes: collectReferencedNodes(rig, ['species']) }),\n }\n}\n","import type { Group, Object3D, PointLight, SpotLight } from 'three'\n\nimport type { LightKind, LightPatch, LinearRgb, ThreeForLights } from './types'\n\nconst PARKED_Y = -50\nconst DEFAULT_DECAY = 2\nconst DEFAULT_ANGLE = Math.PI / 3\nconst POSITION_EPSILON = 1e-6\n\nexport type Vec3 = [number, number, number]\n\nexport type LightValues = {\n color: number | LinearRgb\n intensity: number\n distance: number\n decay: number\n position: Vec3\n target: Vec3 | undefined\n angle: number\n penumbra: number\n}\n\n/** Whoever owns a slot: a request, a source or a lease. */\nexport interface Holder {\n readonly kind: 'request' | 'source' | 'lease'\n readonly owner: string\n readonly lightKind: LightKind\n priority: number\n urgent: boolean\n values: LightValues\n slot: Slot | null\n alive: boolean\n onLost: (() => void) | undefined\n /** Sources only. */\n follow: Object3D | undefined\n shadow: boolean\n wanted: boolean\n distance: number\n /** Sources only: `values` with the world position and direction of `follow` of this frame. */\n resolved: LightValues\n}\n\nexport interface Slot {\n readonly index: number\n readonly kind: LightKind\n readonly shadow: boolean\n readonly light: PointLight | SpotLight\n holder: Holder | null\n pending: Holder | null\n leased: boolean\n /** 0..1 multiplier on the holder's intensity. */\n fade: number\n fadeTarget: number\n fadeOutMs: number\n baseIntensity: number\n lastPosition: Vec3\n}\n\nexport function createValues(base: Partial<LightValues> & Pick<LightValues, 'color' | 'intensity' | 'distance' | 'position'>): LightValues {\n return {\n decay: DEFAULT_DECAY,\n target: undefined,\n angle: DEFAULT_ANGLE,\n penumbra: 0,\n ...base,\n position: [...base.position],\n }\n}\n\nexport function mergeValues(values: LightValues, patch: LightPatch): void {\n if (patch.color !== undefined) values.color = patch.color\n if (patch.intensity !== undefined) values.intensity = patch.intensity\n if (patch.distance !== undefined) values.distance = patch.distance\n if (patch.decay !== undefined) values.decay = patch.decay\n if (patch.position !== undefined) values.position = [...patch.position]\n if (patch.target !== undefined) values.target = [...patch.target]\n if (patch.angle !== undefined) values.angle = patch.angle\n if (patch.penumbra !== undefined) values.penumbra = patch.penumbra\n}\n\nexport type ShadowSettings = { readonly bias?: number; readonly normalBias?: number; readonly near?: number }\n\nexport function createSlots(THREE: ThreeForLights, group: Group, kind: LightKind, count: number, shadows: number, mapSize: number, settings: ShadowSettings = {}): Slot[] {\n const slots: Slot[] = []\n for (let index = 0; index < count; index++) {\n const light = kind === 'point' ? new THREE.PointLight(0xffffff, 0, 0, DEFAULT_DECAY) : new THREE.SpotLight(0xffffff, 0, 0, DEFAULT_ANGLE, 0, DEFAULT_DECAY)\n light.position.set(0, PARKED_Y, 0)\n light.layers.enableAll()\n const shadow = index < shadows\n if (shadow) {\n light.castShadow = true\n light.shadow.autoUpdate = false\n light.shadow.mapSize.set(mapSize, mapSize)\n if (settings.bias !== undefined) light.shadow.bias = settings.bias\n if (settings.normalBias !== undefined) light.shadow.normalBias = settings.normalBias\n if (settings.near !== undefined) light.shadow.camera.near = settings.near\n }\n group.add(light)\n if ('target' in light) group.add(light.target)\n slots.push({ index, kind, shadow, light, holder: null, pending: null, leased: false, fade: 0, fadeTarget: 0, fadeOutMs: 0, baseIntensity: 0, lastPosition: [0, PARKED_Y, 0] })\n }\n return slots\n}\n\nexport function isFree(slot: Slot): boolean {\n return !slot.leased && slot.holder === null && slot.pending === null\n}\n\nexport function applyIntensity(slot: Slot): void {\n slot.light.intensity = slot.baseIntensity * slot.fade\n}\n\nexport function markShadow(slot: Slot): void {\n if (slot.shadow) slot.light.shadow.needsUpdate = true\n}\n\nexport function writeLight(THREE: ThreeForLights, slot: Slot, values: LightValues): void {\n const { light } = slot\n if (typeof values.color === 'number') light.color.set(values.color)\n else light.color.setRGB(values.color[0], values.color[1], values.color[2], THREE.LinearSRGBColorSpace)\n light.distance = values.distance\n light.decay = values.decay\n const [x, y, z] = values.position\n light.position.set(x, y, z)\n if ('angle' in light) {\n light.angle = values.angle\n light.penumbra = values.penumbra\n const [tx, ty, tz] = values.target ?? [x, y - 1, z]\n light.target.position.set(tx, ty, tz)\n }\n const last = slot.lastPosition\n if (slot.shadow && (Math.abs(last[0] - x) > POSITION_EPSILON || Math.abs(last[1] - y) > POSITION_EPSILON || Math.abs(last[2] - z) > POSITION_EPSILON)) markShadow(slot)\n slot.lastPosition = [x, y, z]\n slot.baseIntensity = values.intensity\n applyIntensity(slot)\n}\n\n/** The holder's writes apply from now on; its light fades in (requests that cut a victim start at full). */\nexport function install(THREE: ThreeForLights, slot: Slot, holder: Holder, startAtFull: boolean): void {\n slot.holder = holder\n slot.pending = null\n slot.fade = startAtFull ? 1 : 0\n slot.fadeTarget = 1\n holder.slot = slot\n markShadow(slot)\n writeLight(THREE, slot, holder.values)\n}\n\n/** Drops the holder: the light keeps its last values and fades to 0 (at once with `fadeOutMs` 0). */\nexport function vacate(slot: Slot, fadeOutMs: number): void {\n slot.holder = null\n slot.fadeTarget = 0\n slot.fadeOutMs = fadeOutMs\n if (fadeOutMs <= 0) {\n slot.fade = 0\n applyIntensity(slot)\n }\n}\n\nexport function stepFade(slot: Slot, dtSeconds: number, fadeInMs: number): void {\n if (slot.fade < slot.fadeTarget) {\n slot.fade = fadeInMs <= 0 ? slot.fadeTarget : Math.min(slot.fadeTarget, slot.fade + (dtSeconds * 1000) / fadeInMs)\n } else if (slot.fade > slot.fadeTarget) {\n slot.fade = slot.fadeOutMs <= 0 ? slot.fadeTarget : Math.max(slot.fadeTarget, slot.fade - (dtSeconds * 1000) / slot.fadeOutMs)\n }\n applyIntensity(slot)\n}\n","import { type Holder, type Slot, install, isFree, stepFade, vacate } from './slots'\nimport type { LightKind, ThreeForLights } from './types'\n\nexport type ArbiterCore = {\n readonly THREE: ThreeForLights\n readonly slots: Record<LightKind, readonly Slot[]>\n readonly urgentReserve: number\n readonly fadeMs: number\n readonly hysteresis: number\n preempted: number\n refused: number\n}\n\n/** The slots requests and sources compete for: not leased, and shadow slots only on their own pass. */\nexport function poolOf(core: ArbiterCore, kind: LightKind, shadow: boolean): Slot[] {\n return core.slots[kind].filter((slot) => !slot.leased && slot.shadow === shadow)\n}\n\nfunction contribution(holder: Holder): number {\n return holder.values.intensity / Math.max(1, holder.distance) ** 2\n}\n\n/** Lowest priority first; on a tie, the smallest perceived contribution. */\nfunction isWeaker(a: Holder, b: Holder): boolean {\n if (a.priority !== b.priority) return a.priority < b.priority\n return contribution(a) < contribution(b)\n}\n\nfunction pickFree(slots: readonly Slot[]): Slot | undefined {\n let best: Slot | undefined\n for (const slot of slots) if (isFree(slot) && (best === undefined || slot.fade < best.fade)) best = slot\n return best\n}\n\n/** Gives a free slot to the holder: at once when its light is dark or the holder is urgent, else after the slot finishes fading out. */\nfunction place(core: ArbiterCore, slot: Slot, holder: Holder): void {\n holder.slot = slot\n if (slot.fade > 0 && !holder.urgent) {\n slot.pending = holder\n slot.fadeTarget = 0\n return\n }\n install(core.THREE, slot, holder, holder.kind === 'request')\n}\n\n/** Takes the slot from its holder; a request is told and dies, a source goes back to the queue. */\nfunction evict(core: ArbiterCore, slot: Slot, challenger: Holder, cut: boolean): void {\n const victim = slot.holder\n if (victim === null) return\n victim.slot = null\n if (victim.kind === 'request') {\n victim.alive = false\n victim.onLost?.()\n }\n core.preempted++\n vacate(slot, cut ? 0 : core.fadeMs)\n if (cut) {\n install(core.THREE, slot, challenger, true)\n return\n }\n slot.pending = challenger\n challenger.slot = slot\n}\n\nfunction weakestHolder(slots: readonly Slot[]): Slot | undefined {\n let weakest: Slot | undefined\n for (const slot of slots) {\n if (slot.holder === null || slot.pending !== null) continue\n if (weakest === undefined || isWeaker(slot.holder, weakest.holder as Holder)) weakest = slot\n }\n return weakest\n}\n\n/** Slots free to a holder that has no right to the urgent reserve. */\nfunction freeBeyondReserve(core: ArbiterCore, pool: readonly Slot[], reserve: number): Slot | undefined {\n const free = pool.filter(isFree)\n return free.length > reserve ? pickFree(pool) : undefined\n}\n\nexport function admitRequest(core: ArbiterCore, holder: Holder): boolean {\n const pool = poolOf(core, holder.lightKind, false)\n const reserve = holder.lightKind === 'point' ? core.urgentReserve : 0\n const free = holder.urgent ? pickFree(pool) : freeBeyondReserve(core, pool, reserve)\n if (free !== undefined) {\n place(core, free, holder)\n return true\n }\n const victim = weakestHolder(pool)\n if (victim === undefined || (victim.holder as Holder).priority >= holder.priority) {\n core.refused++\n return false\n }\n evict(core, victim, holder, holder.urgent)\n return true\n}\n\nfunction beats(challenger: Holder, incumbent: Holder, hysteresis: number): boolean {\n if (challenger.priority !== incumbent.priority) return challenger.priority > incumbent.priority\n return challenger.distance < incumbent.distance * (1 - hysteresis)\n}\n\nfunction bestFirst(a: Holder, b: Holder): number {\n return b.priority - a.priority || a.distance - b.distance\n}\n\n/** Hands the pool's slots to the wanted sources: best first, an incumbent only yields to a clearly better challenger. */\nexport function arbitrateSources(core: ArbiterCore, pool: readonly Slot[], sources: readonly Holder[], reserve: number): void {\n for (const slot of pool) {\n const holder = slot.holder\n if (slot.pending?.kind === 'source' && !slot.pending.wanted) {\n slot.pending.slot = null\n slot.pending = null\n }\n if (holder?.kind === 'source' && !holder.wanted) {\n holder.slot = null\n vacate(slot, core.fadeMs)\n }\n }\n const challengers = sources.filter((source) => source.wanted && source.slot === null).sort(bestFirst)\n for (const challenger of challengers) {\n const free = freeBeyondReserve(core, pool, reserve)\n if (free !== undefined) {\n place(core, free, challenger)\n continue\n }\n const incumbentSlots = pool.filter((slot) => slot.holder?.kind === 'source')\n let worst: Slot | undefined\n for (const slot of incumbentSlots) {\n if (slot.pending !== null) continue\n if (worst === undefined || bestFirst(slot.holder as Holder, worst.holder as Holder) > 0) worst = slot\n }\n if (worst === undefined || !beats(challenger, worst.holder as Holder, core.hysteresis)) break\n evict(core, worst, challenger, false)\n }\n}\n\nexport function stepSlots(core: ArbiterCore, dtSeconds: number): void {\n for (const kind of ['point', 'spot'] as const) {\n for (const slot of core.slots[kind]) {\n stepFade(slot, dtSeconds, core.fadeMs)\n if (slot.pending !== null && slot.fade <= 0) install(core.THREE, slot, slot.pending, false)\n }\n }\n}\n","import type { Fog, FogBase, FogExp2, Scene } from 'three'\n\nimport type { ThreeLib } from './three-lib'\n\n/** Both exact in half precision (multiples of 32 under 65 504), so the neutral fog survives a `mediump` fragment shader. */\nexport const NEUTRAL_FOG_NEAR = 6.0e4\nexport const NEUTRAL_FOG_FAR = 6.4e4\n\nexport interface FogOptions {\n /** `clear()` leaves a neutral linear Fog instead of `null`: the fog presence and type never change, so no program recompiles. */\n readonly stable?: boolean\n /** Colour of the neutral fog. Default 0x000000. */\n readonly color?: number\n}\n\nexport interface FogController {\n useLinear(color: number, near: number, far: number): Fog\n useExp2(color: number, density: number): FogExp2\n /** Back to no fog: the original fog (or `null`); when `stable`, the host's own linear fog or the neutral one, never `null`. */\n clear(): void\n}\n\nfunction isLinearFog(fog: FogBase | null): fog is Fog {\n return (fog as Partial<Fog> | null)?.isFog === true\n}\n\nfunction isExp2Fog(fog: FogBase | null): fog is FogExp2 {\n return (fog as Partial<FogExp2> | null)?.isFogExp2 === true\n}\n\nexport function isNeutralFog(fog: unknown): boolean {\n const candidate = fog as Partial<Fog> | null | undefined\n return candidate?.isFog === true && candidate.near === NEUTRAL_FOG_NEAR && candidate.far === NEUTRAL_FOG_FAR\n}\n\n/** Writes into the scene's existing fog object when its type matches, as the weather layer does, so nothing is reallocated per change. */\nexport function createFog(THREE: Pick<ThreeLib, 'Fog' | 'FogExp2'>, scene: Scene, options: FogOptions = {}): FogController {\n const original = scene.fog\n const hostLinear = isLinearFog(original) ? { color: original.color.getHex(), near: original.near, far: original.far } : undefined\n const resting = hostLinear ?? { color: options.color ?? 0, near: NEUTRAL_FOG_NEAR, far: NEUTRAL_FOG_FAR }\n return {\n useLinear(color, near, far) {\n const current = scene.fog\n if (isLinearFog(current)) {\n current.color.set(color)\n current.near = near\n current.far = far\n return current\n }\n const fog = new THREE.Fog(color, near, far)\n scene.fog = fog\n return fog\n },\n useExp2(color, density) {\n const current = scene.fog\n if (isExp2Fog(current)) {\n current.color.set(color)\n current.density = density\n return current\n }\n const fog = new THREE.FogExp2(color, density)\n scene.fog = fog\n return fog\n },\n clear() {\n if (options.stable !== true) {\n scene.fog = original\n return\n }\n const current = scene.fog\n if (isLinearFog(current)) {\n current.color.set(resting.color)\n current.near = resting.near\n current.far = resting.far\n return\n }\n scene.fog = new THREE.Fog(resting.color, resting.near, resting.far)\n },\n }\n}\n","import type { Object3D, Scene } from 'three'\n\nimport { NEUTRAL_FOG_NEAR } from '../fog'\nimport type { FogState, LightCheck, RendererState, SealOptions } from './types'\n\ntype Flags = Record<string, unknown>\ntype Counter = { count: number; shadows: number }\n\nconst LIGHT_FLAGS = [\n ['point', 'isPointLight'],\n ['spot', 'isSpotLight'],\n ['hemi', 'isHemisphereLight'],\n ['dir', 'isDirectionalLight'],\n ['ambient', 'isAmbientLight'],\n ['rect', 'isRectAreaLight'],\n] as const\n\n/** The visible lights the way the renderer collects them: they decide the shader's light counts. */\nexport function lightSignature(scene: Object3D): string {\n const counters: Record<string, Counter> = {}\n for (const [name] of LIGHT_FLAGS) counters[name] = { count: 0, shadows: 0 }\n scene.traverseVisible((object) => {\n const flags = object as unknown as Flags\n for (const [name, flag] of LIGHT_FLAGS) {\n if (flags[flag] !== true) continue\n const counter = counters[name] as Counter\n counter.count++\n if ((flags as { castShadow?: boolean }).castShadow === true) counter.shadows++\n }\n })\n return LIGHT_FLAGS.map(([name]) => `${name}:${(counters[name] as Counter).count}/${(counters[name] as Counter).shadows}`).join(' ')\n}\n\nexport function fogStateOf(scene: Scene): FogState {\n const fog = scene.fog as Flags | null\n if (fog === null) return 'none'\n return fog.isFogExp2 === true ? 'exp2' : 'linear'\n}\n\n/** Every PointLight/SpotLight under `scene` that is not under `group`, as a path of child indices. */\nexport function findStrays(scene: Object3D, group: Object3D): string[] {\n const strays: string[] = []\n const visit = (object: Object3D, path: string): void => {\n if (object === group) return\n const flags = object as unknown as Flags\n if (flags.isPointLight === true || flags.isSpotLight === true) strays.push(`${path}${object.name === '' ? object.type : object.name}`)\n object.children.forEach((child, index) => visit(child, `${path}${index}/`))\n }\n visit(scene, '')\n return strays\n}\n\nexport type Sealed = {\n readonly lights: string\n readonly environment: string\n readonly toneMapping: number\n readonly outputColorSpace: string\n readonly targetColorSpace: string\n readonly shadowMapType: number\n readonly fogStates: readonly FogState[]\n readonly cameras: SealOptions['cameras']\n readonly target: SealOptions['target']\n}\n\nfunction environmentOf(scene: Scene): string {\n return scene.environment === null ? 'none' : `texture:${scene.environment.mapping}`\n}\n\nfunction targetColorSpaceOf(target: SealOptions['target']): string {\n return target ? target.texture.colorSpace : 'canvas'\n}\n\nexport function captureSealed(renderer: RendererState, scene: Scene, options: SealOptions): Sealed {\n return {\n lights: lightSignature(scene),\n environment: environmentOf(scene),\n toneMapping: renderer.toneMapping,\n outputColorSpace: renderer.outputColorSpace,\n targetColorSpace: targetColorSpaceOf(options.target),\n shadowMapType: renderer.shadowMap.type,\n fogStates: options.fogStates ?? [fogStateOf(scene)],\n cameras: options.cameras,\n target: options.target,\n }\n}\n\ntype Described = {\n readonly lights: string\n readonly fog: string\n readonly environment: string\n readonly toneMapping: number\n readonly outputColorSpace: string\n readonly targetColorSpace: string\n readonly shadowMapType: number\n}\n\nfunction describe(state: Described): string {\n const { lights, fog, environment, toneMapping, outputColorSpace, targetColorSpace, shadowMapType } = state\n return `${lights} | fog:${fog} env:${environment} tone:${toneMapping} out:${outputColorSpace}/${targetColorSpace} shadow:${shadowMapType}`\n}\n\nfunction farBeyondNeutralFog(scene: Scene, sealed: Sealed): boolean {\n const fog = scene.fog as { isFog?: boolean; near?: number } | null\n if (fog?.isFog !== true || fog.near !== NEUTRAL_FOG_NEAR) return false\n return (sealed.cameras ?? []).some((camera) => ((camera as unknown as { far?: number }).far ?? 0) >= NEUTRAL_FOG_NEAR)\n}\n\nexport function checkSealed(sealed: Sealed | null, renderer: RendererState, scene: Scene, group: Object3D): LightCheck {\n const strays = findStrays(scene, group)\n const lights = lightSignature(scene)\n if (sealed === null) return { ok: strays.length === 0, expected: lights, actual: lights, strays }\n const fog = fogStateOf(scene)\n const fogAllowed = sealed.fogStates.includes(fog)\n const expected = describe({ ...sealed, fog: fogAllowed ? fog : sealed.fogStates.join('|') })\n const actual = describe({\n lights,\n fog,\n environment: environmentOf(scene),\n toneMapping: renderer.toneMapping,\n outputColorSpace: renderer.outputColorSpace,\n targetColorSpace: targetColorSpaceOf(sealed.target),\n shadowMapType: renderer.shadowMap.type,\n })\n const ok = strays.length === 0 && expected === actual && !farBeyondNeutralFog(scene, sealed)\n return { ok, expected, actual, strays }\n}\n","import type { HemisphereLight, Object3D, Scene } from 'three'\n\nimport { type ArbiterCore, admitRequest, arbitrateSources, poolOf, stepSlots } from './arbiter'\nimport { type Sealed, captureSealed, checkSealed } from './scene-state'\nimport { type Holder, type Slot, type Vec3, createSlots, createValues, isFree, markShadow, mergeValues, vacate, writeLight } from './slots'\nimport type {\n LightBudget,\n LightBudgetOptions,\n LightHandle,\n LightKind,\n LightLease,\n LightPatch,\n LightRequest,\n LightSource,\n RendererState,\n SourceHandle,\n ThreeForLights,\n} from './types'\n\nconst DEFAULTS = { point: 16, spot: 4, pointShadows: 2, spotShadows: 0, shadowMapSize: 512, urgentReserve: 2, fadeMs: 120, hysteresis: 0.15, hemisphere: 0 }\nconst ZERO: Vec3 = [0, 0, 0]\nconst ALL_LAYERS_MASK = -1\n\nfunction newHolder(kind: Holder['kind'], lightKind: LightKind, request: LightRequest | LightSource, urgent: boolean): Holder {\n return {\n kind,\n owner: request.owner,\n lightKind,\n priority: request.priority,\n urgent,\n values: createValues({ color: request.color, intensity: request.intensity, distance: request.distance, position: [...request.position], ...optional(request) }),\n slot: null,\n alive: true,\n onLost: undefined,\n follow: undefined,\n shadow: false,\n wanted: true,\n distance: 0,\n resolved: createValues({ color: request.color, intensity: 0, distance: 0, position: ZERO }),\n }\n}\n\nfunction optional(request: LightRequest | LightSource): Partial<Holder['values']> {\n const values: Partial<Holder['values']> = {}\n if (request.decay !== undefined) values.decay = request.decay\n if (request.target !== undefined) values.target = [...request.target]\n if (request.angle !== undefined) values.angle = request.angle\n if (request.penumbra !== undefined) values.penumbra = request.penumbra\n return values\n}\n\nfunction isAttached(object: Object3D, root: Object3D): boolean {\n for (let node: Object3D | null = object; node !== null; node = node.parent) if (node === root) return true\n return false\n}\n\nexport function createLightBudget(THREE: ThreeForLights, scene: Object3D, options: LightBudgetOptions = {}): LightBudget {\n const opts = { ...DEFAULTS, ...options }\n const pointShadows = Math.min(opts.pointShadows, opts.point)\n const spotShadows = Math.min(opts.spotShadows, opts.spot)\n const group = new THREE.Group()\n group.name = 'light-budget'\n const core: ArbiterCore = {\n THREE,\n slots: {\n point: createSlots(THREE, group, 'point', opts.point, pointShadows, opts.shadowMapSize, options.shadowSettings),\n spot: createSlots(THREE, group, 'spot', opts.spot, spotShadows, opts.shadowMapSize, options.shadowSettings),\n },\n urgentReserve: opts.urgentReserve,\n fadeMs: opts.fadeMs,\n hysteresis: opts.hysteresis,\n preempted: 0,\n refused: 0,\n }\n const hemispheres: HemisphereLight[] = []\n for (let index = 0; index < opts.hemisphere; index++) {\n const light = new THREE.HemisphereLight(0xffffff, 0xffffff, 0)\n light.layers.enableAll()\n group.add(light)\n hemispheres.push(light)\n }\n scene.add(group)\n scene.children.splice(scene.children.indexOf(group), 1)\n scene.children.unshift(group)\n\n const sources = new Set<Holder>()\n const world = new THREE.Vector3()\n const direction = new THREE.Vector3()\n let sealed: Sealed | null = null\n let leasedHemispheres = 0\n\n const allSlots = (): Slot[] => [...core.slots.point, ...core.slots.spot]\n\n function writeHolder(holder: Holder): void {\n if (holder.slot !== null && holder.slot.holder === holder) writeLight(THREE, holder.slot, holder.values)\n }\n\n function makeHandle(holder: Holder, release: ((fadeMs: number | undefined) => void) | null): LightHandle {\n return {\n get slot() {\n return holder.slot?.index ?? -1\n },\n kind: holder.lightKind,\n get alive() {\n return holder.alive\n },\n update(patch: LightPatch) {\n if (!holder.alive) return\n mergeValues(holder.values, patch)\n writeHolder(holder)\n },\n release(releaseOptions) {\n if (!holder.alive || release === null) return\n holder.alive = false\n release(releaseOptions?.fadeMs)\n },\n }\n }\n\n function dropHolder(holder: Holder, fadeMs: number): void {\n const slot = holder.slot\n holder.slot = null\n if (slot === null) return\n if (slot.pending === holder) slot.pending = null\n else if (slot.holder === holder) vacate(slot, fadeMs)\n }\n\n function resolveSource(holder: Holder, focus: readonly [number, number, number]): void {\n const { follow, values, resolved } = holder\n holder.wanted = values.intensity > 0\n if (follow === undefined) {\n holder.distance = Math.hypot(values.position[0] - focus[0], values.position[1] - focus[1], values.position[2] - focus[2])\n Object.assign(resolved, values)\n return\n }\n holder.wanted = holder.wanted && isAttached(follow, scene)\n if (!holder.wanted) return\n follow.getWorldPosition(world)\n const [ox, oy, oz] = values.position\n if (ox !== 0 || oy !== 0 || oz !== 0) world.set(ox, oy, oz).applyMatrix4(follow.matrixWorld)\n const axis = follow.matrixWorld.elements\n direction.set(-(axis[8] as number), -(axis[9] as number), -(axis[10] as number))\n holder.distance = Math.hypot(world.x - focus[0], world.y - focus[1], world.z - focus[2])\n Object.assign(resolved, values)\n resolved.position = [world.x, world.y, world.z]\n resolved.target = values.target ?? [world.x + direction.x, world.y + direction.y, world.z + direction.z]\n }\n\n return {\n group,\n sizes: { point: opts.point, spot: opts.spot, pointShadows, spotShadows },\n acquire(request) {\n const holder = newHolder('request', request.kind ?? 'point', request, request.urgent === true)\n holder.onLost = request.onLost\n if (!admitRequest(core, holder)) return null\n return makeHandle(holder, (fadeMs) => dropHolder(holder, fadeMs ?? opts.fadeMs))\n },\n addSource(source) {\n const holder = newHolder('source', source.kind ?? 'point', source, false)\n holder.follow = source.follow\n holder.shadow = source.shadow === true\n sources.add(holder)\n return {\n get slot() {\n return holder.slot !== null && holder.slot.holder === holder ? holder.slot.index : -1\n },\n update(patch: Partial<LightSource>) {\n mergeValues(holder.values, patch)\n if (patch.priority !== undefined) holder.priority = patch.priority\n if ('follow' in patch) holder.follow = patch.follow\n if (holder.follow === undefined) writeHolder(holder)\n },\n remove() {\n holder.alive = false\n holder.wanted = false\n sources.delete(holder)\n dropHolder(holder, opts.fadeMs)\n },\n } satisfies SourceHandle\n },\n lease({ owner, point, shadows = 0 }) {\n const shadowSlots = core.slots.point.filter((slot) => slot.shadow && isFree(slot))\n const plainSlots = core.slots.point.filter((slot) => !slot.shadow && isFree(slot))\n if (shadowSlots.length < shadows || plainSlots.length < point - shadows) return null\n const taken = [...shadowSlots.slice(0, shadows), ...plainSlots.slice(0, point - shadows)]\n const holders = taken.map((slot) => {\n const holder = newHolder('lease', 'point', { owner, priority: Number.POSITIVE_INFINITY, color: 0xffffff, intensity: 0, distance: 0, position: ZERO }, false)\n slot.leased = true\n slot.holder = holder\n slot.fade = 1\n slot.fadeTarget = 1\n holder.slot = slot\n markShadow(slot)\n return holder\n })\n const handles = holders.map((holder) => makeHandle(holder, null))\n return {\n handles,\n invalidateShadow(index) {\n for (const [at, holder] of holders.entries()) if ((index === undefined || index === at) && holder.slot !== null) markShadow(holder.slot)\n },\n release() {\n for (const holder of holders) {\n const slot = holder.slot\n holder.alive = false\n holder.slot = null\n if (slot === null) continue\n slot.leased = false\n slot.holder = null\n vacate(slot, 0)\n }\n },\n } satisfies LightLease\n },\n leaseHemisphere() {\n const light = hemispheres[leasedHemispheres]\n if (light === undefined) return null\n leasedHemispheres++\n return light\n },\n present(focus, dtSeconds) {\n for (const holder of sources) resolveSource(holder, focus)\n for (const kind of ['point', 'spot'] as const) {\n const reserve = kind === 'point' ? core.urgentReserve : 0\n const kindSources = [...sources].filter((holder) => holder.lightKind === kind)\n arbitrateSources(core, poolOf(core, kind, true), kindSources.filter((holder) => holder.shadow), 0)\n arbitrateSources(core, poolOf(core, kind, false), kindSources.filter((holder) => !holder.shadow), reserve)\n }\n stepSlots(core, dtSeconds)\n for (const holder of sources) if (holder.wanted && holder.slot?.holder === holder) writeLight(THREE, holder.slot, holder.resolved)\n },\n seal(renderer, scene, sealOptions = {}) {\n sealed = captureSealed(renderer, scene, sealOptions)\n },\n check(renderer: RendererState, checkedScene: Scene) {\n const result = checkSealed(sealed, renderer, checkedScene, group)\n const layersIntact = allSlots().every((slot) => slot.light.visible && slot.light.layers.mask === ALL_LAYERS_MASK)\n const isFirst = checkedScene.children[0] === group\n if (!layersIntact) return { ...result, ok: false, actual: `${result.actual} | budget lights hidden or off a layer` }\n return isFirst ? result : { ...result, ok: false, actual: `${result.actual} | budget group is not the first child of the scene` }\n },\n stats() {\n const slots = allSlots()\n return {\n lit: slots.filter((slot) => slot.light.intensity > 0).length,\n free: slots.filter(isFree).length,\n leased: slots.filter((slot) => slot.leased).length,\n sources: sources.size,\n sourcesWithoutSlot: [...sources].filter((holder) => holder.wanted && holder.slot === null).length,\n preempted: core.preempted,\n refused: core.refused,\n }\n },\n dispose() {\n group.removeFromParent()\n for (const slot of allSlots()) slot.light.dispose()\n sources.clear()\n },\n }\n}\n","import type { Camera, Group, HemisphereLight, Object3D, Scene, WebGLRenderTarget } from 'three'\n\nimport type { ThreeLib } from '../three-lib'\n\nexport type LightKind = 'point' | 'spot'\nexport type LinearRgb = readonly [number, number, number]\nexport type FogState = 'none' | 'linear' | 'exp2'\nexport type ThreeForLights = Pick<\n ThreeLib,\n 'Group' | 'PointLight' | 'SpotLight' | 'HemisphereLight' | 'Object3D' | 'Vector3' | 'Color' | 'LinearSRGBColorSpace'\n>\n/** What the budget reads of a renderer: a structural subset, so a fake works in Node. */\nexport type RendererState = {\n readonly toneMapping: number\n readonly outputColorSpace: string\n readonly shadowMap: { readonly type: number }\n}\n\nexport interface LightBudgetOptions {\n /** Point lights created at boot. Default 16. */\n readonly point?: number\n /** Spot lights created at boot. Default 4. */\n readonly spot?: number\n /** How many of the point lights cast shadows (cube maps). Default 2. Fixed for the life of the budget. */\n readonly pointShadows?: number\n /** How many of the spot lights cast shadows. Default 0. */\n readonly spotShadows?: number\n /** Shadow map size of the shadow slots. Default 512. */\n readonly shadowMapSize?: number\n /** Bias, normal bias and camera near plane of the shadow slots. Defaults: three.js's (0, 0, 0.5). The house's room lights use -0.0012, 0.05 and 0.12. */\n readonly shadowSettings?: { readonly bias?: number; readonly normalBias?: number; readonly near?: number }\n /** Point slots only `urgent` requests may take while others are free. Default 2. */\n readonly urgentReserve?: number\n /** Milliseconds a light fades out when a source loses its slot, and fades in when it gets one. Default 120. */\n readonly fadeMs?: number\n /** A source keeps its slot until a challenger of the same priority is this much closer (0.15 = 15%). Default 0.15. */\n readonly hysteresis?: number\n /** HemisphereLights created at boot for adapters that flash the sky; leased with `leaseHemisphere`. Default 0. */\n readonly hemisphere?: number\n}\n\n/** A light that lives as long as the request: a blast, a rocket in flight, a muzzle flash, a flare in the hand. */\nexport interface LightRequest {\n readonly owner: string\n readonly kind?: LightKind\n /** Higher wins a slot; see LIGHT_PRIORITY. */\n readonly priority: number\n /** Hex (sRGB, like `new Color(hex)`) or linear RGB. */\n readonly color: number | LinearRgb\n readonly intensity: number\n readonly distance: number\n readonly decay?: number\n readonly position: readonly [number, number, number]\n /** Spot only. */\n readonly target?: readonly [number, number, number]\n readonly angle?: number\n readonly penumbra?: number\n /** Takes a slot at once: cuts the lowest victim instead of fading it, and may use the urgent reserve. */\n readonly urgent?: boolean\n /** Called once when a higher priority took the slot; the handle is dead from then on. */\n readonly onLost?: () => void\n}\n\nexport type LightPatch = Partial<Pick<LightRequest, 'color' | 'intensity' | 'distance' | 'decay' | 'position' | 'target' | 'angle' | 'penumbra'>>\n\nexport interface LightHandle {\n /** Index of this light among the scene's point (or spot) lights in the shader; stable while the handle lives. */\n readonly slot: number\n readonly kind: LightKind\n readonly alive: boolean\n update(patch: LightPatch): void\n /** Gives the slot back; with fadeMs the light fades first. Idempotent. */\n release(options?: { readonly fadeMs?: number }): void\n}\n\n/** A light that exists in the world for a long time and competes for a slot every frame: a lamp, a street light, a headlight. */\nexport interface LightSource extends Omit<LightRequest, 'urgent' | 'onLost'> {\n /** Wants a shadow slot; a shadow source competes for shadow slots only. */\n readonly shadow?: boolean\n /**\n * The object the light sits on. Its world position (and, for a spot, its world -z as the direction when `target` is absent) is read\n * once per `present`; `position` is then an offset in its local space. A source whose `follow` left the scene counts as off.\n */\n readonly follow?: Object3D\n}\n\nexport interface SourceHandle {\n /** Slot now, or -1 while it has none (its light is not drawn). */\n readonly slot: number\n update(patch: Partial<LightSource>): void\n remove(): void\n}\n\n/** Slots a client keeps for itself for good: the room lights of a house (their masks are indexed by slot). */\nexport interface LightLease {\n readonly handles: readonly LightHandle[]\n /** Redraws the shadow map of one leased slot (`handles[index]`), or of all of them, on the next render: the walls it shades changed. */\n invalidateShadow(index?: number): void\n release(): void\n}\n\nexport interface SealOptions {\n /** Fog states the game will switch between at run time. Default: only the state at seal time. */\n readonly fogStates?: readonly FogState[]\n /** Cameras whose `far` must stay under the neutral fog's near (read at every `check`). */\n readonly cameras?: readonly Camera[]\n /** Where the game really draws; its colour space is part of the sealed state. */\n readonly target?: WebGLRenderTarget | null\n}\n\nexport interface LightCheck {\n readonly ok: boolean\n readonly expected: string\n readonly actual: string\n /** Paths of PointLight/SpotLight found outside the budget's group. */\n readonly strays: readonly string[]\n}\n\nexport interface LightBudgetStats {\n readonly lit: number\n readonly free: number\n readonly leased: number\n readonly sources: number\n readonly sourcesWithoutSlot: number\n readonly preempted: number\n readonly refused: number\n}\n\nexport interface LightBudget {\n /** The group holding every light; first child of the scene. Never hidden, never removed while the budget lives. */\n readonly group: Group\n readonly sizes: { readonly point: number; readonly spot: number; readonly pointShadows: number; readonly spotShadows: number }\n acquire(request: LightRequest): LightHandle | null\n addSource(source: LightSource): SourceHandle\n /** Takes `point` slots (of which `shadows` shadow slots) for good. null when not enough unleased slots remain. */\n lease(options: { readonly owner: string; readonly point: number; readonly shadows?: number }): LightLease | null\n /** Takes one of the boot HemisphereLights for good. null when none is left. Its intensity starts at 0. */\n leaseHemisphere(owner: string): HemisphereLight | null\n /** Once per frame, with the camera (or the player) as focus. `dtSeconds` is real time: fades do not stop in pause. */\n present(focus: readonly [number, number, number], dtSeconds: number): void\n /** After this, any change in the program-key state of the scene is a bug and `check()` reports it. */\n seal(renderer: RendererState, scene: Scene, options?: SealOptions): void\n check(renderer: RendererState, scene: Scene): LightCheck\n stats(): LightBudgetStats\n dispose(): void\n}\n\nexport const LIGHT_PRIORITY = {\n blast: 900,\n rocket: 800,\n muzzle: 750,\n fire: 600,\n flare: 500,\n headlight: 400,\n weaponLight: 400,\n lamp: 200,\n street: 150,\n} as const\n","import type { LightBudget } from './types'\n\n/** For adapters that receive the budget as `unknown` through the host (the contract does not know three). */\nexport function isLightBudget(value: unknown): value is LightBudget {\n const candidate = value as Partial<LightBudget> | null | undefined\n return typeof candidate === 'object' && candidate !== null && typeof candidate.acquire === 'function' && typeof candidate.addSource === 'function' && typeof candidate.lease === 'function' && typeof candidate.present === 'function'\n}\n","import type { Material } from 'three'\n\n/** What `programKeysOf` reads of a renderer: `properties` is internal to three r160 and may be absent in another version. */\nexport type PropertiesReader = { readonly properties?: { get(object: object): unknown } }\n\n/**\n * Every program cache key three linked for `material` (one per state it was drawn in: fog type, shadow, light counts). Reads three's\n * internal `properties.get(material).programs` Map (r160). `[]` for a material never drawn; `undefined` when this three does not have\n * that internal, which sends the pools to their conservative mode.\n */\nexport function programKeysOf(renderer: PropertiesReader, material: Material): readonly string[] | undefined {\n const properties = renderer.properties\n if (typeof properties?.get !== 'function') return undefined\n const programs = (properties.get(material) as { programs?: unknown } | undefined)?.programs\n if (programs === undefined) return []\n if (!(programs instanceof Map)) return undefined\n return [...programs.keys()].map(String)\n}\n","import type { Material } from 'three'\n\nimport { type PropertiesReader, programKeysOf } from '../program-key'\n\n/** Materials kept alive and never disposed, one per program family: what keeps a program linked once its users are gone. */\nexport interface MaterialAnchors {\n /**\n * Keeps `material` if any of its program keys has no anchor yet; returns true when it was kept (the caller must then not dispose it).\n * A material whose keys are all covered returns false and may be disposed. A material never drawn is not kept.\n */\n offer(material: Material): boolean\n readonly size: number\n /** Where program keys are read from. Until bound (or when three has no readable keys) every offered material is kept. */\n bind(renderer: PropertiesReader): void\n}\n\nexport function createMaterialAnchors(renderer?: PropertiesReader): MaterialAnchors {\n const anchors = new Set<Material>()\n let reader = renderer\n\n function coveredKeys(): Set<string> {\n const covered = new Set<string>()\n if (reader === undefined) return covered\n for (const anchor of anchors) for (const key of programKeysOf(reader, anchor) ?? []) covered.add(key)\n return covered\n }\n\n return {\n get size() {\n return anchors.size\n },\n bind(next) {\n reader = next\n },\n offer(material) {\n if (anchors.has(material)) return true\n const keys = reader === undefined ? undefined : programKeysOf(reader, material)\n if (keys === undefined) {\n anchors.add(material)\n return true\n }\n if (keys.length === 0) return false\n const covered = coveredKeys()\n if (keys.every((key) => covered.has(key))) return false\n anchors.add(material)\n return true\n },\n }\n}\n","import type { Object3D } from 'three'\n\nimport type { ThreeLib } from './three-lib'\nimport type { WarmupProbe } from './warmup-types'\n\n/** The sprite probes the room builds by hand today: one sprite material with fog and one without. */\nexport function spriteProbes(THREE: Pick<ThreeLib, 'Group' | 'Sprite' | 'SpriteMaterial' | 'DataTexture'>): WarmupProbe {\n return {\n id: 'sprite',\n build() {\n const group = new THREE.Group()\n const texture = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1)\n texture.needsUpdate = true\n const materials = [true, false].map((fog) => new THREE.SpriteMaterial({ map: texture, fog, transparent: true }))\n for (const material of materials) group.add(new THREE.Sprite(material))\n return { group, materials, dispose: () => texture.dispose() }\n },\n }\n}\n\nexport function hasMissingDistanceMaterial(root: Object3D): string | undefined {\n let found: string | undefined\n root.traverse((object) => {\n const mesh = object as { isInstancedMesh?: boolean; castShadow?: boolean; customDepthMaterial?: unknown; customDistanceMaterial?: unknown }\n if (found === undefined && mesh.isInstancedMesh === true && mesh.castShadow === true && mesh.customDepthMaterial && !mesh.customDistanceMaterial) found = object.name || object.type\n })\n return found\n}\n","import type { BufferGeometry, Camera, Material, Object3D, Scene } from 'three'\n\nimport { fogStateOf, lightSignature } from './lights/scene-state'\nimport type { FogState } from './lights/types'\nimport { createMaterialAnchors } from './pools/anchors'\nimport { hasMissingDistanceMaterial } from './probes'\nimport type { ThreeLib } from './three-lib'\nimport type { EffectWarmup, ProgramWarmup, WarmupContext, WarmupOptions, WarmupProbe, WarmupReport, WarmupRenderer } from './warmup-types'\n\nconst EFFECT_STEP_SECONDS = 1 / 50\nconst DEFAULT_EFFECT_FRAMES = 3\nconst WARM_FOG_DENSITY = 0.01\nconst WARM_FOG_FAR = 100\n\ntype LinkCounter = { count: number }\ntype BuiltProbe = { readonly probe: WarmupProbe; readonly built: ReturnType<WarmupProbe['build']> }\n\nfunction register<TItem>(items: Set<TItem>, item: TItem): () => void {\n items.add(item)\n return () => void items.delete(item)\n}\n\nfunction countLinks(context: WarmupContext, counters: WeakMap<WarmupContext, LinkCounter>): void {\n const existing = counters.get(context)\n if (existing !== undefined) {\n existing.count = 0\n return\n }\n const counter: LinkCounter = { count: 0 }\n counters.set(context, counter)\n const original = context.linkProgram.bind(context)\n context.linkProgram = (program) => {\n counter.count++\n original(program)\n }\n}\n\nfunction failedProgram(renderer: WarmupRenderer): string | undefined {\n for (const program of renderer.info.programs ?? []) {\n const diagnostics = (program as { diagnostics?: { runnable?: boolean; programLog?: string } }).diagnostics\n if (diagnostics?.runnable === false) return diagnostics.programLog ?? 'program failed to link'\n }\n return undefined\n}\n\nexport function createProgramWarmup(THREE: Pick<ThreeLib, 'Group' | 'Sprite' | 'SpriteMaterial' | 'DataTexture' | 'Fog' | 'FogExp2' | 'Mesh' | 'Points' | 'LineSegments' | 'BufferGeometry' | 'BufferAttribute'>): ProgramWarmup {\n const probes = new Set<WarmupProbe>()\n const effects = new Set<EffectWarmup>()\n const textures = new Set<Parameters<WarmupRenderer['initTexture']>[0]>()\n const anchors = createMaterialAnchors()\n const linkCounters = new WeakMap<WarmupContext, LinkCounter>()\n\n function setFog(scene: Scene, state: FogState): void {\n const color = scene.fog?.color.getHex() ?? 0\n if (state === 'none') scene.fog = null\n else if (state === 'linear' && (scene.fog as { isFog?: boolean } | null)?.isFog !== true) scene.fog = new THREE.Fog(color, 1, WARM_FOG_FAR)\n else if (state === 'exp2' && (scene.fog as { isFogExp2?: boolean } | null)?.isFogExp2 !== true) scene.fog = new THREE.FogExp2(color, WARM_FOG_DENSITY)\n }\n\n function draw(renderer: WarmupRenderer, scene: Scene, camera: Camera, target: WarmupOptions['target']): void {\n renderer.setRenderTarget(target ?? null)\n renderer.compile(scene, camera)\n renderer.render(scene, camera)\n }\n\n function playEffect(renderer: WarmupRenderer, scene: Scene, camera: Camera, effect: EffectWarmup, target: WarmupOptions['target']): void {\n effect.play()\n try {\n for (let frame = 0; frame < (effect.frames ?? DEFAULT_EFFECT_FRAMES); frame++) {\n effect.step(EFFECT_STEP_SECONDS)\n renderer.setRenderTarget(target ?? null)\n renderer.render(scene, camera)\n }\n } finally {\n effect.stop()\n }\n }\n\n /** One object per material, of the kind its program is for, each a single triangle: drawn once so that the program is used, not only linked. */\n function materialsProbe(materials: readonly Material[]): BuiltProbe {\n const geometry = new THREE.BufferGeometry()\n geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array([0, 0, 0, 0.001, 0, 0, 0, 0.001, 0]), 3))\n const group = new THREE.Group()\n for (const material of materials) group.add(drawableOf(material, geometry))\n return { probe: { id: 'materials', build: () => ({ group, materials: [], dispose: () => geometry.dispose() }) }, built: { group, materials: [], dispose: () => geometry.dispose() } }\n }\n\n function drawableOf(material: Material, geometry: BufferGeometry): Object3D {\n const flags = material as { isSpriteMaterial?: boolean; isPointsMaterial?: boolean; isLineBasicMaterial?: boolean }\n const object: Object3D =\n flags.isSpriteMaterial === true ? new THREE.Sprite(material as never)\n : flags.isPointsMaterial === true ? new THREE.Points(geometry, material)\n : flags.isLineBasicMaterial === true ? new THREE.LineSegments(geometry, material)\n : new THREE.Mesh(geometry, material)\n object.frustumCulled = false\n return object\n }\n\n function buildProbes(materials: readonly Material[] | undefined): BuiltProbe[] {\n const built = [...probes].map((probe) => ({ probe, built: probe.build() }))\n if (materials !== undefined && materials.length > 0) built.push(materialsProbe(materials))\n return built\n }\n\n function addProbes(scene: Scene, built: readonly BuiltProbe[]): void {\n for (const { probe, built: item } of built) {\n const missing = hasMissingDistanceMaterial(item.group)\n if (missing !== undefined) throw new Error(`warmup probe \"${probe.id}\": instanced mesh \"${missing}\" casts shadows with a customDepthMaterial but no customDistanceMaterial`)\n scene.add(item.group)\n }\n }\n\n function removeProbes(scene: Scene, built: readonly BuiltProbe[]): void {\n for (const { built: item } of built) {\n scene.remove(item.group)\n item.dispose()\n for (const material of item.materials) anchors.offer(material)\n }\n }\n\n function drawStates(renderer: WarmupRenderer, scene: Scene, camera: Camera, states: readonly FogState[], target: WarmupOptions['target']): void {\n for (const state of states) {\n setFog(scene, state)\n draw(renderer, scene, camera, target)\n }\n }\n\n /** Makes every instanced batch draw at least once: three skips an empty instanced batch, and a driver finishes a program's state on its first real draw (a 200 ms stall). Returns the undo. */\n function forceDraws(scene: Scene): () => void {\n const undo: Array<() => void> = []\n scene.traverse((object) => {\n const drawable = object as Object3D & { isInstancedMesh?: boolean; count?: number; geometry?: { isInstancedBufferGeometry?: boolean; instanceCount?: number } }\n const { geometry } = drawable\n if (geometry?.isInstancedBufferGeometry === true && geometry.instanceCount === 0) {\n geometry.instanceCount = 1\n undo.push(() => (geometry.instanceCount = 0))\n }\n if (drawable.isInstancedMesh === true && drawable.count === 0) {\n drawable.count = 1\n undo.push(() => (drawable.count = 0))\n }\n })\n return () => undo.forEach((restore) => restore())\n }\n\n return {\n anchors,\n register: (probe) => register(probes, probe),\n registerEffect: (effect) => register(effects, effect),\n registerTexture: (texture) => register(textures, texture),\n warmUp(renderer, scene, camera, options = {}): WarmupReport {\n const started = performance.now()\n const { lights, target } = options\n anchors.bind(renderer)\n const originalFog = scene.fog\n const states = options.fogStates ?? [fogStateOf(scene)]\n lights?.seal(renderer, scene, { fogStates: options.fogStates, cameras: [camera], target })\n scene.traverse((object) => {\n const light = object as { castShadow?: boolean; shadow?: { needsUpdate: boolean } }\n if (light.castShadow === true && light.shadow !== undefined) light.shadow.needsUpdate = true\n })\n const built = buildProbes(options.materials)\n let restoreDraws = (): void => undefined\n try {\n addProbes(scene, built)\n restoreDraws = forceDraws(scene)\n drawStates(renderer, scene, camera, states, target)\n scene.fog = originalFog\n for (const effect of effects) playEffect(renderer, scene, camera, effect, target)\n for (const texture of textures) renderer.initTexture(texture)\n } finally {\n restoreDraws()\n scene.fog = originalFog\n removeProbes(scene, built)\n }\n const context = renderer.getContext()\n const uniformVectors = Number(context.getParameter(context.MAX_FRAGMENT_UNIFORM_VECTORS))\n const failure = failedProgram(renderer)\n if (failure !== undefined) {\n throw new Error(`a shader program failed to link (MAX_FRAGMENT_UNIFORM_VECTORS ${uniformVectors}, lights: ${lightSignature(scene)}): ${failure}`)\n }\n countLinks(context, linkCounters)\n // the GPU process is still working through all of the above: wait here, at load, or the first upload of play stalls on it\n if (context.RGBA !== undefined && context.UNSIGNED_BYTE !== undefined) context.readPixels?.(0, 0, 1, 1, context.RGBA, context.UNSIGNED_BYTE, new Uint8Array(4))\n return {\n programs: renderer.info.programs?.length ?? 0,\n geometries: renderer.info.memory.geometries,\n textures: renderer.info.memory.textures,\n milliseconds: performance.now() - started,\n lightSignature: lightSignature(scene),\n uniformVectors,\n anchors: anchors.size,\n }\n },\n linkedSince(renderer) {\n return linkCounters.get(renderer.getContext())?.count ?? 0\n },\n }\n}\n","const POOLED = '__pooled'\n\ntype Disposable = { dispose(): void; userData: Record<string, unknown> }\n\nfunction isDevelopment(): boolean {\n return typeof process === 'undefined' || process.env.NODE_ENV !== 'production'\n}\n\n/** Marks a pool resource and, in development, warns when something disposes it directly instead of releasing it to the pool. */\nexport function markPooled(resource: Disposable, key: string, owner: string): { disposeForPool(): void } {\n resource.userData[POOLED] = key\n const original = resource.dispose.bind(resource)\n let fromPool = false\n if (isDevelopment()) {\n resource.dispose = () => {\n if (!fromPool) console.warn(`[game-render-three] ${owner} \"${key}\" was disposed directly; release it to its pool instead`)\n original()\n }\n }\n return {\n disposeForPool() {\n fromPool = true\n original()\n fromPool = false\n },\n }\n}\n\nexport function pooledKey(resource: { userData: Record<string, unknown> }): string | undefined {\n const key = resource.userData[POOLED]\n return typeof key === 'string' ? key : undefined\n}\n\n/** A number as a short stable string, rounded to `step`. */\nexport function roundedNumber(value: number, step: number): string {\n return String(Number((Math.round(value / step) * step).toFixed(6)))\n}\n","import type { BufferGeometry } from 'three'\n\nimport { markPooled, pooledKey, roundedNumber } from './pooled'\n\nexport interface GeometryPool {\n /** The geometry for `key`, built once with `build`. Counts one use. */\n acquire(key: string, build: () => BufferGeometry): BufferGeometry\n release(geometry: BufferGeometry): void\n /** Never disposed while the pool lives (unit shapes, effect geometries warmed at load). */\n retain(key: string): void\n /** Disposes unretained geometries with no use, oldest first, keeping at most `keep` idle ones. Only at load points, never in play. */\n trim(keep?: number): number\n stats(): { readonly entries: number; readonly retained: number; readonly inUse: number }\n dispose(): void\n}\n\ntype Entry = { geometry: BufferGeometry; uses: number; retained: boolean; idleSince: number; disposeForPool(): void }\n\nconst DEFAULT_KEY_STEP = 1e-3\n\n/** Builds a key from a kind and its numbers, rounded to `step`, so near-equal sizes share one geometry. */\nexport function geometryKey(kind: string, params: readonly number[], step: number = DEFAULT_KEY_STEP): string {\n return [kind, ...params.map((value) => roundedNumber(value, step))].join('|')\n}\n\nexport function createGeometryPool(): GeometryPool {\n const entries = new Map<string, Entry>()\n let clock = 0\n\n function disposeEntry(key: string, entry: Entry): void {\n entries.delete(key)\n entry.disposeForPool()\n }\n\n return {\n acquire(key, build) {\n let entry = entries.get(key)\n if (entry === undefined) {\n const geometry = build()\n entry = { geometry, uses: 0, retained: false, idleSince: 0, ...markPooled(geometry, key, 'geometry') }\n entries.set(key, entry)\n }\n entry.uses++\n return entry.geometry\n },\n release(geometry) {\n const key = pooledKey(geometry)\n const entry = key === undefined ? undefined : entries.get(key)\n if (entry === undefined || entry.uses === 0) return\n entry.uses--\n if (entry.uses === 0) entry.idleSince = ++clock\n },\n retain(key) {\n const entry = entries.get(key)\n if (entry !== undefined) entry.retained = true\n },\n trim(keep = 0) {\n const idle = [...entries].filter(([, entry]) => entry.uses === 0 && !entry.retained).sort((a, b) => a[1].idleSince - b[1].idleSince)\n const excess = idle.slice(0, Math.max(0, idle.length - keep))\n for (const [key, entry] of excess) disposeEntry(key, entry)\n return excess.length\n },\n stats() {\n const all = [...entries.values()]\n return { entries: all.length, retained: all.filter((entry) => entry.retained).length, inUse: all.filter((entry) => entry.uses > 0).length }\n },\n dispose() {\n for (const [key, entry] of [...entries]) disposeEntry(key, entry)\n },\n }\n}\n","import type { Material } from 'three'\n\nimport type { PropertiesReader } from '../program-key'\nimport { type MaterialAnchors, createMaterialAnchors } from './anchors'\nimport { markPooled, pooledKey, roundedNumber } from './pooled'\n\nexport interface MaterialPool {\n /**\n * The material for `key`, built once. Shared: callers must not change it. Per-instance variety goes into instance attributes, not\n * into new materials.\n */\n acquire<TMaterial extends Material>(key: string, build: () => TMaterial): TMaterial\n release(material: Material): void\n /** Declares a variant that must never be disposed (its program stays linked). Effects register theirs at load. */\n register<TMaterial extends Material>(key: string, build: () => TMaterial): TMaterial\n /**\n * A material the caller owns and changes per frame (a puff's opacity): not shared. On `release` the first one of its program family is\n * handed to the anchors, so disposing the others never frees the program.\n */\n own<TMaterial extends Material>(build: () => TMaterial): TMaterial\n /** The materials declared with `register`: the ones that must never be disposed and whose programs the warmup draws once. */\n registeredMaterials?(): readonly Material[]\n /** Disposes unregistered materials with no use, oldest first, keeping at most `keep` idle ones; a family's anchor is kept alive. */\n trim(keep?: number): number\n stats(): { readonly entries: number; readonly registered: number; readonly inUse: number; readonly families: number }\n /**\n * The end of the pool: disposes every material it made, in use or not, registered or anchored (the guarantee that a program stays linked\n * holds while the pool lives, not after it). The anchors object itself is not touched, and the pool is empty and usable again afterwards.\n */\n dispose(): void\n}\n\nexport type MaterialPoolOptions = { readonly anchors?: MaterialAnchors; readonly renderer?: PropertiesReader }\n\ntype Entry = { material: Material; uses: number; registered: boolean; idleSince: number; disposeForPool(): void }\n\nconst DEFAULT_KEY_STEP = 1e-3\n\n/** Key from the material class and every parameter that changes its look, sorted, numbers rounded. */\nexport function materialKey(type: string, params: Readonly<Record<string, unknown>>): string {\n const parts = Object.keys(params)\n .sort()\n .map((name) => {\n const value = params[name]\n return `${name}=${typeof value === 'number' ? roundedNumber(value, DEFAULT_KEY_STEP) : JSON.stringify(value)}`\n })\n return [type, ...parts].join('|')\n}\n\nexport function createMaterialPool(options: MaterialPoolOptions = {}): MaterialPool {\n const anchors = options.anchors ?? createMaterialAnchors()\n if (options.renderer !== undefined) anchors.bind(options.renderer)\n const entries = new Map<string, Entry>()\n const owned = new Map<Material, () => void>()\n let clock = 0\n\n function createEntry(key: string, material: Material, registered: boolean): Entry {\n const entry: Entry = { material, uses: 0, registered, idleSince: 0, ...markPooled(material, key, 'material') }\n entries.set(key, entry)\n return entry\n }\n\n /** Disposes the material unless it is the anchor of a program family. */\n function retire(material: Material, disposeForPool: () => void): boolean {\n if (anchors.offer(material)) return false\n disposeForPool()\n return true\n }\n\n return {\n acquire(key, build) {\n const entry = entries.get(key) ?? createEntry(key, build(), false)\n entry.uses++\n return entry.material as ReturnType<typeof build>\n },\n register(key, build) {\n const entry = entries.get(key) ?? createEntry(key, build(), true)\n entry.registered = true\n return entry.material as ReturnType<typeof build>\n },\n own(build) {\n const material = build()\n owned.set(material, markPooled(material, 'own', 'material').disposeForPool)\n return material\n },\n release(material) {\n const disposeOwned = owned.get(material)\n if (disposeOwned !== undefined) {\n owned.delete(material)\n retire(material, disposeOwned)\n return\n }\n const key = pooledKey(material)\n const entry = key === undefined ? undefined : entries.get(key)\n if (entry === undefined || entry.uses === 0) return\n entry.uses--\n if (entry.uses === 0) entry.idleSince = ++clock\n },\n trim(keep = 0) {\n const idle = [...entries].filter(([, entry]) => entry.uses === 0 && !entry.registered).sort((a, b) => a[1].idleSince - b[1].idleSince)\n let disposed = 0\n for (const [key, entry] of idle.slice(0, Math.max(0, idle.length - keep))) {\n entries.delete(key)\n if (retire(entry.material, entry.disposeForPool)) disposed++\n }\n return disposed\n },\n registeredMaterials() {\n return [...entries.values()].filter((entry) => entry.registered).map((entry) => entry.material)\n },\n stats() {\n const all = [...entries.values()]\n return {\n entries: all.length,\n registered: all.filter((entry) => entry.registered).length,\n inUse: all.filter((entry) => entry.uses > 0).length,\n families: anchors.size,\n }\n },\n dispose() {\n for (const entry of entries.values()) entry.disposeForPool()\n for (const disposeOwned of owned.values()) disposeOwned()\n entries.clear()\n owned.clear()\n },\n }\n}\n","import type { GeometryPool } from './geometry'\nimport type { MaterialPool } from './material'\n\n/** The pools a host lends its adapters (`host.pools`): geometries and materials that outlive the effect that asked for them. */\nexport interface RenderPools {\n readonly geometries: GeometryPool\n readonly materials: MaterialPool\n}\n\n/** For adapters that receive the pools as `unknown` through the host (the contract does not know three). */\nexport function isRenderPools(value: unknown): value is RenderPools {\n const candidate = value as Partial<RenderPools> | null | undefined\n return typeof candidate === 'object' && candidate !== null && typeof candidate.geometries?.acquire === 'function' && typeof candidate.materials?.acquire === 'function'\n}\n","/** The part of a WebGL2 context the pacer reads; the renderer's `getContext()` satisfies it. */\nexport type FramePacerContext = {\n readonly SYNC_GPU_COMMANDS_COMPLETE: number\n readonly TIMEOUT_EXPIRED: number\n fenceSync(condition: number, flags: number): WebGLSync | null\n clientWaitSync(sync: WebGLSync, flags: number, timeout: number): number\n deleteSync(sync: WebGLSync | null): void\n flush(): void\n}\n\nexport type FramePacerOptions = {\n /** Most frames in a row the pacer may hold back before it lets one through (default 3), so a stuck fence cannot freeze the picture. */\n readonly maxSkips?: number\n /** Frames the GPU may be behind before a draw is held back (default 2). The browser reports a fence only some 20 ms after the GPU finished it, so 1 would cap every GPU at one frame per report. */\n readonly framesInFlight?: number\n}\n\nexport type FramePacer = {\n /** True when fewer than `framesInFlight` drawn frames are unfinished: draw this frame. False: the GPU is too far behind, skip the draw and keep simulating. */\n ready(): boolean\n /** Fences the frame just drawn; call it right after the render. */\n submitted(): void\n /** Draws held back so far. */\n readonly skipped: number\n dispose(): void\n}\n\nconst DEFAULT_MAX_SKIPS = 3\nconst DEFAULT_FRAMES_IN_FLIGHT = 2\n\n/**\n * Keeps the GPU a few frames behind the CPU at most. A frame loop that draws as fast as it can (no vsync, as the perf runs do, or a slow GPU) fills the driver's queue and\n * then blocks in the compositor for the length of the whole queue: a run of 5 ms frames and a 150 ms one. Holding back the draw while the last frame is unfinished spends that\n * time on the next simulation step instead, and the longest frame is the GPU's own. A context without fences (WebGL1) is never held back.\n */\nexport function createFramePacer(context: FramePacerContext | null, options: FramePacerOptions = {}): FramePacer {\n const maxSkips = options.maxSkips ?? DEFAULT_MAX_SKIPS\n const framesInFlight = options.framesInFlight ?? DEFAULT_FRAMES_IN_FLIGHT\n const fences: WebGLSync[] = []\n let skippedInRow = 0\n let skipped = 0\n const dropOldest = (): void => {\n const oldest = fences.shift()\n if (oldest && context) context.deleteSync(oldest)\n }\n return {\n ready() {\n if (!context) return true\n while (fences.length > 0 && context.clientWaitSync(fences[0] as WebGLSync, 0, 0) !== context.TIMEOUT_EXPIRED) dropOldest()\n if (fences.length >= framesInFlight && skippedInRow < maxSkips) {\n skippedInRow++\n skipped++\n return false\n }\n skippedInRow = 0\n if (fences.length >= framesInFlight) dropOldest()\n return true\n },\n submitted() {\n if (!context || typeof context.fenceSync !== 'function') return\n const fence = context.fenceSync(context.SYNC_GPU_COMMANDS_COMPLETE, 0)\n if (fence) fences.push(fence)\n context.flush()\n },\n get skipped() {\n return skipped\n },\n dispose() {\n while (fences.length > 0) dropOldest()\n },\n }\n}\n","import type { Bone, BufferAttribute, BufferGeometry, Material, Matrix3, Matrix4, Mesh, Object3D, SkinnedMesh } from 'three'\n\nimport type { ThreeLib } from './three-lib'\n\n/** The layer a batched mesh is moved to: no camera draws it, a raycaster that enables every layer still finds it. */\nexport const BATCHED_LAYER = 30\n\nconst MATRIX_EPSILON = 1e-6\n\nexport type StaticBatcherOptions = {\n /** Which meshes may be batched (default every shown mesh with one material and no instancing, skinning or morphing). */\n readonly include?: (mesh: Mesh) => boolean\n /** Milliseconds of merging per `update` (default all at once, in the call): batches are made a material at a time over the next frames. */\n readonly budgetMs?: number\n /**\n * The meshes move on their own (a body posed bone by bone): each batch is skinned to its meshes, so a mesh carries its part of the batch\n * wherever it goes, and only a new geometry, material, dent or hiding gives it its own draw back.\n */\n readonly followMeshes?: boolean\n}\n\nexport type StaticBatcherStats = { readonly batches: number; readonly batched: number; readonly released: number }\n\nexport type StaticBatcher = {\n /** The batch meshes, a child of the root. */\n readonly group: Object3D\n /**\n * Gives back to its own draw every batched mesh that changed since the batch was made (moved, dented, hidden, given another geometry or\n * material); returns how many it released. Call it once per frame, before the render.\n */\n update(): number\n stats(): StaticBatcherStats\n /** Every mesh draws on its own again and the batches are disposed. */\n dispose(): void\n}\n\ntype Source = {\n readonly mesh: Mesh\n readonly geometry: BufferGeometry\n readonly material: Material\n readonly version: number\n readonly local: Matrix4\n /** The mesh's world matrix when last seen unchanged: equal again, nothing moved and the local check is skipped. */\n readonly world: Matrix4\n readonly mask: number\n readonly batch: Batch\n readonly start: number\n readonly count: number\n released: boolean\n}\n\ntype Batch = { readonly mesh: Mesh; live: number; readonly index: { array: ArrayLike<number> & { [n: number]: number }; needsUpdate: boolean } }\n\nconst ATTRIBUTES = ['position', 'normal', 'uv', 'color'] as const\n\n/** Visible all the way up to `root`; a mesh no longer under it (its model was rebuilt) is not shown. */\nconst isShown = (object: Object3D, root: Object3D): boolean => {\n for (let node: Object3D | null = object; node; node = node.parent) {\n if (node === root) return true\n if (!node.visible) return false\n }\n return false\n}\n\nconst isBatchable = (mesh: Mesh): boolean => {\n const flags = mesh as Mesh & { isInstancedMesh?: boolean; isSkinnedMesh?: boolean }\n if (flags.isInstancedMesh === true || flags.isSkinnedMesh === true || Array.isArray(mesh.material)) return false\n const geometry = mesh.geometry\n return geometry.attributes.position !== undefined && Object.keys(geometry.morphAttributes).length === 0\n}\n\nconst signatureOf = (mesh: Mesh): string => {\n const { attributes } = mesh.geometry\n const present = ATTRIBUTES.filter((name) => attributes[name] !== undefined).map((name) => `${name}${attributes[name]!.itemSize}`)\n return `${(mesh.material as Material).uuid}|${present.join(',')}|${mesh.castShadow ? 1 : 0}${mesh.receiveShadow ? 1 : 0}`\n}\n\nconst sameMatrix = (a: Matrix4, b: Matrix4): boolean => a.elements.every((value, index) => Math.abs(value - (b.elements[index] as number)) < MATRIX_EPSILON)\n\n/**\n * Draws the still, undamaged meshes of an object (a vehicle, a piece of furniture) as one batch per material, and gives each mesh its own draw\n * back the moment it changes. The meshes stay where they are: what hits, dents or cuts them works on them as before, they only leave the\n * camera's layer while batched.\n */\nexport function createStaticBatcher(THREE: ThreeLib, root: Object3D, options: StaticBatcherOptions = {}): StaticBatcher {\n const include = options.include ?? (() => true)\n const follow = options.followMeshes === true\n root.updateMatrixWorld(true)\n const inverse = new THREE.Matrix4().copy(root.matrixWorld).invert()\n const groups = new Map<string, Mesh[]>()\n root.traverse((object) => {\n const mesh = object as Mesh\n if (!mesh.isMesh || !isShown(mesh, root) || !isBatchable(mesh) || !include(mesh)) return\n const key = signatureOf(mesh)\n groups.set(key, [...(groups.get(key) ?? []), mesh])\n })\n const group = new THREE.Group()\n group.name = 'static-batches'\n const sources: Source[] = []\n const pending = [...groups.values()].filter((meshes) => meshes.length >= 2)\n const mergeSome = (deadline: number): void => {\n while (pending.length > 0) {\n const meshes = pending.shift() as Mesh[]\n // a mesh that changed while it waited is left to draw on its own\n if (meshes.every((mesh) => isShown(mesh, root))) sources.push(...mergeGroup(THREE, { meshes, inverse: new THREE.Matrix4().copy(root.matrixWorld).invert(), group, follow }))\n if (performance.now() >= deadline) return\n }\n }\n if (options.budgetMs === undefined) mergeSome(Number.POSITIVE_INFINITY)\n root.add(group)\n const scratch = new THREE.Matrix4()\n let released = 0\n\n const release = (source: Source): void => {\n source.released = true\n source.mesh.layers.mask = source.mask\n const { index } = source.batch\n const first = index.array[source.start] as number\n for (let at = source.start; at < source.start + source.count; at++) index.array[at] = first\n index.needsUpdate = true\n // a batch whose meshes all draw on their own again is an empty draw: it leaves the screen\n if (--source.batch.live === 0) source.batch.mesh.visible = false\n released++\n }\n const hasChanged = (source: Source): boolean => {\n const { mesh } = source\n if (mesh.geometry !== source.geometry || mesh.material !== source.material || !isShown(mesh, root)) return true\n if ((mesh.geometry.attributes.position as BufferAttribute).version !== source.version) return true\n if (follow || sameMatrix(mesh.matrixWorld, source.world)) return false\n if (!sameMatrix(scratch.multiplyMatrices(inverse, mesh.matrixWorld), source.local)) return true\n // the whole object moved, the mesh did not move within it\n source.world.copy(mesh.matrixWorld)\n return false\n }\n\n return {\n group,\n update() {\n if (pending.length > 0) mergeSome(performance.now() + (options.budgetMs ?? Number.POSITIVE_INFINITY))\n const before = released\n inverse.copy(root.matrixWorld).invert()\n for (const source of sources) if (!source.released && hasChanged(source)) release(source)\n return released - before\n },\n stats: () => ({ batches: group.children.length, batched: sources.filter((source) => !source.released).length, released }),\n dispose() {\n for (const source of sources) if (!source.released) source.mesh.layers.mask = source.mask\n group.removeFromParent()\n for (const child of group.children) {\n ;(child as Mesh).geometry.dispose()\n ;(child as SkinnedMesh).skeleton?.dispose()\n }\n },\n }\n}\n\n/** Copies `attribute` into `target` at `offset` vertices: positions through `matrix`, normals through its normal matrix, the rest as they are. */\nfunction copyAttribute(name: string, attribute: BufferAttribute, target: Float32Array, offset: number, matrix: Matrix4, normalMatrix: Matrix3): void {\n const size = attribute.itemSize\n const source = attribute.array as ArrayLike<number>\n const flat = !(attribute as { isInterleavedBufferAttribute?: boolean }).isInterleavedBufferAttribute && source.length === attribute.count * size\n if (name !== 'position' && name !== 'normal') {\n if (flat) target.set(source as Float32Array, offset * size)\n else for (let at = 0; at < attribute.count; at++) for (let k = 0; k < size; k++) target[(offset + at) * size + k] = attribute.getComponent(at, k)\n return\n }\n const e = (name === 'position' ? matrix : normalMatrix).elements\n const isNormal = name === 'normal'\n for (let at = 0; at < attribute.count; at++) {\n const x = attribute.getX(at)\n const y = attribute.getY(at)\n const z = attribute.getZ(at)\n const out = (offset + at) * 3\n if (isNormal) {\n const nx = e[0]! * x + e[3]! * y + e[6]! * z\n const ny = e[1]! * x + e[4]! * y + e[7]! * z\n const nz = e[2]! * x + e[5]! * y + e[8]! * z\n const length = Math.hypot(nx, ny, nz) || 1\n target[out] = nx / length\n target[out + 1] = ny / length\n target[out + 2] = nz / length\n } else {\n target[out] = e[0]! * x + e[4]! * y + e[8]! * z + e[12]!\n target[out + 1] = e[1]! * x + e[5]! * y + e[9]! * z + e[13]!\n target[out + 2] = e[2]! * x + e[6]! * y + e[10]! * z + e[14]!\n }\n }\n}\n\ntype MergeGroupParams = { readonly meshes: readonly Mesh[]; readonly inverse: Matrix4; readonly group: Object3D; readonly follow: boolean }\n\n/** A skinned batch whose bone `n` is mesh `n` itself (no inverse, no bind offset): every vertex lands where its own mesh puts it. */\nfunction skinnedBatch(THREE: ThreeLib, merged: BufferGeometry, material: Material, meshes: readonly Mesh[]): Mesh {\n const vertices = (merged.attributes.position as BufferAttribute).count\n const skinIndex = new Uint16Array(vertices * 4)\n const skinWeight = new Float32Array(vertices * 4)\n let at = 0\n meshes.forEach((mesh, bone) => {\n for (let k = 0; k < (mesh.geometry.attributes.position as BufferAttribute).count; k++, at++) {\n skinIndex[at * 4] = bone\n skinWeight[at * 4] = 1\n }\n })\n merged.setAttribute('skinIndex', new THREE.BufferAttribute(skinIndex, 4))\n merged.setAttribute('skinWeight', new THREE.BufferAttribute(skinWeight, 4))\n const mesh = new THREE.SkinnedMesh(merged, material)\n const identities = meshes.map(() => new THREE.Matrix4())\n mesh.bind(new THREE.Skeleton(meshes as unknown as Bone[], identities), new THREE.Matrix4())\n // the parts move apart as the body is posed: the rest-pose bounds would cull the moved ones\n mesh.frustumCulled = false\n return mesh\n}\n\nfunction mergeGroup(THREE: ThreeLib, { meshes, inverse: rootInverse, group, follow }: MergeGroupParams): Source[] {\n const identity = new THREE.Matrix4()\n const inverse = follow ? identity : rootInverse\n const first = meshes[0] as Mesh\n const names = ATTRIBUTES.filter((name) => first.geometry.attributes[name] !== undefined)\n const pieces = meshes.map((mesh) => {\n const local = new THREE.Matrix4().multiplyMatrices(inverse, mesh.matrixWorld)\n const vertices = (mesh.geometry.attributes.position as BufferAttribute).count\n const indexCount = mesh.geometry.index ? mesh.geometry.index.count : vertices\n return { mesh, local, vertices, indexCount }\n })\n const totalVertices = pieces.reduce((sum, piece) => sum + piece.vertices, 0)\n const totalIndices = pieces.reduce((sum, piece) => sum + piece.indexCount, 0)\n const merged = new THREE.BufferGeometry()\n const normalMatrix = new THREE.Matrix3()\n for (const name of names) {\n const size = first.geometry.attributes[name]!.itemSize\n const array = new Float32Array(totalVertices * size)\n let offset = 0\n for (const piece of pieces) {\n const matrix = follow ? identity : piece.local\n copyAttribute(name, piece.mesh.geometry.attributes[name] as BufferAttribute, array, offset, matrix, normalMatrix.getNormalMatrix(matrix))\n offset += piece.vertices\n }\n merged.setAttribute(name, new THREE.BufferAttribute(array, size))\n }\n const indexArray = totalVertices > 65535 ? new Uint32Array(totalIndices) : new Uint16Array(totalIndices)\n const indexAttribute = new THREE.BufferAttribute(indexArray, 1)\n merged.setIndex(indexAttribute)\n const mesh = follow ? skinnedBatch(THREE, merged, first.material as Material, meshes) : new THREE.Mesh(merged, first.material)\n mesh.name = `batch:${(first.material as Material).name || (first.material as Material).type}`\n mesh.castShadow = first.castShadow\n mesh.receiveShadow = first.receiveShadow\n mesh.raycast = () => undefined\n merged.computeBoundingBox()\n merged.computeBoundingSphere()\n group.add(mesh)\n const batch: Batch = { mesh, live: pieces.length, index: indexAttribute as unknown as Batch['index'] }\n const sources: Source[] = []\n let vertexOffset = 0\n let indexOffset = 0\n for (const piece of pieces) {\n const index = piece.mesh.geometry.index\n if (index) for (let at = 0; at < piece.indexCount; at++) indexArray[indexOffset + at] = index.getX(at) + vertexOffset\n else for (let at = 0; at < piece.indexCount; at++) indexArray[indexOffset + at] = at + vertexOffset\n sources.push({\n mesh: piece.mesh,\n geometry: piece.mesh.geometry,\n material: piece.mesh.material as Material,\n version: (piece.mesh.geometry.attributes.position as BufferAttribute).version,\n local: piece.local,\n world: piece.mesh.matrixWorld.clone(),\n mask: piece.mesh.layers.mask,\n batch,\n start: indexOffset,\n count: piece.indexCount,\n released: false,\n })\n piece.mesh.layers.set(BATCHED_LAYER)\n vertexOffset += piece.vertices\n indexOffset += piece.indexCount\n }\n return sources\n}\n","import { visualStateOf } from '@adatechnology/game-animals'\nimport type { AnimalSpecies } from '@adatechnology/game-animals'\nimport type { ModuleScenario } from '@adatechnology/game-contracts'\n\nimport type { AnimalsThree } from '../animals-three'\nimport type { OutbreakLike } from './types'\n\n/** What a scenario needs of the room: the pass/fail line and time. The test room's `ScenarioContext` has both. */\nexport type AnimalsContext = { expect(condition: boolean, label: string, data?: Record<string, unknown>): void; step(seconds: number): void }\n\nexport type ScenarioDeps = {\n readonly text: Readonly<Record<string, string>>\n species(): readonly AnimalSpecies[]\n ensure(): AnimalsThree\n outbreak(): OutbreakLike | null\n rebuildOutbreak(enabled: boolean): OutbreakLike | null\n skins(): { count: number; meshes: { drawCalls: number; triangles: number }; nodes: number; rich: number }\n clearSkins(): void\n}\n\nconst SKIN_COUNT = 40\n\nexport const createScenarios = (deps: ScenarioDeps): ModuleScenario<AnimalsContext>[] => {\n const scenario = (id: string, run: ModuleScenario<AnimalsContext>['run']): ModuleScenario<AnimalsContext> => ({ id, name: deps.text[id] ?? id, run })\n const rate = () => 0.5\n return [\n scenario('visual_states', ({ expect }) => {\n const animals = deps.ensure()\n for (const species of deps.species()) {\n const rig = animals.build(species)\n animals.setVisualState(rig, 'infected')\n expect(rig.infected && !rig.dead, `${species.id}: infected`)\n animals.setVisualState(rig, 'dead')\n expect(rig.dead && !rig.infected, `${species.id}: dead`)\n animals.setVisualState(rig, 'alive')\n expect(!rig.dead && !rig.infected, `${species.id}: alive again`)\n }\n }),\n scenario('gait_moves_legs', ({ expect }) => {\n const animals = deps.ensure()\n const species = deps.species().find((entry) => entry.body.kind === 'quadruped')\n if (!species) return expect(false, 'no quadruped among the species')\n const rig = animals.build(species)\n animals.animate(rig, { time: 0, delta: 0, clip: 'walk' })\n const before = rig.legs.map((leg) => leg.hip.rotation.x)\n animals.animate(rig, { time: 0.4, delta: 0.4, clip: 'walk' })\n expect(rig.legs.some((leg, index) => leg.hip.rotation.x !== before[index]), `${species.id}: a walk step swings a hip`)\n }),\n scenario('outbreak_rule', ({ expect }) => {\n const turns = deps.species().find((entry) => entry.outcome === 'turns')\n const immune = deps.species().find((entry) => entry.outcome === 'immune')\n const on = deps.rebuildOutbreak(true)\n if (!on || !turns) return expect(false, 'no outbreak rule or no species that turns')\n expect(on.bite(turns.id, rate, 0) !== undefined, `a bite on ${turns.id} starts a fate with the rule on`)\n if (immune) expect(on.bite(immune.id, rate, 0) === undefined, `${immune.id} shrugs the bite off`)\n const off = deps.rebuildOutbreak(false)\n expect(off?.bite(turns.id, rate, 0) === undefined, 'with the rule off a bite does nothing')\n deps.rebuildOutbreak(true)\n }),\n scenario('carcass_lure', ({ expect }) => {\n const outbreak = deps.rebuildOutbreak(true)\n const dies = deps.species().find((entry) => entry.outbreak?.carcass)\n if (!outbreak || !dies) return expect(false, 'no outbreak rule or no species that leaves a carcass')\n const carcass = outbreak.carcass(dies.id, [0, 0, 0], 0)\n expect(carcass !== undefined, `${dies.id} leaves a carcass`)\n if (!carcass) return\n expect(outbreak.lures(carcass, carcass.lureRadiusMetres - 1, 1), 'a zombie inside the radius is drawn')\n expect(!outbreak.lures(carcass, carcass.lureRadiusMetres + 1, 1), 'a zombie outside it is not')\n }),\n scenario('bite_to_turn_timer', ({ expect }) => {\n const outbreak = deps.rebuildOutbreak(true)\n const turns = deps.species().find((entry) => entry.outcome === 'turns')\n if (!outbreak || !turns) return expect(false, 'no outbreak rule or no species that turns')\n const fate = outbreak.bite(turns.id, rate, 0)\n if (!fate) return expect(false, 'the bite took')\n expect(outbreak.stateOf(fate, 0) === 'incubating', 'it incubates at first')\n expect(outbreak.stateOf(fate, fate.dueAt + 1) === 'turned', 'it has turned once the time is up (the bench clock runs it accelerated)')\n expect(visualStateOf(turns.outcome, true) === 'infected', 'and shows the infected look')\n }),\n scenario('skins_40', ({ expect }) => {\n const result = deps.skins()\n expect(result.count === SKIN_COUNT, `${SKIN_COUNT} animals are in the field`, { count: result.count })\n expect(result.meshes.drawCalls <= SKIN_COUNT + 16, 'the skins are about one draw call each', result.meshes)\n expect(result.nodes > 0, 'each skin keeps the nodes the animation moves', { nodes: result.nodes })\n deps.clearSkins()\n }),\n ]\n}\n","export type AnimalsText = {\n group: string\n name: string\n controls: Record<'visual_state' | 'gait' | 'speed' | 'outbreak' | 'time_scale', string>\n hints: Record<'visual_state' | 'gait' | 'outbreak' | 'time_scale', string>\n options: { visual_state: Record<string, string>; gait: Record<string, string> }\n actions: Record<'bite_all' | 'recover' | 'call' | 'skin_40' | 'clear_skins', string>\n layers: Record<'joints' | 'split' | 'sound_range' | 'carcass_lure', string>\n metrics: Record<'animals' | 'draw_calls' | 'triangles' | 'nodes_per_skin' | 'incubating' | 'turned' | 'dead', string>\n scenarios: Record<string, string>\n}\n\nexport const TEXT: Record<string, AnimalsText> = {\n en: {\n group: 'Animals',\n name: 'Animals area',\n controls: { visual_state: 'Visual state', gait: 'Gait', speed: 'Animation speed', outbreak: 'Outbreak rule', time_scale: 'Bite clock' },\n hints: {\n visual_state: 'The look of every animal: alive, infected (grey coat, glowing eyes, wounds) or dead (lying down, a pool of blood)',\n gait: 'idle, walk or run; the cycle of each species runs at its own rate',\n outbreak: 'Off: a bite does nothing. On: a species that turns incubates and turns, one that dies dies, the rest shrug it off',\n time_scale: 'Game seconds per real second for the bite timers (60: ten minutes pass in ten seconds)',\n },\n options: { visual_state: { alive: 'Alive', infected: 'Infected', dead: 'Dead' }, gait: { idle: 'Idle', walk: 'Walk', run: 'Run' } },\n actions: { bite_all: 'Bite every animal', recover: 'Heal every animal', call: 'Make the animals with a call call (hen, lapwing, cow, dog)', skin_40: 'Add 40 animals as node skins', clear_skins: 'Remove the skins' },\n layers: { joints: 'Joints', split: 'Split view', sound_range: 'Sound range', carcass_lure: 'Carcass lure radius' },\n metrics: { animals: 'Animals', draw_calls: 'Draw calls', triangles: 'Triangles', nodes_per_skin: 'Nodes per skin', incubating: 'Incubating', turned: 'Turned', dead: 'Dead' },\n scenarios: {\n visual_states: 'Alive, infected and dead for every species',\n gait_moves_legs: 'The gait moves the legs',\n outbreak_rule: 'Outbreak rule on and off',\n carcass_lure: 'A carcass lures zombies within its radius',\n bite_to_turn_timer: 'Bite, incubation, turn (accelerated)',\n skins_40: '40 animals as node skins',\n },\n },\n 'pt-BR': {\n group: 'Animais',\n name: 'Área de animais',\n controls: { visual_state: 'Estado visual', gait: 'Andar', speed: 'Velocidade da animação', outbreak: 'Regra de surto', time_scale: 'Relógio da mordida' },\n hints: {\n visual_state: 'O aspecto de todos os bichos: vivo, infectado (pelo cinza, olhos brilhando, feridas) ou morto (deitado, poça de sangue)',\n gait: 'idle, walk ou run; o ciclo de cada espécie anda no seu ritmo',\n outbreak: 'Desligada: a mordida não faz nada. Ligada: a espécie que vira incuba e vira, a que morre morre, as outras não sentem',\n time_scale: 'Segundos de jogo por segundo real nos prazos da mordida (60: dez minutos passam em dez segundos)',\n },\n options: { visual_state: { alive: 'Vivo', infected: 'Infectado', dead: 'Morto' }, gait: { idle: 'Parado', walk: 'Andando', run: 'Correndo' } },\n actions: { bite_all: 'Morder todos os bichos', recover: 'Curar todos os bichos', call: 'Fazer os bichos com chamado chamarem (galinha, quero-quero, vaca, cachorro)', skin_40: 'Pôr 40 bichos como pele de nós', clear_skins: 'Tirar as peles' },\n layers: { joints: 'Juntas', split: 'Vista aberta', sound_range: 'Alcance do som', carcass_lure: 'Raio de atração da carcaça' },\n metrics: { animals: 'Bichos', draw_calls: 'Chamadas de desenho', triangles: 'Triângulos', nodes_per_skin: 'Nós por pele', incubating: 'Incubando', turned: 'Viraram', dead: 'Morreram' },\n scenarios: {\n visual_states: 'Vivo, infectado e morto em cada espécie',\n gait_moves_legs: 'O andar mexe as pernas',\n outbreak_rule: 'Regra de surto ligada e desligada',\n carcass_lure: 'A carcaça atrai zumbis dentro do raio',\n bite_to_turn_timer: 'Mordida, incubação, vira (acelerado)',\n skins_40: '40 bichos como pele de nós',\n },\n },\n}\n","import { discardObject3D } from '@adatechnology/game-shapes-three'\nimport { lineUpLength, visualStateOf } from '@adatechnology/game-animals'\nimport type { AnimalSpecies, AnimalVisualState } from '@adatechnology/game-animals'\nimport type { BenchChange, ModuleArea, ModuleControl, ModuleHost, ModuleLayer, ModuleMetric, ModuleScenario, TestableItem, TestableModule } from '@adatechnology/game-contracts'\nimport type { NodeSkin } from '@adatechnology/game-props3d'\nimport { createStaticBatcher } from '@adatechnology/game-render-three'\nimport type { StaticBatcher } from '@adatechnology/game-render-three'\nimport type * as THREE_NS from 'three'\n\nimport { createAnimalSound } from '../audio/sound'\nimport type { AnimalSound } from '../audio/sound'\nimport { createAnimalsThree } from '../animals-three'\nimport type { AnimalsThree } from '../animals-three'\nimport type { AnimalRig, Three } from '../types'\nimport { createScenarios } from './scenarios'\nimport type { AnimalsContext, ScenarioDeps } from './scenarios'\nimport { TEXT } from './text'\nimport type { AnimalsTestOptions, OutbreakLike } from './types'\n\nexport const ANIMALS_AREA_ID = 'animals_area'\nexport const ANIMAL_ITEM_PREFIX = 'animal_'\nconst SKIN_COUNT = 40\nconst SKIN_FIELD = { x0: -18, z0: -10, columns: 8, gap: 4.5 }\nconst DEFAULT_TIME_SCALE = 60\n\ntype Fate = NonNullable<ReturnType<OutbreakLike['bite']>>\ntype Placed = { readonly id: string; readonly rig: AnimalRig; fate?: Fate; counted?: 'turned' | 'dead'; batcher?: StaticBatcher }\n/** Milliseconds a frame the batches of a new animal may take to be made. */\nconst BATCH_BUDGET_MS = 1\n\n/**\n * The animals test area as a `TestableModule`: the species of the pack in a row, each drawn by the Bichos do Vale builders, with the alive / infected / dead looks, the gait,\n * the joints and split views, the rings for how far its sound carries and how far a carcass lures, the outbreak rule with an accelerated bite clock, and a field of 40 animals\n * drawn as node skins. The species and the outbreak rule come in by option, so this package knows no game.\n */\nexport function createAnimalsTestModule(options: AnimalsTestOptions = {}): TestableModule<AnimalsContext> & { readonly placed: ReadonlyMap<string, unknown> } {\n const locale = options.locale ?? 'pt-BR'\n const text = TEXT[locale] ?? (TEXT['pt-BR'] as (typeof TEXT)[string])\n const species = options.species ?? []\n const settings = { visual: 'alive' as AnimalVisualState, gait: 'walk', speed: 1, outbreak: true, timeScale: DEFAULT_TIME_SCALE }\n let three: Three | null = null\n let animals: AnimalsThree | null = null\n let host: ModuleHost | null = null\n let outbreak: OutbreakLike | null = null\n let sound: AnimalSound | null = null\n let time = 0\n let clock = 0\n let skinGroup: THREE_NS.Group | null = null\n let skins: { rig: AnimalRig; skin: NodeSkin }[] = []\n let rings: THREE_NS.Object3D[] = []\n const placed = new Map<string, Placed>()\n const random = (): number => 0.5\n const drawSeed = options.seed ?? 7\n const bitten = () => ({ turned: [...placed.values()].filter((p) => p.counted === 'turned').length, dead: [...placed.values()].filter((p) => p.counted === 'dead').length })\n\n const ensure = (THREE: Three): AnimalsThree => {\n three = THREE\n animals ??= createAnimalsThree(THREE, { seed: drawSeed, ...(options.document ? { document: options.document } : {}) })\n return animals\n }\n const rebuildOutbreak = (): void => {\n outbreak = options.createOutbreak ? options.createOutbreak({ species, enabled: settings.outbreak, timeScale: settings.timeScale }) : null\n }\n rebuildOutbreak()\n\n /** Draws the animal's parts one batch per material, posed with it; a new look (other materials) makes the batches again. */\n const batch = (entry: Placed): void => {\n entry.batcher?.dispose()\n if (three) entry.batcher = createStaticBatcher(three, entry.rig.root, { followMeshes: true, budgetMs: BATCH_BUDGET_MS })\n }\n\n const look = (entry: Placed, state: AnimalVisualState): void => {\n animals?.setVisualState(entry.rig, state)\n batch(entry)\n }\n\n const item = (animal: AnimalSpecies): TestableItem => ({\n id: `${ANIMAL_ITEM_PREFIX}${animal.id}`,\n name: animal.name[locale as 'en'] ?? animal.id,\n movable: false,\n build: (THREE: unknown) => {\n const rig = ensure(THREE as Three).build(animal)\n // the room puts the tree in its scene right after this; trees that never reach it are dropped at the next frame\n const entry: Placed = { id: rig.root.uuid, rig }\n placed.set(rig.root.uuid, entry)\n if (settings.visual !== 'alive') animals?.setVisualState(rig, settings.visual)\n batch(entry)\n return rig.root\n },\n })\n\n const statusOf = (entry: Placed): void => {\n if (!entry.fate || !outbreak) return\n const state = outbreak.stateOf(entry.fate, clock)\n if (state === 'incubating') return\n entry.counted = state === 'turned' ? 'turned' : 'dead'\n look(entry, visualStateOf(entry.rig.species.outcome, true, state === 'dead'))\n }\n\n const clearRings = (): void => {\n discardObject3D(rings)\n rings = []\n }\n const refreshLayers = (): void => {\n if (!host?.bench || !animals || !three) return\n clearRings()\n for (const entry of placed.values()) {\n animals.setJointView(entry.rig, host.bench.layer('joints'))\n animals.setSplit(entry.rig, host.bench.layer('split'))\n }\n const ring = (radius: number, color: number, parent: THREE_NS.Object3D): void => {\n const mesh = new (three as Three).Mesh(new (three as Three).RingGeometry(radius - 0.15, radius, 96).rotateX(-Math.PI / 2), new (three as Three).MeshBasicMaterial({ color, transparent: true, opacity: 0.55, depthWrite: false }))\n mesh.position.y = 0.03\n parent.add(mesh)\n rings.push(mesh)\n }\n for (const entry of placed.values()) {\n if (!entry.rig.root.parent) continue\n const radius = entry.rig.species.soundRangeMetres\n if (host.bench.layer('sound_range') && radius) ring(radius, 0x44aaff, entry.rig.root)\n const lure = entry.rig.species.outbreak?.carcass?.lureRadiusMetres\n if (host.bench.layer('carcass_lure') && lure) ring(lure, 0xff5533, entry.rig.root)\n }\n }\n\n const addSkins = (): void => {\n if (!animals || !three || !host) return\n skinGroup ??= new three.Group()\n if (!skinGroup.parent) (host.scene as THREE_NS.Object3D).add(skinGroup)\n for (let index = skins.length; index < SKIN_COUNT; index++) {\n const animal = species[index % Math.max(1, species.length)] as AnimalSpecies\n const rig = animals.build(animal)\n const skin = animals.createSkin(rig)\n rig.root.position.set(SKIN_FIELD.x0 + (index % SKIN_FIELD.columns) * SKIN_FIELD.gap, 0, SKIN_FIELD.z0 - Math.floor(index / SKIN_FIELD.columns) * SKIN_FIELD.gap)\n // the tree still drives the animation, but only the skin is drawn\n rig.root.traverse((object) => {\n if ((object as THREE_NS.Mesh).isMesh) object.visible = false\n })\n skinGroup.add(rig.root, skin.mesh)\n skins.push({ rig, skin })\n }\n }\n const clearSkins = (): void => {\n if (skinGroup) discardObject3D(skinGroup)\n skinGroup = null\n skins = []\n }\n\n const applyVisual = (): void => {\n for (const entry of placed.values()) {\n if (entry.fate) continue\n look(entry, settings.visual)\n }\n }\n\n const onChange = (change: BenchChange): void => {\n if (change.kind === 'layer') return refreshLayers()\n if (change.kind === 'action') {\n if (change.id === 'bite_all') {\n for (const entry of placed.values()) {\n entry.fate = outbreak?.bite(entry.rig.species.id, random, clock)\n if (entry.fate) look(entry, visualStateOf(entry.rig.species.outcome, true))\n }\n } else if (change.id === 'recover') {\n for (const entry of placed.values()) {\n delete entry.fate\n delete entry.counted\n look(entry, settings.visual)\n }\n } else if (change.id === 'call' && sound) {\n for (const [id, entry] of placed) {\n const at = entry.rig.root.getWorldPosition(new (three as Three).Vector3())\n sound.call(entry.rig.species.id, [at.x, at.y + 0.5, at.z], { key: id })\n }\n } else if (change.id === 'skin_40') addSkins()\n else if (change.id === 'clear_skins') clearSkins()\n return\n }\n switch (change.id) {\n case 'visual_state': settings.visual = String(change.value) as AnimalVisualState; applyVisual(); break\n case 'gait': settings.gait = String(change.value); break\n case 'speed': settings.speed = Number(change.value); break\n case 'outbreak': settings.outbreak = Boolean(change.value); if (outbreak) outbreak.enabled = settings.outbreak; break\n case 'time_scale': settings.timeScale = Number(change.value); rebuildOutbreak(); break\n default: break\n }\n }\n\n const frame = (dt: number): void => {\n time += dt * settings.speed\n clock += dt\n sound?.update(dt)\n for (const [id, entry] of placed) {\n if (!entry.rig.root.parent) {\n entry.batcher?.dispose()\n placed.delete(id)\n continue\n }\n statusOf(entry)\n animals?.animate(entry.rig, { time, delta: dt * settings.speed, clip: settings.gait, split: host?.bench?.layer('split') ?? false })\n animals?.updateSplit(entry.rig, dt)\n entry.batcher?.update()\n }\n for (const { rig, skin } of skins) {\n animals?.animate(rig, { time, delta: dt * settings.speed, clip: settings.gait })\n rig.root.updateMatrixWorld(true)\n skin.update()\n }\n }\n\n const countMeshes = (only?: 'skins'): { drawCalls: number; triangles: number } => {\n let drawCalls = 0\n let triangles = 0\n const count = (root: THREE_NS.Object3D): void =>\n root.traverse((object) => {\n const mesh = object as THREE_NS.Mesh\n if (!mesh.isMesh || !mesh.visible) return\n drawCalls += 1\n const geometry = mesh.geometry\n triangles += geometry.index ? geometry.index.count / 3 : geometry.attributes.position.count / 3\n })\n if (only !== 'skins') for (const entry of placed.values()) count(entry.rig.root)\n for (const { skin } of skins) count(skin.mesh)\n return { drawCalls, triangles: Math.round(triangles) }\n }\n\n const deps: ScenarioDeps = {\n text: text.scenarios,\n species: () => species,\n ensure: () => ensure((three ?? host?.THREE) as Three),\n outbreak: () => outbreak,\n rebuildOutbreak: (enabled) => {\n settings.outbreak = enabled\n rebuildOutbreak()\n return outbreak\n },\n skins: () => {\n ensure((three ?? host?.THREE) as Three)\n addSkins()\n return { count: skins.length, meshes: countMeshes('skins'), nodes: skins[0]?.skin.nodes.length ?? 0, rich: skins[0]?.rig.triangles ?? 0 }\n },\n clearSkins,\n }\n\n return {\n id: ANIMALS_AREA_ID,\n name: text.name,\n ground: { surface: 'lawn' },\n group: text.group,\n get placed() {\n return placed\n },\n items: () => species.map(item),\n area: (): ModuleArea => {\n // the page's line-up: each animal given room by its length (and a gap), the row centred on the origin\n const gap = 0.85\n let cursor = 0\n const spots = species.map((animal) => {\n const length = lineUpLength(animal.body)\n const x = cursor + length / 2\n cursor += length + gap\n return { animal, x }\n })\n const offset = -(cursor - gap) / 2\n return {\n placements: spots.map(({ animal, x }) => ({ item: `${ANIMAL_ITEM_PREFIX}${animal.id}`, position: [x + offset, 0, 0], rotationY: animal.body.kind === 'snake' ? 2.2 : 1.1 })),\n view: { eye: [0, 4, Math.max(10, (cursor - gap) * 0.55)], target: [0, 0.6, 0] },\n }\n },\n controls: (): ModuleControl[] => [\n { kind: 'choice', id: 'visual_state', name: text.controls.visual_state, hint: text.hints.visual_state, options: (['alive', 'infected', 'dead'] as const).map((id) => ({ id, name: text.options.visual_state[id] as string })), default: 'alive' },\n { kind: 'choice', id: 'gait', name: text.controls.gait, hint: text.hints.gait, options: (['idle', 'walk', 'run'] as const).map((id) => ({ id, name: text.options.gait[id] as string })), default: 'walk' },\n { kind: 'number', id: 'speed', name: text.controls.speed, min: 0, max: 4, step: 0.25, default: 1 },\n { kind: 'boolean', id: 'outbreak', name: text.controls.outbreak, hint: text.hints.outbreak, default: true },\n { kind: 'number', id: 'time_scale', name: text.controls.time_scale, hint: text.hints.time_scale, min: 1, max: 600, step: 1, default: DEFAULT_TIME_SCALE },\n { kind: 'action', id: 'bite_all', name: text.actions.bite_all },\n { kind: 'action', id: 'recover', name: text.actions.recover },\n { kind: 'action', id: 'call', name: text.actions.call },\n { kind: 'action', id: 'skin_40', name: text.actions.skin_40, slow: true },\n { kind: 'action', id: 'clear_skins', name: text.actions.clear_skins },\n ],\n layers: (): ModuleLayer[] => (['joints', 'split', 'sound_range', 'carcass_lure'] as const).map((id) => ({ id, name: text.layers[id], default: false })),\n metrics: (): ModuleMetric[] => [\n { id: 'animals', name: text.metrics.animals, read: () => placed.size + skins.length },\n { id: 'draw_calls', name: text.metrics.draw_calls, read: () => countMeshes().drawCalls },\n { id: 'triangles', name: text.metrics.triangles, read: () => countMeshes().triangles },\n { id: 'nodes_per_skin', name: text.metrics.nodes_per_skin, read: () => skins[0]?.skin.nodes.length ?? 0 },\n { id: 'incubating', name: text.metrics.incubating, read: () => [...placed.values()].filter((p) => p.fate && !p.counted).length },\n { id: 'turned', name: text.metrics.turned, read: () => bitten().turned },\n { id: 'dead', name: text.metrics.dead, read: () => bitten().dead },\n ],\n scenarios: (): ModuleScenario<AnimalsContext>[] => createScenarios(deps),\n attach(lent) {\n host = lent\n ensure(lent.THREE as Three)\n sound = lent.audio && options.sound !== false ? createAnimalSound(lent.audio, options.sound ?? {}) : null\n const stopChange = lent.bench?.onChange(onChange)\n const stopFrame = lent.onFrame(frame)\n return () => {\n stopChange?.()\n stopFrame()\n clearRings()\n clearSkins()\n sound?.dispose()\n sound = null\n host = null\n }\n },\n }\n\n}\n"],"mappings":"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","names":["object","landing","landing","mesh","color","width","height","depth","indexArray","sign","list","num","equals","area","ring","bs","layeroffset","built","make","object","list","list","object","document","object","mesh","color","material","geometry","normalMatrix","PI","PI","list","sphere","object","mesh","object","mesh","normalMatrix","indexArray","SKIN_COUNT","ring","color","mesh","object"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.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/falloff.ts","../../game-audio/src/data/game-sfx.json","../../game-audio/src/recipes/noise-class.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/data/mix-profiles.json","../../game-audio/src/mix.ts","../../game-audio/src/recipe.ts","../../game-audio/src/data/voice-groups.json","../../game-audio/src/voices.ts","../../game-audio/src/bank.ts","../../game-audio/src/listener.ts","../../game-audio/src/rng.ts","../../game-audio/src/plan.ts","../../game-audio/src/webaudio/noise.ts","../../game-audio/src/webaudio/chain.ts","../../game-audio/src/webaudio/context.ts","../../game-audio/src/loops.ts","../../game-audio/src/webaudio/nodes.ts","../../game-audio/src/webaudio/realize-loop.ts","../../game-audio/src/webaudio/engine-loop.ts","../../game-audio/src/sample.ts","../../game-audio/src/webaudio/sample-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/variation.ts","../../game-audio/src/webaudio/sample-store.ts","../../game-audio/src/webaudio/realize-sample.ts","../../game-audio/src/webaudio/engine.ts","../../game-audio/src/bench/locale.ts","../../game-audio/src/bench/module.ts","../../game-audio/src/catalog.ts","../../game-audio/src/measure/calibration.ts","../../game-audio/src/measure/analyze.ts","../../game-audio/src/measure/criteria.ts","../../game-audio/src/bench/sound-check-locale.ts","../../game-audio/src/bench/sound-check.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/calls.json","../src/audio/sound.ts","../src/context.ts","../src/extras.ts","../../game-animals/src/data/gaits.json","../../game-animals/src/gaits.ts","../../game-animals/src/layout.ts","../../game-animals/src/registry.ts","../../game-animals/src/kit.ts","../src/line-up.ts","../../game-shapes/src/color.ts","../../game-shapes/src/hash.ts","../../game-shapes/src/math.ts","../../game-shapes/src/primitives/three-generators.ts","../../game-shapes/src/primitives/difference.ts","../../game-shapes/src/primitives/earcut.ts","../../game-shapes/src/primitives/extrude.ts","../../game-shapes/src/primitives/index.ts","../../game-shapes/src/random.ts","../../game-shapes/src/registry.ts","../../game-shapes/schema/shape.schema.json","../../game-shapes/src/schema.ts","../../game-shapes/src/generate.ts","../../game-shapes/src/merge.ts","../../game-shapes/src/worker.ts","../../game-shapes-three/src/geometry.ts","../../game-shapes-three/src/namespace.ts","../../game-shapes-three/src/object.ts","../../game-shapes-three/src/merge-by-material.ts","../../game-shapes-three/src/cord.ts","../../game-shapes-three/src/release.ts","../src/profile.ts","../src/textures.ts","../src/visual-state.ts","../src/views.ts","../../game-props3d/src/colors.ts","../../game-props3d/src/atlas.ts","../../game-props3d/src/catalog.ts","../../game-props3d/src/builders/ceiling.ts","../../game-props3d/src/builders/floors.ts","../../game-props3d/src/builders/shared.ts","../../game-props3d/src/builders/furniture.ts","../../game-props3d/src/builders/house-state.ts","../../game-props3d/src/builders/plants.ts","../../game-props3d/src/builders/wall.ts","../../game-props3d/src/builders/yard.ts","../../game-props3d/src/cover.ts","../../game-props3d/src/kit.ts","../../game-props3d/src/locale.ts","../../game-props3d/src/create.ts","../../game-props3d/src/node-skin.ts","../../game-props3d/src/recorder.ts","../../game-props3d/src/merge-by-material.ts","../../game-props3d/src/shape-recorder.ts","../src/animate-bird.ts","../src/animate-quadruped.ts","../src/animate-snake.ts","../src/bird.ts","../src/quadruped.ts","../src/snake.ts","../src/animals-three.ts","../../game-render-three/src/lights/slots.ts","../../game-render-three/src/lights/arbiter.ts","../../game-render-three/src/fog.ts","../../game-render-three/src/lights/scene-state.ts","../../game-render-three/src/lights/budget.ts","../../game-render-three/src/lights/types.ts","../../game-render-three/src/lights/guard.ts","../../game-render-three/src/program-key.ts","../../game-render-three/src/pools/anchors.ts","../../game-render-three/src/probes.ts","../../game-render-three/src/warmup.ts","../../game-render-three/src/pools/pooled.ts","../../game-render-three/src/pools/geometry.ts","../../game-render-three/src/pools/material.ts","../../game-render-three/src/pools/render-pools.ts","../../game-render-three/src/frame-pacer.ts","../../game-render-three/src/static-batcher.ts","../src/test-area/scenarios.ts","../src/test-area/text.ts","../src/test-area/module.ts"],"sourcesContent":["/**\n * @adatechnology/game-animals-three: the three.js drawing of the animals, a literal port of the Bichos do Vale builders (`quad`, `bird`, `snake`) and\n * their animation, with the alive / infected / dead looks for every species, the joints and split views, and a GPU node skin for crowds.\n */\n\nexport { ANIMAL_SOUND_BANK, ANIMAL_SOUNDS, COW_MOO, DOG_BARK, HEN_CLUCK, LAPWING_CALL } from './audio/recipes'\nexport { ANIMAL_CALLS, ANIMAL_CALL_SOURCES, createAnimalSound } from './audio/sound'\nexport type { AnimalCall, AnimalSound, AnimalSoundOptions, AnimalSoundSink } from './audio/sound'\nexport type { AnimalLeg, AnimalPivot, AnimalRig, AnimalWing, AnimalsThreeOptions, CanvasLike, DocumentLike, Three } from './types'\nexport type { AnimateParams } from './animate-types'\nexport type { BirdExtra, BirdParts, ExtraContext, QuadrupedExtra, QuadrupedParts } from './extras'\nexport { BIRD_EXTRAS, QUADRUPED_EXTRAS } from './extras'\nexport { lineUpAnimals } from './line-up'\nexport type { LineUpOptions } from './line-up'\nexport { profileGeometry } from './profile'\nexport { createCoatTexture } from './textures'\nexport { applyVisualState } from './visual-state'\nexport { setJointView, setSplit, updateSplit } from './views'\n\nexport type { AnimalsThree } from './animals-three'\nexport { createAnimalsThree } from './animals-three'\n/** @deprecated import from '@adatechnology/game-animals-three/testing' */\nexport { createAnimalsTestModule, ANIMALS_AREA_ID, ANIMAL_ITEM_PREFIX } from './testing'\n/** @deprecated import from '@adatechnology/game-animals-three/testing' */\nexport type { AnimalsContext } from './testing'\n/** @deprecated import from '@adatechnology/game-animals-three/testing' */\nexport type { AnimalsTestOptions, OutbreakLike } from './testing'\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","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","{\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","{\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","[\n {\n \"id\": \"game\",\n \"master\": 0.5,\n \"noise\": {\n \"seconds\": 1.5,\n \"create\": \"lazy\"\n },\n \"sources\": [\n \"jogo:L62077-62081 master 0.5 to the output\",\n \"jogo:L62082-62088 noise 1.5 s made by the first sound that needs it\"\n ]\n },\n {\n \"id\": \"art_scene\",\n \"master\": 0.85,\n \"compressor\": {\n \"threshold\": -12,\n \"ratio\": 5\n },\n \"reverb\": {\n \"seconds\": 2.2,\n \"decay\": 3.4,\n \"wet\": 0.4\n },\n \"noise\": {\n \"seconds\": 2,\n \"create\": \"eager\"\n },\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 \"id\": \"kit\",\n \"master\": 0.85,\n \"compressor\": {\n \"threshold\": -12,\n \"ratio\": 5\n },\n \"reverb\": {\n \"seconds\": 2.2,\n \"decay\": 3.4,\n \"wet\": 0.4\n },\n \"noise\": {\n \"seconds\": 2,\n \"create\": \"eager\"\n },\n \"sources\": [\n \"sdk: the art scene chain (cena L1308-L1312) with a limiter after the compressor: the page has none, and a blast with its debris and a fire catching reached +7 dBFS\"\n ],\n \"limiter\": {\n \"ceiling\": -1,\n \"knee\": -6\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) → (limiter) → 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 /**\n * A last stage after the compressor that keeps every peak under `ceiling` dBFS: below `knee` dBFS the sound passes untouched, above it the peak\n * bends smoothly toward the ceiling (a soft clipper, no lookahead and no delay), so a blast with its debris and a fire catching all at once never\n * clips the output. `range` is the loudest input it shapes, as a factor of full scale (default 4, +12 dB); louder is held at the ceiling.\n */\n readonly limiter?: { readonly ceiling: number; readonly knee: number; readonly range?: 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) with a limiter at the end (`kit`); `mix: 'art_scene'` is the page as it was, `mix: 'game'` the game's. */\nexport const DEFAULT_MIX = 'kit'\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","/**\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 /**\n * Distance falloff of the loop, applied each time `set` gives a `position` (an engine, a fire, a skid heard from where it is). Left out, the loop is\n * heard at the listener, as the team loops are. `cutoffK` also lowers the first filter's cutoff with the distance, as it does for one-shots.\n */\n readonly falloff?: Falloff\n /** Stereo pan from the listener, from the `position` of each `set` (a panner after the gain). Off by default. */\n readonly pan?: boolean\n readonly sources: readonly string[]\n}\n\n/**\n * One id that plays one of several recipes (the takes of a footstep, of a punch): `play(id)` draws a variant, never the one it played last time (unless\n * `repeat`), and plays it at a rate drawn from `rate` (1.04 is 4% higher and 4% shorter; see `retunePlan`), so ten footsteps never sound like one.\n */\nexport type SoundSet = {\n readonly id: string\n /** Recipe ids, at least one. */\n readonly variants: readonly string[]\n /** `[min, max]` of the playback rate (pitch and speed together). Default `[1, 1]`. */\n readonly rate?: readonly [min: number, max: number]\n /** Allow the same variant twice in a row (default false). */\n readonly repeat?: boolean\n}\n\nexport type SoundBank = {\n readonly id: string\n readonly sounds?: readonly SoundRecipe[]\n readonly loops?: readonly LoopRecipe[]\n readonly sets?: readonly SoundSet[]\n /** Sounds played from recorded files (see `sample.ts`). A sample and a recipe may share an id: the sample plays once its files are loaded. */\n readonly samples?: readonly SampleSound[]\n}\n\n// ----- sounds from recorded files (planned by `planSample` in sample.ts; the types live here so the bank has them without an import cycle)\n\n/** One take: a file and its own trim (linear gain, default 1). */\nexport type SampleRef = { readonly url: string; readonly gain?: number }\n\n/** Where a sound happens: a closed room rings short and dense, the open field long and thin. */\nexport type SampleEnvironment = 'indoor' | 'outdoor'\n\n/** Where the near take hands over to the far one: all near up to `from` metres, all far from `to`, an equal-power crossfade between. */\nexport type SampleCrossover = { readonly from: number; readonly to: number }\n\nexport type SampleTail = {\n readonly indoor?: readonly SampleRef[]\n readonly outdoor?: readonly SampleRef[]\n /** Level of the tail against the dry take (default 0.6). */\n readonly gain?: number\n /** Seconds after the start (default 0: the tail file carries its own pre-delay). */\n readonly at?: number\n}\n\n/**\n * A set of takes for a strength of the action (criterion C7: a light blow, a heavy one, a car at 10 or 60 km/h): from `from` (0..1) up, these takes\n * replace `close` and `far`. The tier played is the last one whose `from` is at or below `PlayOptions.intensity`.\n */\nexport type SampleTier = {\n readonly name: string\n readonly from: number\n readonly close: readonly SampleRef[]\n readonly far?: readonly SampleRef[]\n}\n\nexport type SampleSound = {\n readonly id: string\n readonly bus: BusId\n /** The takes heard near; at least one (the criteria ask four for a sound heard all the time). */\n readonly close: readonly SampleRef[]\n /** The takes heard far; without them the near take plays at every distance (muffled by the lowpass). */\n readonly far?: readonly SampleRef[]\n /** Takes by strength (light, medium, heavy), in rising `from`; without `intensity` the near takes above play. */\n readonly tiers?: readonly SampleTier[]\n /** Default `{ from: 30, to: 80 }`. */\n readonly crossover?: SampleCrossover\n readonly tail?: SampleTail\n /** ± fraction of the playback rate drawn per play (default 0.04: ±4 %, inside the 3–5 % of the criteria). 0 plays every take at its pitch. */\n readonly pitch?: number\n /** Level of the sound (default 1). */\n readonly gain?: number\n /** Default `{ model: 'inverse_square', k: 0.002, minGain: 0.002 }`: a recorded take already sounds near or far, so the curve is gentler than a recipe's. */\n readonly falloff?: Falloff\n /** The lowpass cutoff is `20000 / (1 + d × cutoffK)` Hz (default 0.01: 10 kHz at 100 m). 0 turns the distance lowpass off. */\n readonly cutoffK?: number\n /** Stereo pan from the listener (default true). */\n readonly pan?: boolean\n readonly group?: string\n readonly priority?: number\n /**\n * At most `voices` plays of this sound ring at once: a new one fades the oldest out in `fade` seconds (default 0.03) at its own start. Rapid fire\n * then stays shot by shot instead of a wall of overlapping takes (criterion C9). Without it every play rings to its end.\n */\n readonly choke?: { readonly voices: number; readonly fade?: number }\n /**\n * A continuous sound (fire, rain, an engine idling) that `engine.loop(id)` plays: the near takes chained without a seam, each crossed into the next\n * in `crossfade` seconds (default 0.25, equal power), never the same take twice in a row. While the files load, the recipe loop of `fallback`\n * (or of the same id) plays. `play(id)` still plays one take as a one-shot.\n */\n readonly loop?: boolean | { readonly crossfade?: number }\n /** A recipe id the engine plays while the files are not loaded yet (or failed): the synthesized sound stays as the fallback. */\n readonly fallback?: string\n /** Where each file came from: `\"freesound:123456 autor CC0\"`, or `\"sdk\"` for files the kit made. */\n readonly sources: readonly string[]\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 \"fire_ignite\": {\n \"maxConcurrent\": 2,\n \"cooldown\": 0.15,\n \"sources\": [\n \"sdk: many parts catching at once (a blast sets a shelf alight) are one or two ignitions, not forty\"\n ]\n },\n \"fire_crackle\": {\n \"maxConcurrent\": 4,\n \"sources\": [\n \"sdk: the crackles of every fire share four voices\"\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 { LoopRecipe, SoundBank, SoundRecipe, SoundSet } from './recipe'\nimport type { SampleSound } from './sample'\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> | DeepPartial<SampleSound>): void\n remove(id: string): void\n sound(id: string): SoundRecipe | undefined\n loop(id: string): LoopRecipe | undefined\n /** The set an id names (`SoundBank.sets`), if any; a recipe with the same id wins in `play`. */\n set(id: string): SoundSet | undefined\n sample(id: string): SampleSound | undefined\n soundIds(): string[]\n setIds(): string[]\n loopIds(): string[]\n sampleIds(): 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 sets = new Map<string, SoundSet>()\n const samples = new Map<string, SampleSound>()\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 for (const set of bank.sets ?? []) {\n if (set.variants.length === 0) throw new Error(`sound set ${set.id} has no variant`)\n sets.set(set.id, set)\n owners.set(set.id, bank.id)\n }\n for (const sample of bank.samples ?? []) {\n samples.set(sample.id, sample)\n owners.set(sample.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 const sample = samples.get(id)\n if (sample) samples.set(id, mergeRecords(sample, patch as Record<string, unknown>) as SampleSound)\n },\n remove(id) {\n sounds.delete(id)\n loops.delete(id)\n sets.delete(id)\n samples.delete(id)\n owners.delete(id)\n },\n sound: (id) => sounds.get(id),\n loop: (id) => loops.get(id),\n set: (id) => sets.get(id),\n sample: (id) => samples.get(id),\n soundIds: () => [...sounds.keys()],\n setIds: () => [...sets.keys()],\n loopIds: () => [...loops.keys()],\n sampleIds: () => [...samples.keys()],\n bankOf: (id) => owners.get(id),\n }\n for (const bank of banks) registry.register(bank)\n return registry\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","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","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","/** `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\nconst LIMITER_POINTS = 4097\n\n/**\n * The limiter's curve over inputs `-range..range` (full scale 1): untouched up to the knee, then `knee + (ceiling − knee) × tanh((|x| − knee) / (ceiling − knee))`,\n * which leaves the knee with the same slope and never passes the ceiling.\n */\nexport function limiterCurve(ceilingDb: number, kneeDb: number, range: number, points = LIMITER_POINTS): Float32Array<ArrayBuffer> {\n const ceiling = 10 ** (ceilingDb / 20)\n const knee = Math.min(10 ** (kneeDb / 20), ceiling)\n const curve = new Float32Array(new ArrayBuffer(points * 4))\n for (let i = 0; i < points; i++) {\n const x = ((i / (points - 1)) * 2 - 1) * range\n const a = Math.abs(x)\n const y = a <= knee || ceiling === knee ? Math.min(a, ceiling) : knee + (ceiling - knee) * Math.tanh((a - knee) / (ceiling - knee))\n curve[i] = Math.sign(x) * y\n }\n return curve\n}\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 // the limiter is the last stage: a gain that brings `range` × full scale into the shaper's -1..1, and a curve that gives the level back, bent above the knee\n let limiter: { readonly input: GainNode; readonly shaper: WaveShaperNode } | undefined\n if (profile.limiter) {\n const range = profile.limiter.range ?? 4\n const input = context.createGain()\n input.gain.value = 1 / range\n const shaper = context.createWaveShaper()\n shaper.curve = limiterCurve(profile.limiter.ceiling, profile.limiter.knee, range)\n input.connect(shaper)\n shaper.connect(context.destination)\n limiter = { input, shaper }\n }\n const output: AudioNode = limiter?.input ?? context.destination\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(output)\n }\n const master = context.createGain()\n master.gain.value = profile.master\n master.connect(compressor ?? output)\n if (!profile.reverb) {\n return {\n master,\n disconnect: () => {\n master.disconnect()\n compressor?.disconnect()\n limiter?.input.disconnect()\n limiter?.shaper.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 limiter?.input.disconnect()\n limiter?.shaper.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 // an OfflineAudioContext (no `close`) is started by `startRendering`, never resumed\n if (context.state === 'suspended' && typeof context.close === 'function') void context.resume()\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","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 /** The node wired to the bus: the panner when the recipe pans, else the gain. */\n readonly out: AudioNode\n readonly panner?: StereoPannerNode\n readonly source: AudioScheduledSourceNode\n readonly frequency?: AudioParam\n /** The first filter's cutoff, for `set({ cutoff })`. */\n readonly cutoff?: AudioParam\n stop(): void\n}\n\n// a panning loop goes gain → panner → bus; the others keep the team wiring (gain → bus)\nfunction outputStage(recipe: LoopRecipe, target: RealizeTarget, gain: GainNode): { out: AudioNode; panner?: StereoPannerNode } {\n if (!recipe.pan) {\n gain.connect(target.output)\n return { out: gain }\n }\n const panner = target.context.createStereoPanner()\n gain.connect(panner)\n panner.connect(target.output)\n return { out: panner, panner }\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 const output = outputStage(recipe, target, gain)\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, ...output, source, ...(frequency ? { frequency } : {}), ...(filters[0] ? { cutoff: filters[0].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 const output = outputStage(recipe, target, gain)\n source.start()\n for (const lfo of lfos) lfo.start()\n return { gain, ...output, source, ...(frequency ? { frequency } : {}), ...(filters[0] ? { cutoff: filters[0].frequency } : {}), stop: stopAll([source, ...lfos]) }\n}\n","import type { Vec3 } from '@adatechnology/game-contracts'\n\nimport { cutoffDivisor, falloffGain } from '../falloff'\nimport { listenerDistance, panFor } from '../listener'\nimport type { DistanceMode, Listener } from '../listener'\nimport { 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/**\n * What one `set` changes. `volume` and `frequency` glide as in the team loops. `position` places the loop: with a recipe `falloff` the level falls\n * with the distance from the listener (and the cutoff with `cutoffK`), with `pan` it goes to the listener's side. `cutoff` glides the first filter\n * (an engine opening up under load, a skid getting brighter).\n */\nexport type LoopSet = { readonly volume?: number; readonly frequency?: number; readonly position?: Vec3; readonly cutoff?: number }\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: LoopSet): 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 /** Where the ears are now, for a loop with a `falloff` or `pan`. */\n listener?(): Listener\n readonly distance?: DistanceMode\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?.out.disconnect()\n nodes?.out.connect(host.output(loopRecipe.bus))\n },\n stop() {\n nodes?.stop()\n nodes?.out.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 let position: Vec3 | undefined\n let cutoff: number | undefined\n const placed = loopRecipe.falloff !== undefined || loopRecipe.pan === true\n return {\n set(next) {\n if (isChain && !nodes) open()\n const context = host.ready()\n if (!context) return\n position = next.position ?? position\n cutoff = next.cutoff ?? cutoff\n const action = state.set(next)\n if (!action) return\n nodes ??= create(context, 0)\n const listener = placed && position ? host.listener?.() : undefined\n const distance = listener && position ? listenerDistance(listener, position, host.distance) : 0\n const level = listener ? falloffGain(loopRecipe.falloff, action.volume, distance) : action.volume\n nodes.gain.gain.setTargetAtTime(level, context.currentTime, smoothing.volume)\n if (action.frequency !== undefined && nodes.frequency) nodes.frequency.setTargetAtTime(action.frequency, context.currentTime, smoothing.frequency)\n if (cutoff !== undefined && nodes.cutoff) nodes.cutoff.setTargetAtTime(cutoff / cutoffDivisor(loopRecipe.falloff, distance), context.currentTime, smoothing.frequency)\n if (listener && position && nodes.panner) nodes.panner.pan.setTargetAtTime(panFor(listener, position), context.currentTime, smoothing.frequency)\n },\n stop: entry.stop,\n }\n}\n","/**\n * Sounds made of recorded files (`.ogg`), as data. A `SampleSound` lists the takes of each layer: `close` (the sound as heard near, the only one\n * required), `far` (the same event heard from far away: a muffled bang with its echo) and the tail of the place it happens in (`indoor` / `outdoor`).\n * `planSample` decides, from a seeded stream, which take plays (round-robin: never the same take twice in a row), the pitch (± a fraction), how the\n * near and far layers cross over with the distance, the level, the pan and the distance lowpass. It is pure, so it runs in Node and in tests; the\n * WebAudio layer (`realizeSample`) plays the plan from decoded buffers.\n */\nimport type { Vec3 } from '@adatechnology/game-contracts'\n\nimport { falloffGain, isSilent } from './falloff'\nimport { DEFAULT_LISTENER, listenerDistance, panFor } from './listener'\nimport type { DistanceMode, Listener } from './listener'\nimport type { BusId, Falloff, SampleCrossover, SampleEnvironment, SampleRef, SampleSound, SampleTier } from './recipe'\nimport { busPriority } from './voices'\n\nexport type { SampleCrossover, SampleEnvironment, SampleRef, SampleSound, SampleTail, SampleTier } from './recipe'\n\nexport const SAMPLE_ENVIRONMENTS = ['indoor', 'outdoor'] as const satisfies readonly SampleEnvironment[]\n\n/** The role of a layer in a plan. */\nexport type SampleLayerRole = 'close' | 'far' | 'tail'\n\nexport const DEFAULT_SAMPLE_FALLOFF: Falloff = { model: 'inverse_square', k: 0.002, minGain: 0.002 }\nexport const DEFAULT_CROSSOVER: SampleCrossover = { from: 30, to: 80 }\nexport const DEFAULT_SAMPLE_PITCH = 0.04\nexport const DEFAULT_TAIL_GAIN = 0.6\nexport const DEFAULT_SAMPLE_CUTOFF_K = 0.01\n/** The open lowpass: at or above it the realizer leaves the filter out. */\nexport const OPEN_CUTOFF = 20000\n/** A layer quieter than this is left out of the plan. */\nexport const LAYER_FLOOR = 0.001\n\n/** One file to play: which take, at what level (already times the sound level), rate and start. */\nexport type PlannedSampleLayer = {\n readonly role: SampleLayerRole\n readonly url: string\n /** Index of the take in its list. */\n readonly take: number\n readonly gain: number\n readonly rate: number\n readonly at: number\n}\n\nexport type SamplePlan = {\n readonly kind: 'sample'\n readonly id: string\n readonly bus: BusId\n readonly level: number\n readonly distance: number\n readonly pan?: number\n /** Lowpass cutoff in Hz (`OPEN_CUTOFF` = no filter). */\n readonly cutoff: number\n /** Playback rate of every layer (the pitch drawn for this play). */\n readonly rate: number\n readonly environment: SampleEnvironment\n /** The tier played (`undefined` without tiers or intensity). */\n readonly tier?: string\n /** 0 = all near, 1 = all far. */\n readonly farMix: number\n readonly layers: readonly PlannedSampleLayer[]\n readonly priority: number\n readonly group?: string\n}\n\n/** The last take each sound layer played (`<id>/<role>` → index), so the next play never repeats it. Keep one per engine or channel. */\nexport type SampleMemory = Map<string, number>\n\nexport type SamplePlanInput = {\n readonly random: () => number\n readonly listener?: Listener\n readonly distanceMode?: DistanceMode\n readonly position?: Vec3\n /** Metres; overrides `position` for the distance (not for the pan). */\n readonly distance?: number\n readonly volume?: number\n /** Default `outdoor`. */\n readonly environment?: SampleEnvironment\n /** 0 (clear) to 1 (behind a wall): lowers the level and closes the lowpass. */\n readonly occlusion?: number\n /** Round-robin memory; without it each plan is drawn on its own (and may repeat). */\n readonly memory?: SampleMemory\n /** Limiter priority when the sound has none. */\n readonly priority?: number\n /** 0 (light) to 1 (heavy): picks the tier (see `SampleTier`). */\n readonly intensity?: number\n}\n\n/**\n * Round-robin without repeat: a take drawn among all but the last one played. With one take it is always 0; without `last` any take may come.\n * One random number per call.\n */\nexport function pickTake(count: number, last: number | undefined, random: () => number): number {\n if (count <= 1) return 0\n if (last === undefined || last < 0 || last >= count) return Math.min(count - 1, Math.floor(random() * count))\n const drawn = Math.min(count - 2, Math.floor(random() * (count - 1)))\n return drawn >= last ? drawn + 1 : drawn\n}\n\n/** 0 at or below `from`, 1 at or beyond `to`, linear between. */\nexport function farMixAt(crossover: SampleCrossover, distance: number): number {\n if (crossover.to <= crossover.from) return distance >= crossover.to ? 1 : 0\n const t = (distance - crossover.from) / (crossover.to - crossover.from)\n return t <= 0 ? 0 : t >= 1 ? 1 : t\n}\n\n/** Equal-power gains of the near and far layers for a far mix (so the crossover has no dip in loudness). */\nexport function crossGains(farMix: number): { readonly close: number; readonly far: number } {\n return { close: Math.cos((farMix * Math.PI) / 2), far: Math.sin((farMix * Math.PI) / 2) }\n}\n\n/** The distance lowpass, closed further behind a wall (an occluded sound loses its highs first). */\nexport function sampleCutoff(cutoffK: number, distance: number, occlusion = 0): number {\n const byDistance = cutoffK > 0 ? OPEN_CUTOFF / (1 + distance * cutoffK) : OPEN_CUTOFF\n const occluded = occlusion > 0 ? byDistance / (1 + occlusion * 9) : byDistance\n return Math.max(80, Math.min(OPEN_CUTOFF, occluded))\n}\n\n/** Every file a sound may play (to load before its first play). */\nexport function sampleUrls(sound: SampleSound): string[] {\n const tiers = (sound.tiers ?? []).flatMap((tier) => [...tier.close, ...(tier.far ?? [])])\n const all = [...sound.close, ...(sound.far ?? []), ...tiers, ...(sound.tail?.indoor ?? []), ...(sound.tail?.outdoor ?? [])].map((ref) => ref.url)\n return [...new Set(all)]\n}\n\n/** The tier for an intensity: the last whose `from` is at or below it. */\nexport function tierFor(sound: SampleSound, intensity: number | undefined): SampleTier | undefined {\n if (intensity === undefined || !sound.tiers) return undefined\n let chosen: SampleTier | undefined\n for (const tier of sound.tiers) if (tier.from <= intensity) chosen = tier\n return chosen ?? sound.tiers[0]\n}\n\n/**\n * The plan of one play, or `null` when the sound is too far to be heard. Random numbers, in order: the pitch, the near take, the far take, the\n * tail take (each drawn only when that layer has more than zero takes and is heard).\n */\nexport function planSample(sound: SampleSound, input: SamplePlanInput): SamplePlan | null {\n if (sound.close.length === 0) throw new Error(`sample sound ${sound.id} has no close take`)\n const listener = input.listener ?? DEFAULT_LISTENER\n const distance = input.distance ?? (input.position ? listenerDistance(listener, input.position, input.distanceMode) : 0)\n const falloff = sound.falloff ?? DEFAULT_SAMPLE_FALLOFF\n const occlusion = Math.max(0, Math.min(1, input.occlusion ?? 0))\n const level = falloffGain(falloff, (input.volume ?? 1) * (sound.gain ?? 1), distance) * (1 - 0.45 * occlusion)\n if (isSilent(falloff, level) || level <= 0) return null\n\n const pitch = sound.pitch ?? DEFAULT_SAMPLE_PITCH\n const rate = pitch > 0 ? 1 + (input.random() * 2 - 1) * pitch : 1\n const environment = input.environment ?? 'outdoor'\n const tier = tierFor(sound, input.intensity)\n const close = tier?.close.length ? tier.close : sound.close\n const far = tier?.far?.length ? tier.far : (sound.far ?? [])\n const farMix = far.length > 0 ? farMixAt(sound.crossover ?? DEFAULT_CROSSOVER, distance) : 0\n const gains = crossGains(farMix)\n const layers: PlannedSampleLayer[] = []\n const take = (role: SampleLayerRole, refs: readonly SampleRef[], gain: number, at: number): void => {\n if (refs.length === 0 || gain < LAYER_FLOOR) return\n const key = `${sound.id}/${tier && role !== 'tail' ? `${tier.name}/` : ''}${role}`\n const index = pickTake(refs.length, input.memory?.get(key), input.random)\n input.memory?.set(key, index)\n const ref = refs[index] as SampleRef\n layers.push({ role, url: ref.url, take: index, gain: gain * (ref.gain ?? 1) * level, rate, at })\n }\n take('close', close, gains.close, 0)\n take('far', far, gains.far, 0)\n const tails = sound.tail?.[environment] ?? []\n take('tail', tails, sound.tail?.gain ?? DEFAULT_TAIL_GAIN, sound.tail?.at ?? 0)\n\n const pan = (sound.pan ?? true) && input.position ? panFor(listener, input.position) : undefined\n return {\n kind: 'sample',\n id: sound.id,\n bus: sound.bus,\n level,\n distance,\n ...(pan === undefined ? {} : { pan }),\n cutoff: sampleCutoff(sound.cutoffK ?? DEFAULT_SAMPLE_CUTOFF_K, distance, occlusion),\n rate,\n environment,\n ...(tier ? { tier: tier.name } : {}),\n farMix,\n layers,\n priority: sound.priority ?? input.priority ?? busPriority(sound.bus),\n ...(sound.group ? { group: sound.group } : {}),\n }\n}\n","/**\n * A continuous sound played from files (fire, rain, an engine idling): `SampleSound.loop` turns a sample sound into a loop that `engine.loop(id)` plays.\n * The takes are chained without a seam: each one starts `crossfade` seconds before the last one ends, the two crossed with equal-power ramps, and\n * the next take is drawn by the round-robin (never the same twice in a row while there are two or more). A take shorter than two crossfades\n * loops on itself (`AudioBufferSourceNode.loop`). While the files are not decoded the recipe loop of the fallback (or of the same id) plays, and\n * the files take over when they are ready, at the volume the loop has then.\n */\nimport { pickTake } from '../sample'\nimport type { BusId, SampleSound } from '../recipe'\nimport type { AudioLoopHandle, LoopEntry } from './engine-loop'\n\n/** The crossfade between two takes of a loop, in seconds, when `loop: true` gives none. */\nexport const DEFAULT_LOOP_CROSSFADE = 0.25\n/** Takes are scheduled until this far (seconds) past the clock, so a busy main thread (a heavy frame, a late timer) never leaves a gap. */\nconst LOOKAHEAD = 1.5\nconst VOLUME_SMOOTHING = 0.05\n\n/** The crossfade of a looping sample sound, or `undefined` when it does not loop. */\nexport function loopCrossfade(sound: SampleSound): number | undefined {\n if (!sound.loop) return undefined\n return sound.loop === true ? DEFAULT_LOOP_CROSSFADE : Math.max(0, sound.loop.crossfade ?? DEFAULT_LOOP_CROSSFADE)\n}\n\n/** Steps of the equal-power ramps (straight lines between points of the sine and cosine quarter). */\nexport const CROSSFADE_STEPS = 8\n\n/** The gain of an equal-power crossfade at `t` (0..1) for the take coming in; the take going out has the gain at `1 − t`. */\nexport const equalPowerIn = (t: number): number => Math.sin((Math.max(0, Math.min(1, t)) * Math.PI) / 2)\n\n// a quarter sine (in) or cosine (out) drawn as straight ramps, so it runs on any AudioParam (and on the recorder of the tests)\nconst ramp = (param: AudioParam, direction: 'in' | 'out', at: number, seconds: number): void => {\n const value = (t: number): number => (direction === 'in' ? equalPowerIn(t) : equalPowerIn(1 - t))\n param.setValueAtTime(value(0), at)\n for (let step = 1; step <= CROSSFADE_STEPS; step++) param.linearRampToValueAtTime(value(step / CROSSFADE_STEPS), at + (seconds * step) / CROSSFADE_STEPS)\n}\n\n/** What a sample loop needs from its engine. */\nexport type SampleLoopHost = {\n ready(): AudioContext | null\n /** The node sample voices of a bus go to (the limiter of the bus). */\n output(bus: BusId): AudioNode\n buffer(url: string): AudioBuffer | undefined\n isReady(sound: SampleSound): boolean\n /** Loads the files of the sound (resolves when they are decoded or failed). */\n load(sound: SampleSound): Promise<unknown>\n /** The recipe loop that plays while the files are not ready. */\n fallback(volume: number): AudioLoopHandle\n random(): number\n register(loop: LoopEntry): void\n unregister(loop: LoopEntry): void\n /** Whether a bus is muted now. */\n muted(bus: BusId): boolean\n}\n\ntype Segment = { readonly source: AudioBufferSourceNode; readonly gain: GainNode; readonly end: number }\n\n/** Plays a looping sample sound; falls back to the recipe loop while its files load. `volume` is the opening level (default 0, opened by `set`). */\nexport function createSampleLoopHandle(sound: SampleSound, host: SampleLoopHost, volume = 0): AudioLoopHandle {\n const crossfade = loopCrossfade(sound) ?? DEFAULT_LOOP_CROSSFADE\n let level = volume\n let stopped = false\n let fallback: AudioLoopHandle | undefined\n let master: GainNode | undefined\n let segments: Segment[] = []\n let last: number | undefined\n let timer: ReturnType<typeof setTimeout> | undefined\n\n const entry: LoopEntry = {\n get created() {\n return master !== undefined\n },\n rewire() {\n master?.disconnect()\n master?.connect(host.output(sound.bus))\n },\n stop() {\n stopped = true\n if (timer !== undefined) clearTimeout(timer)\n timer = undefined\n for (const segment of segments) {\n try {\n segment.source.stop()\n } catch {\n // already stopped\n }\n segment.source.disconnect()\n segment.gain.disconnect()\n }\n segments = []\n master?.disconnect()\n master = undefined\n fallback?.stop()\n fallback = undefined\n host.unregister(entry)\n },\n }\n\n const nextBuffer = (): AudioBuffer | undefined => {\n const takes = sound.close\n const take = pickTake(takes.length, last, host.random)\n last = take\n const ref = takes[take]\n return ref ? host.buffer(ref.url) : undefined\n }\n\n // schedules one take from `at`; returns when the next one starts\n const schedule = (context: AudioContext, output: AudioNode, at: number, first: boolean): number | undefined => {\n const buffer = nextBuffer()\n if (!buffer) return undefined\n const source = context.createBufferSource()\n source.buffer = buffer\n const gain = context.createGain()\n source.connect(gain)\n gain.connect(output)\n const fade = Math.min(crossfade, buffer.duration / 2)\n if (buffer.duration < 2 * crossfade || sound.close.length === 1) {\n // one take (or a take too short to cross): it loops on itself\n source.loop = true\n if (!first) ramp(gain.gain, 'in', at, Math.max(0.001, fade))\n source.start(at)\n segments.push({ source, gain, end: Number.POSITIVE_INFINITY })\n return undefined\n }\n if (first) gain.gain.setValueAtTime(1, at)\n else ramp(gain.gain, 'in', at, fade)\n const end = at + buffer.duration\n ramp(gain.gain, 'out', end - fade, fade)\n source.start(at)\n source.stop(end)\n segments.push({ source, gain, end })\n source.onended = () => {\n segments = segments.filter((segment) => segment.source !== source)\n source.disconnect()\n gain.disconnect()\n }\n return end - fade\n }\n\n const pump = (context: AudioContext, output: AudioNode, at: number, first: boolean): void => {\n if (stopped) return\n let next = schedule(context, output, at, first)\n // a late timer: catch up from the clock instead of stacking takes in the past\n while (next !== undefined && next < context.currentTime + LOOKAHEAD) next = schedule(context, output, Math.max(next, context.currentTime), false)\n if (next === undefined) return\n const wait = Math.max(0, (next - context.currentTime - LOOKAHEAD / 2) * 1000)\n timer = setTimeout(() => {\n timer = undefined\n pump(context, output, next, false)\n }, wait)\n }\n\n // the files play at the sound's own level (`SampleSound.gain`); the fallback recipe loop has its own\n const fileLevel = (): number => level * (sound.gain ?? 1)\n\n const startFiles = (): boolean => {\n if (stopped || master || !host.isReady(sound)) return master !== undefined\n const context = host.ready()\n if (!context) return false\n master = context.createGain()\n master.gain.setValueAtTime(fileLevel(), context.currentTime)\n master.connect(host.output(sound.bus))\n host.register(entry)\n pump(context, master, context.currentTime, true)\n if (fallback) {\n fallback.set({ volume: 0 })\n const old = fallback\n fallback = undefined\n setTimeout(() => old.stop(), VOLUME_SMOOTHING * 6000)\n }\n return true\n }\n\n const ensure = (): void => {\n if (stopped || master) return\n if (startFiles()) return\n if (!fallback) {\n fallback = host.fallback(level)\n void host\n .load(sound)\n .then(() => {\n if (level > 0 || master) startFiles()\n })\n .catch(() => undefined)\n }\n }\n\n if (volume > 0) ensure()\n\n return {\n set(next) {\n if (stopped) return\n if (next.volume !== undefined) level = host.muted(sound.bus) ? 0 : Math.max(0, next.volume)\n if (level > 0) ensure()\n const context = host.ready()\n if (master && context) master.gain.setTargetAtTime(fileLevel(), context.currentTime, VOLUME_SMOOTHING)\n fallback?.set(next.volume === undefined ? next : { ...next, volume: level })\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","import { DEFERRED_QUEUE_ID } from '@adatechnology/game-contracts'\nimport type { DeferredQueue } from '@adatechnology/game-contracts'\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 = {\n push(job: Omit<StartJob, 'queuedAt'>): void\n clear(): void\n readonly size: number\n /** The same queue as a `DeferredQueue` for a shared frame budget. Asking for it changes nothing: the queue drains by itself until a budget attaches it. */\n deferredQueue?(): DeferredQueue\n}\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. */\n/** The queue this package makes always has its `deferredQueue`; a third-party `StartQueue` may leave it out. */\nexport type OwnStartQueue = StartQueue & { deferredQueue(): DeferredQueue }\n\nexport function createStartQueue(options: StartQueueOptions = {}): OwnStartQueue {\n const budgetMs = options.budgetMs ?? 2\n const maxLatencyMs = options.maxLatencyMs ?? 250\n let jobs: StartJob[] = []\n let scheduled = false\n // true once a frame budget owns the draining: nothing schedules a task by itself\n let attached = false\n\n const runNext = (): void => {\n const job = jobs.shift() as StartJob\n if (nowMs() - job.queuedAt > maxLatencyMs) job.drop()\n else job.run()\n }\n\n const drain = (): void => {\n scheduled = false\n if (attached) return\n const began = nowMs()\n while (jobs.length > 0) {\n runNext()\n if (nowMs() - began >= budgetMs) break\n }\n if (jobs.length > 0) schedule()\n }\n\n const schedule = (): void => {\n if (scheduled || attached) return\n scheduled = true\n scheduleTask(drain)\n }\n\n const queue: OwnStartQueue = {\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 deferredQueue: () => deferredQueue,\n }\n\n const deferredQueue: DeferredQueue = {\n id: DEFERRED_QUEUE_ID.AUDIO_VOICES,\n pending: () => jobs.length,\n run(deadline, clock) {\n let ran = 0\n while (jobs.length > 0) {\n runNext()\n ran++\n if (clock() >= deadline) break\n }\n return ran\n },\n flush() {\n while (jobs.length > 0) runNext()\n },\n attach() {\n attached = true\n },\n detach() {\n attached = false\n if (jobs.length > 0) schedule()\n },\n }\n return queue\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 { PlannedAutomation, PlannedFilter, PlannedLayer, PlannedModulator, PlannedSource, SoundPlan } from './plan'\nimport type { SoundSet } from './recipe'\n\nconst tuneAutomation = (automation: PlannedAutomation, rate: number): PlannedAutomation => {\n const tuned: { -readonly [K in keyof PlannedAutomation]: PlannedAutomation[K] } = { ...automation }\n if (automation.value !== undefined) tuned.value = automation.value * rate\n if (automation.from !== undefined) tuned.from = automation.from * rate\n if (automation.to !== undefined) tuned.to = automation.to * rate\n if (automation.at !== undefined) tuned.at = automation.at / rate\n return tuned\n}\n\nconst tuneFilter = (filter: PlannedFilter, rate: number): PlannedFilter => ({ ...filter, frequency: tuneAutomation(filter.frequency, rate) })\n\nconst tuneSource = (source: PlannedSource, rate: number): PlannedSource => {\n switch (source.kind) {\n case 'noise':\n return source\n case 'osc':\n return { ...source, frequency: tuneAutomation(source.frequency, rate) }\n case 'formant_voice':\n return {\n ...source,\n frequency: tuneAutomation(source.frequency, rate),\n formants: source.formants.map((formant) => ({ ...formant, frequency: formant.frequency * rate })),\n breath: { ...source.breath, frequency: source.breath.frequency * rate },\n }\n }\n}\n\nconst tuneModulator = (modulator: PlannedModulator, rate: number): PlannedModulator => ({\n ...modulator,\n rate: modulator.rate * rate,\n ...(modulator.target === 'frequency' ? { depth: modulator.depth * rate } : {}),\n})\n\nconst tuneLayer = (layer: PlannedLayer, rate: number): PlannedLayer => ({\n ...layer,\n at: layer.at / rate,\n duration: layer.duration / rate,\n tail: layer.tail / rate,\n source: tuneSource(layer.source, rate),\n ...(layer.filters ? { filters: { ...layer.filters, bank: layer.filters.bank.map((filter) => tuneFilter(filter, rate)) } } : {}),\n modulators: layer.modulators.map((modulator) => tuneModulator(modulator, rate)),\n envelope: layer.envelope.map((point) => ({ ...point, time: point.time / rate })),\n})\n\n/**\n * A plan played faster or slower, as a tape would: every frequency (tones, filters, formants, LFOs) times `rate`, every time divided by it. Noise\n * has no pitch of its own, so its colour moves with its filters. `rate` 1 gives the plan back.\n */\nexport function retunePlan(plan: SoundPlan, rate: number): SoundPlan {\n if (rate === 1 || !(rate > 0)) return plan\n return { ...plan, length: plan.length / rate, layers: plan.layers.map((layer) => tuneLayer(layer, rate)) }\n}\n\n/** Remembers the last variant of each set, so the next draw never repeats it. */\nexport type SetPicker = {\n /** The variant to play and its rate, drawn from `random` (two numbers: the variant, then the rate). */\n pick(set: SoundSet, random: () => number): { readonly id: string; readonly rate: number }\n /** Forget the last variants (a fresh scene). */\n reset(): void\n}\n\nexport function createSetPicker(): SetPicker {\n const last = new Map<string, number>()\n return {\n pick(set, random) {\n const count = set.variants.length\n const previous = last.get(set.id)\n let index: number\n if (count === 1) index = 0\n else if (set.repeat || previous === undefined) index = Math.min(count - 1, Math.floor(random() * count))\n else {\n // one of the others: draw among count − 1 and skip the previous\n index = Math.min(count - 2, Math.floor(random() * (count - 1)))\n if (index >= previous) index += 1\n }\n last.set(set.id, index)\n const [min, max] = set.rate ?? [1, 1]\n const rate = min === max ? min : min + random() * (max - min)\n return { id: set.variants[index] as string, rate }\n },\n reset: () => last.clear(),\n }\n}\n","/**\n * Decoded sample buffers by url. Files load once (concurrent asks share the same promise), decode on the engine's context and stay until `clear`.\n * The bytes come from a `SampleLoader`: `fetch` by default, or anything else (a single-file page passes the files inlined as base64).\n */\n\n/** Gets the bytes of a file. */\nexport type SampleLoader = (url: string) => Promise<ArrayBuffer>\n\nexport type SampleStatus = 'none' | 'loading' | 'ready' | 'failed'\n\nexport type SampleLoadReport = { readonly loaded: readonly string[]; readonly failed: readonly { readonly url: string; readonly error: string }[] }\n\nexport type SampleStore = {\n status(url: string): SampleStatus\n /** The decoded buffer, once ready. */\n buffer(url: string): AudioBuffer | undefined\n /** Whether every url is ready. */\n ready(urls: readonly string[]): boolean\n load(urls: readonly string[], context: BaseAudioContext): Promise<SampleLoadReport>\n /** Seconds of the decoded file, or `undefined` while not ready. */\n seconds(url: string): number | undefined\n clear(): void\n}\n\n/** `fetch` relative to `baseUrl` (default the page). */\nexport function fetchLoader(baseUrl?: string): SampleLoader {\n return async (url) => {\n const target = baseUrl ? new URL(url, baseUrl).toString() : url\n const response = await fetch(target)\n if (!response.ok) throw new Error(`${response.status} ${response.statusText} for ${target}`)\n return response.arrayBuffer()\n }\n}\n\n/** Bytes of files given inline as base64 (or `data:` urls), by url. */\nexport function inlineLoader(files: Readonly<Record<string, string>>): SampleLoader {\n return async (url) => {\n const text = files[url]\n if (text === undefined) throw new Error(`no inline file for ${url}`)\n const base64 = text.startsWith('data:') ? text.slice(text.indexOf(',') + 1) : text\n const binary = atob(base64)\n const bytes = new Uint8Array(binary.length)\n for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index)\n return bytes.buffer\n }\n}\n\nconst decode = (context: BaseAudioContext, bytes: ArrayBuffer): Promise<AudioBuffer> =>\n new Promise((resolve, reject) => {\n // the callback form also covers old Safari, where decodeAudioData returns no promise\n const result = context.decodeAudioData(bytes, resolve, reject) as Promise<AudioBuffer> | undefined\n result?.catch?.(reject)\n })\n\nexport function createSampleStore(loader: SampleLoader = fetchLoader()): SampleStore {\n const buffers = new Map<string, AudioBuffer>()\n const pending = new Map<string, Promise<AudioBuffer>>()\n const failed = new Map<string, string>()\n let generation = 0\n\n const loadOne = (url: string, context: BaseAudioContext): Promise<AudioBuffer> => {\n const existing = buffers.get(url)\n if (existing) return Promise.resolve(existing)\n const inFlight = pending.get(url)\n if (inFlight) return inFlight\n const started = generation\n failed.delete(url)\n const promise = loader(url)\n .then((bytes) => decode(context, bytes))\n .then((buffer) => {\n if (started === generation) buffers.set(url, buffer)\n return buffer\n })\n .catch((error: unknown) => {\n if (started === generation) failed.set(url, error instanceof Error ? error.message : String(error))\n throw error\n })\n .finally(() => {\n if (pending.get(url) === promise) pending.delete(url)\n })\n pending.set(url, promise)\n return promise\n }\n\n return {\n status(url) {\n if (buffers.has(url)) return 'ready'\n if (pending.has(url)) return 'loading'\n if (failed.has(url)) return 'failed'\n return 'none'\n },\n buffer: (url) => buffers.get(url),\n ready: (urls) => urls.every((url) => buffers.has(url)),\n seconds: (url) => buffers.get(url)?.duration,\n async load(urls, context) {\n const results = await Promise.allSettled(urls.map((url) => loadOne(url, context)))\n const loaded: string[] = []\n const errors: { url: string; error: string }[] = []\n results.forEach((result, index) => {\n const url = urls[index] as string\n if (result.status === 'fulfilled') loaded.push(url)\n else errors.push({ url, error: result.reason instanceof Error ? result.reason.message : String(result.reason) })\n })\n return { loaded, failed: errors }\n },\n clear() {\n generation++\n buffers.clear()\n pending.clear()\n failed.clear()\n },\n }\n}\n","import { OPEN_CUTOFF } from '../sample'\nimport type { SamplePlan } from '../sample'\nimport { gainNode, voiceOf } from './nodes'\nimport type { RealizedVoice } from './nodes'\n\nexport type SampleTarget = {\n readonly context: BaseAudioContext\n readonly output: AudioNode\n readonly buffer: (url: string) => AudioBuffer | undefined\n readonly time: number\n}\n\n/** A sample voice can also be faded out (the choke of its sound). */\nexport type RealizedSampleVoice = RealizedVoice & { fade(at: number, seconds: number): void }\n\n/**\n * Builds a sample plan: each layer is a buffer source at the planned rate with its own gain, the layers meet in one voice gain (the choke fades it),\n * one lowpass (left out when open) and one stereo panner (when the plan has a pan), then the bus. A layer whose buffer is not loaded is skipped.\n */\nexport function realizeSample(plan: SamplePlan, target: SampleTarget): RealizedSampleVoice {\n const context = target.context\n let head: AudioNode = target.output\n if (plan.pan !== undefined && typeof context.createStereoPanner === 'function') {\n const panner = context.createStereoPanner()\n panner.pan.value = plan.pan\n panner.connect(head)\n head = panner\n }\n if (plan.cutoff < OPEN_CUTOFF) {\n const lowpass = context.createBiquadFilter()\n lowpass.type = 'lowpass'\n lowpass.frequency.value = plan.cutoff\n lowpass.Q.value = 0.7\n lowpass.connect(head)\n head = lowpass\n }\n const voice = context.createGain()\n voice.connect(head)\n head = voice\n const sources: AudioBufferSourceNode[] = []\n for (const layer of plan.layers) {\n const buffer = target.buffer(layer.url)\n if (!buffer) continue\n const source = context.createBufferSource()\n source.buffer = buffer\n source.playbackRate.value = layer.rate\n const gain = gainNode(context as AudioContext, layer.gain)\n source.connect(gain)\n gain.connect(head)\n source.start(target.time + layer.at)\n sources.push(source)\n }\n const realized = voiceOf(sources)\n return {\n ...realized,\n fade(at, seconds) {\n voice.gain.setValueAtTime(1, at)\n voice.gain.linearRampToValueAtTime(0, at + seconds)\n for (const source of sources) {\n try {\n source.stop(at + seconds)\n } catch {\n // already stopped\n }\n }\n },\n }\n}\n\n/** Seconds a sample plan lasts with its buffers (the longest layer, at its rate). */\nexport function sampleLength(plan: SamplePlan, seconds: (url: string) => number | undefined): number {\n let length = 0\n for (const layer of plan.layers) length = Math.max(length, layer.at + (seconds(layer.url) ?? 0) / layer.rate)\n return length\n}\n","import type { DeferredQueue, 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 { createSampleLoopHandle } from './sample-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'\nimport { createSetPicker, retunePlan } from '../variation'\nimport { planSample, sampleUrls } from '../sample'\nimport type { SampleEnvironment, SampleMemory, SamplePlan, SampleSound } from '../sample'\nimport { createSampleStore, fetchLoader } from './sample-store'\nimport type { SampleLoadReport, SampleLoader, SampleStatus } from './sample-store'\nimport { realizeSample, sampleLength } from './realize-sample'\nimport type { RealizedSampleVoice } from './realize-sample'\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 /** Built now instead of waiting in the engine's deferred start queue: for a sound that has to land on the frame the player sees it (a blow's\n * contact, a heel strike, a landing). The queue builds a voice on the next frame budget and drops it after its latency, which at a low frame\n * rate is late or silent. Default false. */\n readonly urgent?: boolean\n /** A named stream of the engine (`streamFor(seed, name)`), so one subject never shifts another's sounds. */\n readonly channel?: string\n /** The tail a sample sound plays (default the engine's, see `setEnvironment`). */\n readonly environment?: SampleEnvironment\n /** 0 (light) to 1 (heavy): the tier of takes a sample sound plays (criterion C7). */\n readonly intensity?: number\n /** 0 (clear) to 1 (behind a wall) for this play, over the engine's `occlusion` (a test bench puts a wall in between). Sample sounds only. */\n readonly occlusion?: number\n}\n\n/** A playing voice: a recipe (`kind: 'recipe'`, the plan of the synthesized sound) or files (`kind: 'sample'`). */\nexport type VoiceHandle =\n | { readonly id: number; readonly kind: 'recipe'; readonly plan: SoundPlan; stop(): void }\n | { readonly id: number; readonly kind: 'sample'; readonly plan: SamplePlan; stop(): void }\n\n/**\n * A voice that started, for measuring (criterion C1: the event against the start of the sound). Times are the context clock, in seconds:\n * `requestedAt` when `play` was called, `startAt` when the first sample of the sound leaves the engine; `outputLatency` is what the device adds\n * after that (`baseLatency + outputLatency` of the context, 0 when unknown).\n */\nexport type VoiceStartEvent = {\n readonly id: string\n readonly kind: 'recipe' | 'sample'\n /** The sample sound this recipe stood in for while its files were not loaded. */\n readonly fallbackOf?: string\n readonly requestedAt: number\n readonly startAt: number\n readonly outputLatency: number\n readonly level: number\n readonly distance: number\n readonly pan?: number\n /** The files a sample voice plays, by layer. */\n readonly layers?: readonly { readonly role: string; readonly url: string; readonly take: number; readonly rate: number }[]\n /** The tier of takes a sample voice played (criterion C7). */\n readonly tier?: string\n}\n\nexport type SampleOptions = {\n /** Gets the bytes of a file (default `fetch`, relative to `baseUrl`). */\n readonly loader?: SampleLoader\n readonly baseUrl?: string\n /** Load a sound's files the first time it is played (default true); the fallback plays meanwhile. */\n readonly autoLoad?: boolean\n /**\n * The peak limiter every sample voice of a bus goes through before the bus (criteria C6 and C9: no sound and no burst goes over −1 dBFS): linear\n * up to `knee` (default 0.5, −6 dBFS), then bent smoothly to `ceiling` (default 0.891, −1 dBFS), with no delay. `false` leaves it out.\n */\n readonly limiter?: false | { readonly knee?: number; readonly ceiling?: number }\n}\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 /** Where sample sounds get their files. */\n readonly samples?: SampleOptions\n /** The tail sample sounds play when `play` names none (default `outdoor`). */\n readonly environment?: SampleEnvironment\n /** Called when a voice is built (recipe or sample): the measuring hook of the criteria. */\n readonly onVoiceStart?: (event: VoiceStartEvent) => void\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 /** Pure: the plan a sample sound would play with (no round-robin memory), from `options.seed` or the engine seed. */\n planSample(id: string, options?: PlayOptions): SamplePlan | null\n /** Loads the files of these sample sounds (default all registered). Creates the context; resolves when every file is decoded or failed. */\n loadSamples(ids?: readonly string[]): Promise<SampleLoadReport>\n /** `ready` when every file of the sound is decoded. */\n sampleStatus(id: string): SampleStatus\n /** The tail sample sounds play from now on. */\n setEnvironment(environment: SampleEnvironment): void\n /** The node everything is mixed into before the speakers (to record or meter the whole output), or `null` before the context exists. */\n output(): AudioNode | null\n /** Another place files come from: urls `accepts` takes go to `loader`, the rest to the engine's own (a bench lends its calibration takes). */\n addSampleSource(accepts: (url: string) => boolean, loader: SampleLoader): void\n /** Follows every voice built from now on (as `onVoiceStart` does); returns the unsubscribe function. */\n watchVoices(listener: (event: VoiceStartEvent) => void): () => void\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 /**\n * The voice start queue as a `DeferredQueue` for a shared frame budget (`budget.register` attaches it and the queue stops scheduling its own tasks), or `null`\n * without `deferredStart`. Asking for it changes nothing.\n */\n deferredQueue?(): DeferredQueue | null\n dispose(): void\n}\n\n/** The limiter's transfer curve over ±`range`: linear up to `knee`, then a tanh bend that never passes `ceiling`. */\nexport function limiterCurve(knee: number, ceiling: number, range: number, points = 2049): Float32Array<ArrayBuffer> {\n const curve = new Float32Array(points)\n const room = Math.max(1e-6, ceiling - knee)\n for (let index = 0; index < points; index++) {\n const value = ((index / (points - 1)) * 2 - 1) * range\n const size = Math.abs(value)\n const bent = size <= knee ? size : knee + room * Math.tanh((size - knee) / room)\n curve[index] = Math.sign(value) * bent\n }\n return curve\n}\n\ntype Admission = { readonly bus: BusId; readonly group?: string; readonly priority?: number }\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 let environment: SampleEnvironment = options.environment ?? 'outdoor'\n const sampleLimiters = new Map<BusId, AudioNode>()\n // recipes go straight to the bus (their traces match the team routines node for node); recorded files go through a limiter of their bus. It is a\n // waveshaper, not a compressor: a compressor of the browser looks 6 ms ahead, which C1 (≤ 20 ms from the event) cannot spare.\n const sampleOutput = (bus: BusId): AudioNode => {\n const setting = options.samples?.limiter\n if (setting === false || !context) return outputFor(bus)\n const existing = sampleLimiters.get(bus)\n if (existing) return existing\n const range = 4\n const input = context.createGain()\n input.gain.value = 1 / range\n const shaper = context.createWaveShaper()\n shaper.curve = limiterCurve(setting?.knee ?? 0.5, setting?.ceiling ?? 0.891, range)\n ensureBus(bus)\n input.connect(shaper)\n shaper.connect(outputFor(bus))\n sampleLimiters.set(bus, input)\n return input\n }\n const chokes = new Map<string, { readonly id: number; readonly voice: RealizedSampleVoice }[]>()\n // the voices of a choked sound still ringing (the limiter forgets the ones that ended)\n const choked = (id: string): { readonly id: number; readonly voice: RealizedSampleVoice }[] => {\n const list = (chokes.get(id) ?? []).filter((entry) => active.has(entry.id))\n chokes.set(id, list)\n return list\n }\n const watchers = new Set<(event: VoiceStartEvent) => void>()\n const announce = (event: VoiceStartEvent): void => {\n options.onVoiceStart?.(event)\n for (const watcher of watchers) watcher(event)\n }\n const sources: { readonly accepts: (url: string) => boolean; readonly loader: SampleLoader }[] = []\n const baseLoader = options.samples?.loader ?? fetchLoader(options.samples?.baseUrl)\n const sampleStore = createSampleStore((url) => (sources.find((source) => source.accepts(url))?.loader ?? baseLoader)(url))\n const memory: SampleMemory = new Map()\n const latencyOf = (audio: BaseAudioContext): number => {\n const live = audio as Partial<Pick<AudioContext, 'baseLatency' | 'outputLatency'>>\n return (live.baseLatency ?? 0) + (live.outputLatency ?? 0)\n }\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, requestedAt: number, fallbackOf?: string): RealizedVoice => {\n const audio = context as AudioContext\n const time = audio.currentTime + delay\n const realized = 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,\n })\n announce({\n id: plan.id,\n kind: 'recipe',\n ...(fallbackOf ? { fallbackOf } : {}),\n requestedAt,\n startAt: time,\n outputLatency: latencyOf(audio),\n level: plan.level,\n distance: plan.distance,\n ...(plan.pan === undefined ? {} : { pan: plan.pan }),\n })\n return realized\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, fallbackOf?: string): RealizedVoice => {\n const requestedAt = context?.currentTime ?? 0\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, requestedAt, fallbackOf)\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, fallbackOf?: string, urgent = false): VoiceHandle | null => {\n const realized = startQueue && !urgent ? deferVoice(plan, store, voiceId, delay, fallbackOf) : build(plan, store, delay, context?.currentTime ?? 0, fallbackOf)\n if (voiceId === undefined) return null\n voices.setLength(voiceId, plan.length + delay)\n active.set(voiceId, realized)\n return {\n id: voiceId,\n kind: 'recipe',\n plan,\n stop() {\n realized.stop()\n voices.release(voiceId)\n active.delete(voiceId)\n },\n }\n }\n\n const admit = (recipe: Admission, 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 // a set id (`SoundBank.sets`) plays one of its variants, never the last one, at a drawn rate; a recipe id plays itself at rate 1\n const picker = createSetPicker()\n const resolve = (id: string, random: () => number, chooser = picker): { readonly id: string; readonly rate: number } => {\n if (registry.sound(id)) return { id, rate: 1 }\n const set = registry.set(id)\n return set ? chooser.pick(set, random) : { id, rate: 1 }\n }\n\n const loadSounds = (sounds: readonly SampleSound[]): Promise<SampleLoadReport> => {\n const audio = ready()\n if (!audio) return Promise.resolve({ loaded: [], failed: sounds.flatMap(sampleUrls).map((url) => ({ url, error: 'no audio context' })) })\n return sampleStore.load([...new Set(sounds.flatMap(sampleUrls))], audio)\n }\n\n const sampleInput = (playOptions: PlayOptions, random: () => number, withMemory: boolean) => ({\n random,\n listener,\n environment: playOptions.environment ?? environment,\n ...(withMemory ? { memory } : {}),\n ...(options.distance ? { distanceMode: options.distance } : {}),\n ...(playOptions.volume === undefined ? {} : { volume: playOptions.volume }),\n ...(playOptions.intensity === undefined ? {} : { intensity: playOptions.intensity }),\n ...(playOptions.distance === undefined ? {} : { distance: playOptions.distance }),\n ...(playOptions.position ? { position: playOptions.position } : {}),\n ...(playOptions.occlusion !== undefined\n ? { occlusion: playOptions.occlusion }\n : options.occlusion && playOptions.position\n ? { occlusion: options.occlusion(playOptions.position) }\n : {}),\n })\n\n // undefined: the files are not ready and there is no fallback, so the recipe of the same id (if any) plays\n const playSample = (sample: SampleSound, playOptions: PlayOptions, random: () => number, store: NoiseStore): VoiceHandle | null | undefined => {\n if (busSettings.get(sample.bus)?.muted) return null\n if (!sampleStore.ready(sampleUrls(sample))) {\n if (options.samples?.autoLoad !== false && ready()) void loadSounds([sample]).catch(() => undefined)\n if (sample.fallback && sample.fallback !== sample.id) return playWith(sample.fallback, playOptions, random, store, sample.id)\n return registry.sound(sample.id) ? undefined : null\n }\n const plan = planSample(sample, sampleInput(playOptions, playOptions.seed === undefined ? random : createRng(playOptions.seed), true))\n if (!plan) return null\n const audio = ready()\n if (!audio) return null\n unlockContext(audio)\n advance()\n const voiceId = admit(plan, plan.level, plan.distance)\n if (voiceId === null) return null\n const requestedAt = audio.currentTime\n const time = requestedAt + (playOptions.delay ?? 0)\n const chokeList = sample.choke ? choked(sample.id) : undefined\n const realized = realizeSample(plan, { context: audio, output: sampleOutput(plan.bus), buffer: (url) => sampleStore.buffer(url), time })\n announce({\n id: plan.id,\n kind: 'sample',\n requestedAt,\n startAt: time,\n outputLatency: latencyOf(audio),\n level: plan.level,\n distance: plan.distance,\n ...(plan.pan === undefined ? {} : { pan: plan.pan }),\n layers: plan.layers.map((layer) => ({ role: layer.role, url: layer.url, take: layer.take, rate: layer.rate })),\n ...(plan.tier ? { tier: plan.tier } : {}),\n })\n voices.setLength(voiceId, sampleLength(plan, (url) => sampleStore.seconds(url)) + (playOptions.delay ?? 0))\n active.set(voiceId, realized)\n if (chokeList && sample.choke) {\n chokeList.push({ id: voiceId, voice: realized })\n while (chokeList.length > Math.max(1, sample.choke.voices)) {\n const oldest = chokeList.shift()\n if (!oldest) break\n oldest.voice.fade(time, sample.choke.fade ?? 0.03)\n voices.release(oldest.id)\n active.delete(oldest.id)\n }\n }\n return {\n id: voiceId,\n kind: 'sample',\n plan,\n stop() {\n realized.stop()\n voices.release(voiceId)\n active.delete(voiceId)\n },\n }\n }\n\n const playWith = (asked: string, playOptions: PlayOptions, streamRandom: () => number, store: NoiseStore, fallbackOf?: string): VoiceHandle | null => {\n const sample = fallbackOf === undefined ? registry.sample(asked) : undefined\n if (sample) {\n const played = playSample(sample, playOptions, streamRandom, store)\n if (played !== undefined) return played\n }\n const seeded = playOptions.seed === undefined ? null : createRng(playOptions.seed)\n const { id, rate } = resolve(asked, seeded ?? streamRandom)\n const random = seeded ?? streamRandom\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, random))\n return plan ? start(retunePlan(plan, rate), store, voiceId, playOptions.delay, fallbackOf, playOptions.urgent) : 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 // pure: a set is drawn with a picker of its own, so planning never moves what `play` picks next\n const planWith = (asked: string, playOptions: PlayOptions, random: () => number): SoundPlan | null => {\n const { id, rate } = resolve(asked, random, createSetPicker())\n const recipe = registry.sound(id)\n const plan = recipe ? planSound(recipe, inputFor(recipe, playOptions, random)) : null\n return plan ? retunePlan(plan, rate) : 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 listener: () => listener,\n ...(options.distance ? { distance: options.distance } : {}),\n }\n\n return {\n registry,\n register: (bank) => registry.register(bank),\n plan: (id, playOptions = {}) => planWith(id, playOptions, createRng(playOptions.seed ?? seed)),\n planSample(id, playOptions = {}) {\n const sample = registry.sample(id)\n return sample ? planSample(sample, sampleInput(playOptions, createRng(playOptions.seed ?? seed), false)) : null\n },\n loadSamples(ids) {\n const chosen = (ids ?? registry.sampleIds()).map((id) => registry.sample(id)).filter((sample): sample is SampleSound => sample !== undefined)\n return loadSounds(chosen)\n },\n sampleStatus(id) {\n const sample = registry.sample(id)\n if (!sample) return 'none'\n const statuses = sampleUrls(sample).map((url) => sampleStore.status(url))\n if (statuses.every((status) => status === 'ready')) return 'ready'\n if (statuses.includes('failed')) return 'failed'\n if (statuses.includes('loading')) return 'loading'\n return statuses.includes('ready') ? 'loading' : 'none'\n },\n setEnvironment(next) {\n environment = next\n },\n output: () => chain?.master ?? null,\n addSampleSource(accepts, loader) {\n sources.push({ accepts, loader })\n },\n watchVoices(listener) {\n watchers.add(listener)\n return () => void watchers.delete(listener)\n },\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 = {}) {\n const sample = registry.sample(id)\n if (!sample?.loop) return createLoopHandle(registry.loop(id), loopHost, loopOptions.volume)\n return createSampleLoopHandle(\n sample,\n {\n ready,\n output: sampleOutput,\n buffer: (url) => sampleStore.buffer(url),\n isReady: (sound) => sampleStore.ready(sampleUrls(sound)),\n load: (sound) => (options.samples?.autoLoad === false ? Promise.resolve() : loadSounds([sound])),\n fallback: (volume) => createLoopHandle(registry.loop(sample.fallback ?? sample.id) ?? registry.loop(sample.id), loopHost, volume),\n random: engineRandom,\n register: loopHost.register,\n unregister: loopHost.unregister,\n muted: (bus) => busSettings.get(bus)?.muted === true,\n },\n loopOptions.volume,\n )\n },\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 deferredQueue: () => startQueue?.deferredQueue() ?? null,\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 sampleStore.clear()\n memory.clear()\n sampleLimiters.clear()\n chokes.clear()\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', kit: 'Kit (art scene + limiter)' },\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', kit: 'Kit (Cena de Arte + limitador)' },\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 { DEFAULT_MIX, 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: DEFAULT_MIX,\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","/**\n * The sounds a game needs, by category, as the sound checklist lists them (generic, real-world names: a 9 mm pistol, a wooden door): the id a file\n * takes, its priority and the synthesized sound it replaces today (`current`, the A of an A/B). A sound is \"file\" once a bank registers a sample under\n * its id; \"synth\" while only `current` exists; \"missing\" when neither does. Data only: a game adds, renames or drops entries without touching the engine.\n */\n\nexport const SFX_CATEGORIES = ['armas', 'impactos', 'explosoes-fogo', 'personagem', 'veiculos', 'itens-portas', 'ambiente'] as const\nexport type SfxCategory = (typeof SFX_CATEGORIES)[number]\n\nexport type SfxPriority = 1 | 2 | 3\n\nexport type SfxEntry = {\n readonly id: string\n readonly category: SfxCategory\n readonly priority: SfxPriority\n /** The synthesized sound playing in its place today (a recipe id of some bank), if any. */\n readonly current?: string\n}\n\nconst entries = (category: SfxCategory, list: readonly (readonly [id: string, priority: SfxPriority, current?: string])[]): SfxEntry[] =>\n list.map(([id, priority, current]) => ({ id, category, priority, ...(current ? { current } : {}) }))\n\nexport const SFX_CATALOG: readonly SfxEntry[] = [\n ...entries('armas', [\n ['pistol_22lr', 2, 'sfx_pistol'],\n ['pistol_9mm', 1, 'sfx_pistol'],\n ['pistol_45', 1, 'sfx_pistol'],\n ['revolver_38_357', 2, 'sfx_pistol'],\n ['revolver_44_50ae', 3, 'sfx_pistol'],\n ['smg_9mm_auto', 1, 'sfx_pistol'],\n ['shotgun_12', 1, 'sfx_shotgun'],\n ['rifle_5_56', 1, 'sfx_rifle'],\n ['rifle_7_62x39', 1, 'sfx_rifle'],\n ['rifle_auto_burst', 1, 'sfx_rifle'],\n ['rifle_308_3006', 2, 'sfx_rifle'],\n ['sniper_338_300wm', 2, 'sfx_rifle'],\n ['antimateriel_50bmg', 3, 'sfx_rifle'],\n ['suppressed_pistol', 2],\n ['suppressed_rifle', 2],\n ['gunshot_far_pistol', 1],\n ['gunshot_far_rifle', 1],\n ['gunshot_far_shotgun', 1],\n ['gunshot_far_heavy', 2],\n ['gunshot_tail_indoor', 1],\n ['gunshot_tail_outdoor', 1],\n ['bullet_crack_by', 2],\n ['bullet_whiz', 2],\n ['ricochet_metal', 2, 'ping'],\n ['dry_fire', 1],\n ['mag_out', 1],\n ['mag_in', 1],\n ['slide_pull_release', 1],\n ['pump_cycle', 1],\n ['casing_drop_concreto_pistol', 1],\n ['casing_drop_concreto_rifle', 1],\n ['casing_drop_concreto_shell', 1],\n ['casing_drop_madeira_pistol', 1],\n ['casing_drop_madeira_rifle', 1],\n ['casing_drop_madeira_shell', 1],\n ['casing_drop_metal_pistol', 1],\n ['casing_drop_metal_rifle', 1],\n ['casing_drop_metal_shell', 1],\n ['casing_drop_terra_pistol', 1],\n ['casing_drop_terra_rifle', 1],\n ['casing_drop_terra_shell', 1],\n ['rocket_fire', 2],\n ]),\n ...entries('explosoes-fogo', [\n ['explosion_grenade', 1, 'boom'],\n ['explosion_big', 1, 'boom_big'],\n ['explosion_car', 2, 'boom_big'],\n ['explosion_far', 1],\n ['debris_rain_stone', 1],\n ['molotov_break_ignite', 1, 'glass_shatter'],\n ['fire_ignite', 1, 'hiss'],\n ['fire_loop_small', 1],\n ['fire_loop_large', 1],\n ['fire_extinguish', 2, 'hiss'],\n ['gas_leak_hiss', 2],\n ['structure_collapse', 2, 'crumble'],\n ]),\n ...entries('personagem', [\n ['footstep_concrete_walk', 1, 'sfx_step'],\n ['footstep_wood_walk', 1, 'sfx_step'],\n ['footstep_dirt_walk', 1, 'sfx_step'],\n ['footstep_grass_walk', 1, 'sfx_step'],\n ['footstep_gravel_walk', 1, 'sfx_step'],\n ['land_concrete', 1, 'sfx_step'],\n ['punch_swing', 1],\n ['punch_hit_body', 1, 'sfx_flesh'],\n ['kick_hit', 1, 'sfx_flesh'],\n ['knife_stab', 1],\n ['blunt_hit', 1],\n ['bite', 1, 'sfx_flesh'],\n ['body_fall', 1],\n ['player_hurt', 2],\n ]),\n ...entries('veiculos', [\n ['engine_loop_car', 1],\n ['engine_start', 1],\n ['crash_light', 1, 'crash'],\n ['crash_heavy', 1, 'crash'],\n ['car_door_open', 1, 'latch'],\n ['car_door_close', 1, 'shut_metal'],\n ['horn_car', 2, 'horn'],\n ['tire_skid_asphalt', 2],\n ]),\n ...entries('itens-portas', [\n ['item_pickup_metal', 1],\n ['can_open', 1],\n ['can_crush', 1],\n ['door_wood_open', 1, 'latch'],\n ['door_wood_close', 1, 'shut_wood'],\n ['door_creak', 1, 'creak_wood'],\n ['door_locked_rattle', 1, 'rattle'],\n ['drawer_open', 2, 'slide'],\n ['fridge_close', 2, 'shut_metal'],\n ]),\n ...entries('ambiente', [\n ['rain_loop', 2],\n ['wind_loop', 2],\n ['thunder_near', 2],\n ['thunder_far', 2],\n ['water_splash', 2],\n ]),\n]\n\n/** The materials of the impact matrix, with the group whose synthesized sound plays for them today. */\nexport const SFX_MATERIALS = [\n ['steel', 'metal'],\n ['aluminium', 'metal'],\n ['wood', 'wood'],\n ['plywood', 'wood'],\n ['mdf', 'wood'],\n ['glass', 'glass'],\n ['tempered_glass', 'glass'],\n ['ceramic', 'glass'],\n ['concrete', 'masonry'],\n ['brick', 'masonry'],\n ['plaster', 'masonry'],\n ['plastic', 'soft'],\n ['fabric', 'soft'],\n ['paper', 'soft'],\n ['foliage', 'soft'],\n ['soil', 'soft'],\n ['flesh', 'soft'],\n] as const satisfies readonly (readonly [string, string])[]\nexport type SfxMaterial = (typeof SFX_MATERIALS)[number][0]\n\nexport const SFX_BLOWS = ['bullet', 'blunt', 'blade', 'break', 'drag'] as const\nexport type SfxBlow = (typeof SFX_BLOWS)[number]\n\n// the destruction renderer's synthesized sounds by material group (art scene keys), the A side of the matrix\nconst CURRENT_BY_GROUP: Readonly<Record<string, Readonly<Partial<Record<SfxBlow, string>>>>> = {\n metal: { bullet: 'clang', blunt: 'clang', blade: 'clang', break: 'clang', drag: 'drag_metal' },\n wood: { bullet: 'wood_hit', blunt: 'wood_hit', blade: 'wood_hit', break: 'crack', drag: 'drag_wood' },\n glass: { bullet: 'ping', blunt: 'ping', break: 'glass_shatter', drag: 'drag_glass' },\n masonry: { bullet: 'chip', blunt: 'chip', blade: 'chip', break: 'crumble', drag: 'drag_masonry' },\n soft: { bullet: 'thud', blunt: 'thud', blade: 'thud', break: 'thud', drag: 'drag_soft' },\n}\n\n/** The id a file of the matrix takes: `impact_<material>_<blow>` (`impact_concrete_bullet`). */\nexport const impactSoundId = (material: SfxMaterial, blow: SfxBlow): string => `impact_${material}_${blow}`\n\n/** The impact matrix as entries (the `impactos` category of the catalogue). */\nexport const SFX_IMPACTS: readonly SfxEntry[] = SFX_MATERIALS.flatMap(([material, group]) =>\n SFX_BLOWS.map((blow) => {\n const current = CURRENT_BY_GROUP[group]?.[blow]\n return { id: impactSoundId(material, blow), category: 'impactos' as const, priority: (['steel', 'wood', 'glass', 'concrete', 'brick', 'soil', 'flesh'].includes(material) && blow === 'bullet' ? 1 : 2) as SfxPriority, ...(current ? { current } : {}) }\n }),\n)\n\n/** Where an entry stands in an engine: a file registered (`file`), only the synthesized sound (`synth`), or nothing (`missing`). */\nexport type SfxState = 'file' | 'synth' | 'missing'\n\nexport function sfxState(entry: SfxEntry, has: { sample(id: string): unknown; sound(id: string): unknown }): SfxState {\n if (has.sample(entry.id)) return 'file'\n if ((entry.current && has.sound(entry.current)) || has.sound(entry.id)) return 'synth'\n return 'missing'\n}\n","/**\n * The calibration sound of the measuring harness: a sample sound made by the kit in code (no file, no licence), with every layer the criteria ask\n * for, so the harness can prove the whole file path (bytes → decode → plan → WebAudio) before any real file exists. Each take is a WAV built here:\n * an attack on the first sample (a click and a short noise burst over a low thump that decays), four near takes with their own pitch and noise, a far\n * take (darker, slower attack of the echo), light and heavy tiers and the two tails (a short dense room, a long thin field).\n */\nimport { createRng } from '../rng'\nimport type { SampleSound } from '../sample'\nimport type { SampleLoader } from '../webaudio/sample-store'\n\nexport const CALIBRATION_ID = 'calibration_shot'\nexport const CALIBRATION_RATE = 48000\n\n/** 16-bit PCM mono WAV bytes of the samples (clamped to ±1). */\nexport function encodeWav(samples: Float32Array, sampleRate = CALIBRATION_RATE): ArrayBuffer {\n const bytes = new ArrayBuffer(44 + samples.length * 2)\n const view = new DataView(bytes)\n const text = (at: number, value: string): void => {\n for (let index = 0; index < value.length; index++) view.setUint8(at + index, value.charCodeAt(index))\n }\n text(0, 'RIFF')\n view.setUint32(4, 36 + samples.length * 2, true)\n text(8, 'WAVE')\n text(12, 'fmt ')\n view.setUint32(16, 16, true)\n view.setUint16(20, 1, true)\n view.setUint16(22, 1, true)\n view.setUint32(24, sampleRate, true)\n view.setUint32(28, sampleRate * 2, true)\n view.setUint16(32, 2, true)\n view.setUint16(34, 16, true)\n text(36, 'data')\n view.setUint32(40, samples.length * 2, true)\n for (let index = 0; index < samples.length; index++) {\n const value = Math.max(-1, Math.min(1, samples[index] as number))\n view.setInt16(44 + index * 2, value < 0 ? value * 0x8000 : value * 0x7fff, true)\n }\n return bytes\n}\n\ntype Shot = { readonly seconds: number; readonly thump: number; readonly noise: number; readonly decay: number; readonly brightness: number; readonly gain: number; readonly seed: number; readonly attack?: number }\n\n/** One take: a thump (sine glide) and a noise burst, both decaying, smoothed by a one-pole lowpass (`brightness` 0..1), faded out at the end. */\nexport function shotSamples(shot: Shot, sampleRate = CALIBRATION_RATE): Float32Array {\n const random = createRng(shot.seed)\n const length = Math.round(shot.seconds * sampleRate)\n const samples = new Float32Array(length)\n let low = 0\n let phase = 0\n const attack = Math.max(1, Math.round((shot.attack ?? 0) * sampleRate))\n for (let index = 0; index < length; index++) {\n const time = index / sampleRate\n const envelope = Math.exp(-time / shot.decay) * Math.min(1, (index + 1) / attack)\n phase += (2 * Math.PI * shot.thump * (1 + 0.6 * Math.exp(-time * 30))) / sampleRate\n const noise = random() * 2 - 1\n low += shot.brightness * (noise - low)\n const fade = Math.min(1, (length - index) / (0.01 * sampleRate))\n samples[index] = shot.gain * envelope * fade * (0.55 * Math.sin(phase) + shot.noise * low)\n }\n return samples\n}\n\nconst TAKES: Readonly<Record<string, Shot>> = {\n 'close_1.wav': { seconds: 0.4, thump: 150, noise: 1, decay: 0.06, brightness: 0.9, gain: 0.85, seed: 1 },\n 'close_2.wav': { seconds: 0.4, thump: 160, noise: 1, decay: 0.065, brightness: 0.85, gain: 0.85, seed: 2 },\n 'close_3.wav': { seconds: 0.4, thump: 142, noise: 1, decay: 0.055, brightness: 0.95, gain: 0.85, seed: 3 },\n 'close_4.wav': { seconds: 0.4, thump: 170, noise: 1, decay: 0.07, brightness: 0.8, gain: 0.85, seed: 4 },\n 'far_1.wav': { seconds: 0.9, thump: 90, noise: 0.8, decay: 0.2, brightness: 0.08, gain: 0.7, seed: 5, attack: 0.004 },\n 'far_2.wav': { seconds: 0.9, thump: 95, noise: 0.8, decay: 0.22, brightness: 0.07, gain: 0.7, seed: 6, attack: 0.004 },\n 'light_1.wav': { seconds: 0.25, thump: 220, noise: 0.6, decay: 0.03, brightness: 0.6, gain: 0.35, seed: 7 },\n 'light_2.wav': { seconds: 0.25, thump: 230, noise: 0.6, decay: 0.03, brightness: 0.55, gain: 0.35, seed: 8 },\n 'heavy_1.wav': { seconds: 0.6, thump: 110, noise: 1.2, decay: 0.11, brightness: 0.95, gain: 0.95, seed: 9 },\n 'heavy_2.wav': { seconds: 0.6, thump: 105, noise: 1.2, decay: 0.12, brightness: 0.9, gain: 0.95, seed: 10 },\n 'tail_in.wav': { seconds: 0.5, thump: 0, noise: 1, decay: 0.08, brightness: 0.4, gain: 0.25, seed: 11, attack: 0.01 },\n 'tail_out.wav': { seconds: 2, thump: 0, noise: 1, decay: 0.45, brightness: 0.25, gain: 0.2, seed: 12, attack: 0.02 },\n 'loop_1.wav': { seconds: 0.6, thump: 0, noise: 1, decay: 100, brightness: 0.5, gain: 0.5, seed: 13, attack: 0.01 },\n 'loop_2.wav': { seconds: 0.6, thump: 0, noise: 1, decay: 100, brightness: 0.45, gain: 0.5, seed: 14, attack: 0.01 },\n 'loop_3.wav': { seconds: 0.6, thump: 0, noise: 1, decay: 100, brightness: 0.55, gain: 0.5, seed: 15, attack: 0.01 },\n}\n\nconst PREFIX = 'calibration/'\nconst url = (name: string): string => `${PREFIX}${name}`\n\n/** The calibration sound (weapons bus, every layer, fallback to the game's pistol noise class). */\nexport const CALIBRATION_SOUND: SampleSound = {\n id: CALIBRATION_ID,\n bus: 'weapons',\n close: ['close_1.wav', 'close_2.wav', 'close_3.wav', 'close_4.wav'].map((name) => ({ url: url(name) })),\n far: ['far_1.wav', 'far_2.wav'].map((name) => ({ url: url(name) })),\n tiers: [\n { name: 'light', from: 0, close: ['light_1.wav', 'light_2.wav'].map((name) => ({ url: url(name) })) },\n { name: 'medium', from: 0.34, close: ['close_1.wav', 'close_2.wav', 'close_3.wav', 'close_4.wav'].map((name) => ({ url: url(name) })) },\n { name: 'heavy', from: 0.67, close: ['heavy_1.wav', 'heavy_2.wav'].map((name) => ({ url: url(name) })) },\n ],\n tail: { indoor: [{ url: url('tail_in.wav') }], outdoor: [{ url: url('tail_out.wav') }] },\n pitch: 0.04,\n choke: { voices: 2 },\n fallback: 'sfx_pistol',\n sources: ['sdk: calibration takes synthesized by the harness (measure/calibration.ts), no file, no licence'],\n}\n\nexport const CALIBRATION_LOOP_ID = 'calibration_loop'\n\n/** A steady hiss in three 0.6 s takes, crossed in 0.15 s: the loop the harness checks for seams (no dip, no gap where two takes meet). */\nexport const CALIBRATION_LOOP_SOUND: SampleSound = {\n id: CALIBRATION_LOOP_ID,\n bus: 'ambience',\n close: ['loop_1.wav', 'loop_2.wav', 'loop_3.wav'].map((name) => ({ url: url(name) })),\n loop: { crossfade: 0.15 },\n sources: ['sdk: calibration takes synthesized by the harness (measure/calibration.ts), no file, no licence'],\n}\n\n/** Whether a url is one of the calibration takes. */\nexport const isCalibrationUrl = (url: string): boolean => url.startsWith(PREFIX)\n\n/** Bytes of the calibration takes (made on demand, the same for the same name). Falls through to `next` for any other url. */\nexport function calibrationLoader(next?: SampleLoader): SampleLoader {\n const made = new Map<string, ArrayBuffer>()\n return async (requested) => {\n if (!requested.startsWith(PREFIX)) {\n if (next) return next(requested)\n throw new Error(`not a calibration file: ${requested}`)\n }\n const name = requested.slice(PREFIX.length)\n const shot = TAKES[name]\n if (!shot) throw new Error(`no calibration take ${name}`)\n let bytes = made.get(name)\n if (!bytes) {\n bytes = encodeWav(shotSamples(shot))\n made.set(name, bytes)\n }\n // decodeAudioData detaches what it gets: every load gets its own copy\n return bytes.slice(0)\n }\n}\n","/**\n * Measurements of rendered sound (pure, on plain sample arrays), for the criteria C1–C9: when it starts, how loud and how bright it is, which side\n * it is on, how long it rings and how many separate hits a burst has.\n */\n\nexport type Channels = readonly Float32Array[]\n\n/** Linear amplitude to dBFS (−Infinity for 0). */\nexport const dbOf = (amplitude: number): number => (amplitude > 0 ? 20 * Math.log10(amplitude) : -Infinity)\n\nconst clampIndex = (channels: Channels, index: number): number => Math.max(0, Math.min(channels[0]?.length ?? 0, Math.round(index)))\n\n/** The loudest sample of every channel between two sample indexes (linear). */\nexport function peakOf(channels: Channels, start = 0, end = Number.POSITIVE_INFINITY): number {\n let peak = 0\n const from = clampIndex(channels, start)\n for (const channel of channels) {\n const to = Math.min(channel.length, end)\n for (let index = from; index < to; index++) {\n const value = Math.abs(channel[index] as number)\n if (value > peak) peak = value\n }\n }\n return peak\n}\n\n/** Index of the loudest sample (any channel) between two indexes. */\nexport function peakIndex(channels: Channels, start = 0, end = Number.POSITIVE_INFINITY): number {\n let peak = -1\n let at = -1\n const from = clampIndex(channels, start)\n for (const channel of channels) {\n const to = Math.min(channel.length, end)\n for (let index = from; index < to; index++) {\n const value = Math.abs(channel[index] as number)\n if (value > peak) {\n peak = value\n at = index\n }\n }\n }\n return at\n}\n\n/** First sample index at or after `start` whose amplitude (any channel) reaches `threshold`, or −1. */\nexport function onsetIndex(channels: Channels, start = 0, threshold = 0.001): number {\n const length = channels[0]?.length ?? 0\n for (let index = clampIndex(channels, start); index < length; index++) {\n for (const channel of channels) if (Math.abs(channel[index] as number) >= threshold) return index\n }\n return -1\n}\n\n/** RMS of all channels between two indexes, dBFS. */\nexport function rmsDb(channels: Channels, start: number, end: number): number {\n let sum = 0\n let count = 0\n const from = clampIndex(channels, start)\n for (const channel of channels) {\n const to = Math.min(channel.length, Math.round(end))\n for (let index = from; index < to; index++) {\n const value = channel[index] as number\n sum += value * value\n count++\n }\n }\n return count === 0 ? -Infinity : dbOf(Math.sqrt(sum / count))\n}\n\n/** Spectral centroid (Hz) of `size` samples from `start` (channels summed, Hann window, plain DFT): the brightness of the sound. */\nexport function centroidHz(channels: Channels, sampleRate: number, start: number, size = 2048): number {\n const from = clampIndex(channels, start)\n const frame = new Float64Array(size)\n for (const channel of channels) {\n for (let index = 0; index < size; index++) frame[index] = (frame[index] as number) + ((channel[from + index] as number | undefined) ?? 0)\n }\n for (let index = 0; index < size; index++) frame[index] = (frame[index] as number) * (0.5 - 0.5 * Math.cos((2 * Math.PI * index) / (size - 1)))\n let weighted = 0\n let total = 0\n const bins = size / 2\n for (let bin = 1; bin < bins; bin++) {\n let re = 0\n let im = 0\n const step = (2 * Math.PI * bin) / size\n for (let index = 0; index < size; index++) {\n const value = frame[index] as number\n re += value * Math.cos(step * index)\n im -= value * Math.sin(step * index)\n }\n const magnitude = Math.hypot(re, im)\n weighted += magnitude * ((bin * sampleRate) / size)\n total += magnitude\n }\n return total > 0 ? weighted / total : 0\n}\n\n/** Energy balance from −1 (all left) to 1 (all right) between two indexes. */\nexport function balanceOf(left: Float32Array, right: Float32Array, start: number, end: number): number {\n let l = 0\n let r = 0\n const to = Math.min(left.length, right.length, Math.round(end))\n for (let index = Math.max(0, Math.round(start)); index < to; index++) {\n l += (left[index] as number) ** 2\n r += (right[index] as number) ** 2\n }\n return l + r > 0 ? (r - l) / (r + l) : 0\n}\n\n/** Seconds from `start` to the last 10 ms window louder than `floorDb` (how long the sound rings, tail included). */\nexport function ringSeconds(channels: Channels, sampleRate: number, start: number, floorDb = -60): number {\n const window = Math.max(1, Math.round(sampleRate * 0.01))\n const length = channels[0]?.length ?? 0\n let last = clampIndex(channels, start)\n for (let index = last; index < length; index += window) {\n if (dbOf(peakOf(channels, index, index + window)) > floorDb) last = index + window\n }\n return (last - start) / sampleRate\n}\n\n/**\n * Separate hits in a burst: attacks of the 2 ms envelope, where it jumps to twice the mean of the 20 ms before it and reaches `threshold`, at least\n * 30 ms apart (so a rapid fire over a long tail still counts every shot).\n */\nexport function countHits(channels: Channels, sampleRate: number, threshold: number): number {\n const window = Math.max(1, Math.round(sampleRate * 0.002))\n const length = channels[0]?.length ?? 0\n const envelope: number[] = []\n for (let index = 0; index < length; index += window) envelope.push(peakOf(channels, index, index + window))\n const history = 10\n const refractory = Math.round(0.03 / 0.002)\n let hits = 0\n let quietUntil = -1\n for (let index = 0; index < envelope.length; index++) {\n const level = envelope[index] as number\n if (index < quietUntil || level < threshold) continue\n let sum = 0\n for (let back = Math.max(0, index - history); back < index; back++) sum += envelope[back] as number\n const before = index === 0 ? 0 : sum / Math.min(history, index)\n if (level >= 2 * before) {\n hits++\n quietUntil = index + refractory\n }\n }\n return hits\n}\n\n/** Peak of the last `seconds` of a file against its own peak, dB (≤ −40: it fades out, it does not end on a cut). */\nexport function endDropDb(channels: Channels, sampleRate: number, seconds = 0.005): number {\n const length = channels[0]?.length ?? 0\n const whole = peakOf(channels)\n if (whole === 0) return -Infinity\n return dbOf(peakOf(channels, length - Math.round(sampleRate * seconds), length) / whole)\n}\n","/**\n * The measuring harness of the sound criteria (C1–C10 of the sound checklist). Each criterion plays the sound through a real engine on an\n * `OfflineAudioContext` (the same mix, limiter and realizer the game uses, rendered faster than real time), and measures the rendered output: when it\n * starts against the event, how loud and bright it is at each distance, which side, how many takes, how it ends. Runs in a browser (or anything with\n * an `OfflineAudioContext`); C10 is the owner's ear, so it is left open with a recording to listen to (`recordOutput`).\n */\nimport type { MixProfile } from '../mix'\nimport type { SoundBank } from '../recipe'\nimport { createRng } from '../rng'\nimport { planSample, sampleUrls } from '../sample'\nimport type { SampleEnvironment, SampleSound } from '../sample'\nimport { createAudioEngine } from '../webaudio/engine'\nimport type { AudioEngine, PlayOptions, VoiceStartEvent } from '../webaudio/engine'\nimport type { SampleLoader } from '../webaudio/sample-store'\nimport { balanceOf, centroidHz, dbOf, endDropDb, onsetIndex, peakIndex, peakOf, ringSeconds, rmsDb } from './analyze'\nimport type { Channels } from './analyze'\n\nexport const CRITERIA = ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9', 'C10'] as const\nexport type CriterionId = (typeof CRITERIA)[number]\n\n/** The targets of the checklist, as numbers the harness compares with. */\nexport const CRITERIA_TARGETS = {\n /** C1: event to the first sample, and onset to the peak (one 60 fps frame). */\n syncMs: 20,\n peakAfterOnsetMs: 1000 / 60,\n /** C3: distances measured, metres. */\n distances: [1, 10, 50, 200] as readonly number[],\n /** C5: plays, distinct takes and the pitch range (fraction). */\n plays: 10,\n minTakes: 4,\n pitch: [0.03, 0.05] as const,\n /** C6: the highest peak, dBFS. */\n peakDb: -1,\n /** C8: the end of each file against its peak, dB. */\n endDropDb: -40,\n /** C9: a burst of rapid fire. */\n burst: 20,\n burstSpacing: 0.06,\n} as const\n\nexport type CriterionResult = {\n readonly id: CriterionId\n /** `null`: not decided by the harness (C10, the owner's ear). */\n readonly pass: boolean | null\n /** What was measured, short (the bench prints it). */\n readonly value: string\n readonly target: string\n readonly data?: Readonly<Record<string, unknown>>\n}\n\n/** One action of the game and the sound it must play (C2). */\nexport type ActionCheck = { readonly action: string; readonly play: string; readonly expect: string; readonly options?: PlayOptions }\n\nexport type MeasureOptions = {\n /** The sample sound measured. */\n readonly sound: string\n readonly banks: readonly SoundBank[]\n readonly loader: SampleLoader\n /** The mixing chain the game uses (default the engine's default). */\n readonly mix?: string | MixProfile\n readonly sampleRate?: number\n /** `new OfflineAudioContext(...)` by default. */\n readonly createOfflineContext?: (channels: number, length: number, sampleRate: number) => OfflineAudioContext\n /** The live context's `baseLatency` (s), added to the rendered delay for C1. */\n readonly baseLatency?: number\n /** The device's `outputLatency` (s), reported with C1 but not counted (it is the speakers', not the game's). */\n readonly outputLatency?: number\n /** C2: the actions to check (default the sound for itself). */\n readonly actions?: readonly ActionCheck[]\n /** C6: sound ids loudest first, the order the buses must keep (tiro > explosão > impacto > passo). */\n readonly loudnessOrder?: readonly string[]\n}\n\ntype Render = { readonly channels: Channels; readonly sampleRate: number; readonly events: readonly VoiceStartEvent[]; readonly handles: number }\n\nconst EVENT_AT = 0.2\n\nconst offlineFactory = (options: MeasureOptions) =>\n options.createOfflineContext ??\n ((channels: number, length: number, sampleRate: number): OfflineAudioContext => {\n const Constructor = (globalThis as { OfflineAudioContext?: new (channels: number, length: number, sampleRate: number) => OfflineAudioContext }).OfflineAudioContext\n if (!Constructor) throw new Error('no OfflineAudioContext here: the harness runs in a browser')\n return new Constructor(channels, length, sampleRate)\n })\n\n/** Renders what `act` plays on a fresh engine (files loaded first), `seconds` long. */\nasync function render(options: MeasureOptions, seconds: number, act: (engine: AudioEngine) => number, engineExtras: { occlusion?: () => number; environment?: SampleEnvironment } = {}): Promise<Render> {\n const sampleRate = options.sampleRate ?? 48000\n const context = offlineFactory(options)(2, Math.ceil(seconds * sampleRate), sampleRate)\n const events: VoiceStartEvent[] = []\n const engine = createAudioEngine({\n banks: options.banks,\n createContext: () => context as unknown as AudioContext,\n samples: { loader: options.loader, autoLoad: false },\n onVoiceStart: (event) => events.push(event),\n ...(options.mix ? { mix: options.mix } : {}),\n ...(engineExtras.occlusion ? { occlusion: engineExtras.occlusion } : {}),\n ...(engineExtras.environment ? { environment: engineExtras.environment } : {}),\n })\n const ids = [...new Set([options.sound, ...(options.actions ?? []).map((action) => action.play), ...(options.loudnessOrder ?? [])])].filter((id) => engine.registry.sample(id))\n const report = await engine.loadSamples(ids)\n if (report.failed.length > 0) throw new Error(`files did not load: ${report.failed.map((entry) => `${entry.url} (${entry.error})`).join(', ')}`)\n const handles = act(engine)\n const buffer = await context.startRendering()\n const channels = Array.from({ length: buffer.numberOfChannels }, (_, index) => buffer.getChannelData(index))\n return { channels, sampleRate, events, handles }\n}\n\nconst ms = (seconds: number): string => `${(seconds * 1000).toFixed(1)} ms`\nconst db = (value: number): string => (Number.isFinite(value) ? `${value.toFixed(1)} dB` : '−∞')\nconst decreasing = (values: readonly number[]): boolean => values.every((value, index) => index === 0 || value < (values[index - 1] as number))\n\nasync function c1(options: MeasureOptions): Promise<CriterionResult> {\n const result = await render(options, 1.2, (engine) => (engine.play(options.sound, { distance: 1, delay: EVENT_AT }) ? 1 : 0))\n const onset = onsetIndex(result.channels, 0, 0.001)\n const event = result.events.find((entry) => entry.id === options.sound)\n if (onset < 0 || !event) return { id: 'C1', pass: false, value: 'sem som', target: `≤ ${CRITERIA_TARGETS.syncMs} ms` }\n const delay = onset / result.sampleRate - EVENT_AT + (event.startAt - event.requestedAt - EVENT_AT) + (options.baseLatency ?? 0)\n const peak = peakIndex(result.channels, onset, onset + Math.round(result.sampleRate * 0.25))\n const toPeak = (peak - onset) / result.sampleRate\n const pass = delay <= CRITERIA_TARGETS.syncMs / 1000 && toPeak <= CRITERIA_TARGETS.peakAfterOnsetMs / 1000\n return {\n id: 'C1',\n pass,\n value: `${ms(delay)}, pico +${ms(toPeak)}`,\n target: `≤ ${CRITERIA_TARGETS.syncMs} ms; pico no quadro do contato`,\n data: { delay, toPeak, baseLatency: options.baseLatency ?? 0, outputLatency: options.outputLatency ?? 0 },\n }\n}\n\nasync function c2(options: MeasureOptions): Promise<CriterionResult> {\n const actions = options.actions ?? [{ action: options.sound, play: options.sound, expect: options.sound }]\n const result = await render(options, 0.5, (engine) => {\n let played = 0\n for (const action of actions) if (engine.play(action.play, { distance: 1, ...action.options })) played++\n return played\n })\n const wrong = actions.filter((action, index) => {\n const event = result.events[index]\n return !event || (event.fallbackOf ?? event.id) !== action.expect || event.kind !== 'sample'\n })\n return {\n id: 'C2',\n pass: wrong.length === 0,\n value: `${actions.length - wrong.length}/${actions.length} certas`,\n target: '100% das ações batem com a tabela (arquivo, não o substituto)',\n data: { wrong: wrong.map((action) => action.action), events: result.events.map((event) => `${event.kind}:${event.id}`) },\n }\n}\n\nasync function c3(options: MeasureOptions, sound: SampleSound): Promise<CriterionResult> {\n const levels: number[] = []\n const brightness: number[] = []\n for (const distance of CRITERIA_TARGETS.distances) {\n const result = await render(options, 1.2, (engine) => (engine.play(options.sound, { position: [0, 0, -distance], seed: 1, delay: EVENT_AT }) ? 1 : 0))\n const onset = onsetIndex(result.channels, 0, 1e-5)\n levels.push(onset < 0 ? -Infinity : rmsDb(result.channels, onset, onset + result.sampleRate * 0.3))\n brightness.push(onset < 0 ? 0 : centroidHz(result.channels, result.sampleRate, onset))\n }\n const far = planSample(sound, { random: createRng(1), distance: CRITERIA_TARGETS.distances.at(-1) as number })\n const switched = (far?.farMix ?? 0) === 1 && (far?.layers.some((layer) => layer.role === 'far') ?? false)\n const pass = decreasing(levels) && decreasing(brightness) && switched\n return {\n id: 'C3',\n pass,\n value: `${levels.map((level) => db(level)).join(' › ')}; ${brightness.map((hz) => `${Math.round(hz)} Hz`).join(' › ')}${switched ? '; longe = far' : '; sem camada far'}`,\n target: 'cai e escurece com a distância; troca para a versão de longe depois de ~50 m',\n data: { distances: CRITERIA_TARGETS.distances, levels, brightness, switched },\n }\n}\n\nasync function c4(options: MeasureOptions): Promise<CriterionResult> {\n const side = async (x: number): Promise<number> => {\n const result = await render(options, 0.8, (engine) => (engine.play(options.sound, { position: [x, 0, -2], seed: 1, delay: EVENT_AT }) ? 1 : 0))\n const [left, right] = result.channels as [Float32Array, Float32Array]\n return balanceOf(left, right, 0, left.length)\n }\n const right = await side(10)\n const left = await side(-10)\n const bright = async (occluded: boolean): Promise<number> => {\n const result = await render(options, 0.8, (engine) => (engine.play(options.sound, { position: [0, 0, -10], seed: 1, delay: EVENT_AT }) ? 1 : 0), occluded ? { occlusion: () => 1 } : {})\n const onset = onsetIndex(result.channels, 0, 1e-5)\n return onset < 0 ? 0 : centroidHz(result.channels, result.sampleRate, onset)\n }\n const clear = await bright(false)\n const wall = await bright(true)\n const pass = right > 0.2 && left < -0.2 && wall < clear * 0.7\n return {\n id: 'C4',\n pass,\n value: `dir ${right.toFixed(2)} esq ${left.toFixed(2)}; parede ${Math.round(wall)} Hz de ${Math.round(clear)} Hz`,\n target: 'lado certo; atrás da parede fica abafado',\n data: { right, left, clear, wall },\n }\n}\n\nasync function c5(options: MeasureOptions, sound: SampleSound): Promise<CriterionResult> {\n const result = await render(options, 0.3 * CRITERIA_TARGETS.plays + 1, (engine) => {\n let played = 0\n for (let index = 0; index < CRITERIA_TARGETS.plays; index++) if (engine.play(options.sound, { distance: 1, delay: 0.1 + index * 0.3 })) played++\n return played\n })\n const close = result.events.map((event) => event.layers?.find((layer) => layer.role === 'close'))\n const takes = close.map((layer) => layer?.url ?? '')\n const repeats = takes.filter((take, index) => index > 0 && take === takes[index - 1]).length\n const distinct = new Set(takes).size\n const rates = close.map((layer) => layer?.rate ?? 1)\n const spread = (Math.max(...rates) - Math.min(...rates)) / 2\n const pitch = sound.pitch ?? 0.04\n const pass = distinct >= CRITERIA_TARGETS.minTakes && repeats === 0 && pitch >= CRITERIA_TARGETS.pitch[0] && pitch <= CRITERIA_TARGETS.pitch[1] && spread > 0.01\n return {\n id: 'C5',\n pass,\n value: `${distinct} tomadas, ${repeats} repetições seguidas, tom ±${(pitch * 100).toFixed(0)}%`,\n target: `≥ ${CRITERIA_TARGETS.minTakes} variações, nunca a mesma duas vezes, tom ±3–5%`,\n data: { takes, rates },\n }\n}\n\nasync function c6(options: MeasureOptions): Promise<CriterionResult> {\n const peakAt = async (id: string): Promise<{ peak: number; rms: number }> => {\n const result = await render(options, 1.2, (engine) => (engine.play(id, { distance: 1, seed: 1, delay: EVENT_AT }) ? 1 : 0))\n const onset = onsetIndex(result.channels, 0, 1e-5)\n return { peak: dbOf(peakOf(result.channels)), rms: onset < 0 ? -Infinity : rmsDb(result.channels, onset, onset + result.sampleRate * 0.3) }\n }\n const own = await peakAt(options.sound)\n const order = options.loudnessOrder ?? []\n const loudness: number[] = []\n for (const id of order) loudness.push((await peakAt(id)).rms)\n const ordered = order.length < 2 || decreasing(loudness)\n const pass = own.peak <= CRITERIA_TARGETS.peakDb && ordered\n return {\n id: 'C6',\n pass,\n value: `pico ${db(own.peak)}, RMS ${db(own.rms)}${order.length > 1 ? `; ordem ${ordered ? 'ok' : 'errada'}` : ''}`,\n target: 'tiro > explosão > impacto > passo; pico ≤ −1 dBFS',\n data: { peak: own.peak, rms: own.rms, order, loudness },\n }\n}\n\nasync function c7(options: MeasureOptions, sound: SampleSound): Promise<CriterionResult> {\n if (!sound.tiers || sound.tiers.length < 2) return { id: 'C7', pass: false, value: 'sem camadas leve/médio/forte', target: 'muda de camada, não só de volume' }\n const at = async (intensity: number): Promise<{ tier: string; rms: number; files: string }> => {\n const result = await render(options, 1.2, (engine) => (engine.play(options.sound, { distance: 1, seed: 1, intensity, delay: EVENT_AT }) ? 1 : 0))\n const onset = onsetIndex(result.channels, 0, 1e-5)\n const event = result.events[0]\n return { tier: event?.tier ?? '', rms: onset < 0 ? -Infinity : rmsDb(result.channels, onset, onset + result.sampleRate * 0.3), files: event?.layers?.find((layer) => layer.role === 'close')?.url ?? '' }\n }\n const light = await at(0.1)\n const heavy = await at(0.9)\n const pass = light.tier !== heavy.tier && light.files !== heavy.files && heavy.rms > light.rms\n return {\n id: 'C7',\n pass,\n value: `${light.tier} ${db(light.rms)} → ${heavy.tier} ${db(heavy.rms)}`,\n target: 'golpe fraco x forte troca a camada (leve/médio/forte), não só o volume',\n data: { light, heavy },\n }\n}\n\nasync function c8(options: MeasureOptions, sound: SampleSound): Promise<CriterionResult> {\n const sampleRate = options.sampleRate ?? 48000\n const decoder = offlineFactory(options)(1, sampleRate, sampleRate)\n const drops: { url: string; drop: number }[] = []\n for (const url of sampleUrls(sound)) {\n const buffer = await decoder.decodeAudioData(await options.loader(url))\n drops.push({ url, drop: endDropDb([buffer.getChannelData(0)], buffer.sampleRate) })\n }\n const worst = drops.reduce((max, entry) => Math.max(max, entry.drop), -Infinity)\n const ring = async (environment: SampleEnvironment): Promise<number> => {\n const result = await render(options, 3, (engine) => (engine.play(options.sound, { distance: 1, seed: 1, environment, delay: EVENT_AT }) ? 1 : 0))\n return ringSeconds(result.channels, result.sampleRate, Math.round(EVENT_AT * result.sampleRate))\n }\n const indoor = await ring('indoor')\n const outdoor = await ring('outdoor')\n const hasTails = Boolean(sound.tail?.indoor?.length && sound.tail?.outdoor?.length)\n const pass = worst <= CRITERIA_TARGETS.endDropDb && hasTails && outdoor > indoor\n return {\n id: 'C8',\n pass,\n value: `fim ${db(worst)}; cauda dentro ${indoor.toFixed(2)} s, fora ${outdoor.toFixed(2)} s${hasTails ? '' : ' (sem as duas caudas)'}`,\n target: 'não corta seco; cauda de interior x exterior coerente',\n data: { drops, indoor, outdoor },\n }\n}\n\nasync function c9(options: MeasureOptions): Promise<CriterionResult> {\n const { burst, burstSpacing } = CRITERIA_TARGETS\n const first = 0.1\n const result = await render(options, burst * burstSpacing + 1.5, (engine) => {\n let played = 0\n for (let index = 0; index < burst; index++) if (engine.play(options.sound, { distance: 1, delay: first + index * burstSpacing })) played++\n return played\n })\n const peak = dbOf(peakOf(result.channels))\n // C9 reads: no pile-up (the level does not build up shot after shot) and no shot that vanishes (every slot carries its shot). RMS of each shot's slot:\n const slots: number[] = []\n for (let index = 0; index < burst; index++) {\n const at = Math.round((first + index * burstSpacing) * result.sampleRate)\n slots.push(rmsDb(result.channels, at, at + Math.round(burstSpacing * result.sampleRate)))\n }\n const sorted = [...slots].sort((a, b) => a - b)\n const median = sorted[Math.floor(sorted.length / 2)] as number\n const vanished = slots.filter((level) => level < median - 6).length\n const buildUp = slots.slice(-5).reduce((sum, level) => sum + level, 0) / 5 - (slots[0] as number)\n const pass = result.handles === burst && vanished === 0 && buildUp <= 6 && peak <= CRITERIA_TARGETS.peakDb\n return {\n id: 'C9',\n pass,\n value: `${result.handles}/${burst} tocaram, ${vanished} sumiram, acúmulo ${buildUp >= 0 ? '+' : ''}${buildUp.toFixed(1)} dB, pico ${db(peak)}`,\n target: 'rajada sem amontoar (acúmulo ≤ 6 dB) nem sumir; pico ≤ −1 dBFS',\n data: { handles: result.handles, slots, vanished, buildUp, peak },\n }\n}\n\n/** Runs the criteria (all by default) and returns one result each, in order. C10 is always open: it is the owner's ear on a recording. */\nexport async function measureCriteria(options: MeasureOptions, only: readonly CriterionId[] = CRITERIA): Promise<CriterionResult[]> {\n const probe = createAudioEngine({ banks: options.banks, createContext: () => null })\n const sound = probe.registry.sample(options.sound)\n if (!sound) throw new Error(`${options.sound} is not a sample sound of these banks`)\n const results: CriterionResult[] = []\n for (const id of only) {\n if (id === 'C1') results.push(await c1(options))\n else if (id === 'C2') results.push(await c2(options))\n else if (id === 'C3') results.push(await c3(options, sound))\n else if (id === 'C4') results.push(await c4(options))\n else if (id === 'C5') results.push(await c5(options, sound))\n else if (id === 'C6') results.push(await c6(options))\n else if (id === 'C7') results.push(await c7(options, sound))\n else if (id === 'C8') results.push(await c8(options, sound))\n else if (id === 'C9') results.push(await c9(options))\n else results.push({ id: 'C10', pass: null, value: 'gravar e ouvir', target: 'aprovado pelo ouvido do dono (print + gravação na 4280)' })\n }\n return results\n}\n\n/**\n * Records what the engine outputs while `act` plays, `seconds` long (the C10 recording): a `MediaRecorder` on a stream tapped from the master. Returns\n * `null` where there is no recorder or no context.\n */\nexport async function recordOutput(engine: AudioEngine, seconds: number, act: () => void): Promise<Blob | null> {\n engine.unlock()\n const output = engine.output()\n const Recorder = (globalThis as { MediaRecorder?: typeof MediaRecorder }).MediaRecorder\n const context = output?.context as AudioContext | undefined\n if (!output || !Recorder || !context || typeof context.createMediaStreamDestination !== 'function') return null\n const tap = context.createMediaStreamDestination()\n output.connect(tap)\n const recorder = new Recorder(tap.stream)\n const chunks: Blob[] = []\n recorder.ondataavailable = (event) => chunks.push(event.data)\n const done = new Promise<void>((resolve) => {\n recorder.onstop = () => resolve()\n })\n recorder.start()\n act()\n await new Promise((resolve) => setTimeout(resolve, seconds * 1000))\n recorder.stop()\n await done\n output.disconnect(tap)\n return new Blob(chunks, { type: recorder.mimeType || 'audio/webm' })\n}\n","import type { SfxBlow, SfxMaterial, SfxState } from '../catalog'\nimport type { CriterionId } from '../measure/criteria'\nimport type { SampleEnvironment } from '../sample'\n\ntype Strength = 'light' | 'medium' | 'heavy'\n\nexport type SoundCheckText = {\n readonly group: string\n readonly name: string\n readonly controls: Readonly<\n Record<\n | 'category'\n | 'sound'\n | 'play'\n | 'material'\n | 'blow'\n | 'strike'\n | 'distance'\n | 'bearing'\n | 'environment'\n | 'strength'\n | 'wall'\n | 'burst'\n | 'play_a'\n | 'play_b'\n | 'ab_cycle'\n | 'blind'\n | 'reveal'\n | 'prefer_a'\n | 'prefer_b'\n | 'measure'\n | 'record'\n | 'approve',\n string\n >\n >\n readonly hints: Readonly<Record<'sound' | 'strike' | 'wall' | 'burst' | 'ab_cycle' | 'blind' | 'measure' | 'record' | 'approve', string>>\n readonly categories: Readonly<Record<string, string>>\n readonly blows: Readonly<Record<SfxBlow, string>>\n readonly materials: Readonly<Record<SfxMaterial, string>>\n readonly environments: Readonly<Record<SampleEnvironment, string>>\n readonly strengths: Readonly<Record<Strength, string>>\n readonly states: Readonly<Record<SfxState, string>>\n readonly criteria: Readonly<Record<CriterionId, string>>\n readonly metrics: Readonly<Record<'chosen' | 'matrix' | 'last' | 'ab' | 'blind' | 'verdicts' | 'state' | 'count', string>>\n readonly run: Readonly<Record<'idle' | 'measuring' | 'done' | 'failed' | 'recording' | 'recorded', string>>\n readonly words: Readonly<Record<'none' | 'hidden' | 'prefers' | 'approved', string>>\n readonly probe: string\n readonly wall: string\n readonly scenarios: Readonly<Record<'pipeline' | 'criteria' | 'loop' | 'panel', string>>\n}\n\nconst PT: SoundCheckText = {\n group: 'Som',\n name: 'Painel de som (arquivos e C1–C10)',\n controls: {\n category: '1 · Categoria',\n sound: '1 · Som',\n play: '1 · ▶ Tocar',\n material: '2 · Material',\n blow: '2 · Golpe',\n strike: '2 · ▶ Tocar golpe',\n distance: '3 · Distância',\n bearing: '3 · Direção',\n environment: '3 · Lugar',\n strength: '3 · Força',\n wall: '3 · Atrás da parede',\n burst: '3 · Rajada 20×',\n play_a: '4 · ▶ A (atual)',\n play_b: '4 · ▶ B (novo)',\n ab_cycle: '4 · Alternar A/B',\n blind: '4 · Às cegas (X)',\n reveal: '4 · Revelar X',\n prefer_a: '4 · Prefiro A',\n prefer_b: '4 · Prefiro B',\n measure: '5 · Medir C1–C10',\n record: '5 · Gravar 3 s',\n approve: '5 · Aprovo (C10)',\n },\n hints: {\n sound: 'A = arquivo, S = sintetizado, F = faltando. O que tem arquivo toca o arquivo; o resto toca o som atual.',\n strike: 'Toca impact_<material>_<golpe>: o arquivo se existir, senão o som atual daquele material.',\n wall: 'Põe a parede de casa entre você e a fonte: o som fica abafado (C4).',\n burst: 'Vinte vezes a cada 60 ms, como uma rajada (C9).',\n ab_cycle: 'A, B, A, B, um por segundo.',\n blind: 'Toca A ou B sem dizer qual; depois revele.',\n measure: 'Mede o som escolhido no motor real (contexto offline): sincronia, som certo, distância, lado, variação, volume, força, cauda e rajada.',\n record: 'Grava 3 s da saída com o som tocando e baixa o arquivo (C10).',\n approve: 'Marca o C10 do som escolhido como aprovado pelo seu ouvido.',\n },\n categories: {\n armas: 'Armas',\n impactos: 'Impactos',\n 'explosoes-fogo': 'Explosões e fogo',\n personagem: 'Personagem',\n veiculos: 'Veículos',\n 'itens-portas': 'Itens e portas',\n ambiente: 'Ambiente',\n calibracao: 'Calibração (SDK)',\n },\n blows: { bullet: 'tiro', blunt: 'soco/chute', blade: 'faca', break: 'quebra', drag: 'arrasto/queda' },\n materials: {\n steel: 'aço',\n aluminium: 'alumínio',\n wood: 'madeira',\n plywood: 'compensado',\n mdf: 'MDF',\n glass: 'vidro',\n tempered_glass: 'vidro temperado',\n ceramic: 'cerâmica',\n concrete: 'concreto',\n brick: 'tijolo',\n plaster: 'gesso',\n plastic: 'plástico',\n fabric: 'tecido',\n paper: 'papelão',\n foliage: 'folhagem',\n soil: 'terra',\n flesh: 'carne',\n },\n environments: { indoor: 'interior', outdoor: 'exterior' },\n strengths: { light: 'leve', medium: 'médio', heavy: 'forte' },\n states: { file: 'A', synth: 'S', missing: 'F' },\n criteria: { C1: 'C1 Sincronia', C2: 'C2 Certo p/ ação', C3: 'C3 Distância', C4: 'C4 Posição', C5: 'C5 Variação', C6: 'C6 Volume', C7: 'C7 Intensidade', C8: 'C8 Cauda', C9: 'C9 Rajada', C10: 'C10 Escuta' },\n metrics: { chosen: '1 · Escolhido', matrix: '2 · Matriz', last: 'Último tocado', ab: '4 · A × B', blind: '4 · X', verdicts: '4 · Preferências', state: '5 · Medição', count: '1 · Catálogo' },\n run: { idle: 'parada', measuring: 'medindo…', done: 'pronta', failed: 'falhou', recording: 'gravando…', recorded: 'gravação baixada' },\n words: { none: '—', hidden: '? (revele)', prefers: 'prefere', approved: 'aprovado pelo dono' },\n probe: 'Fonte do som',\n wall: 'Parede (oclusão)',\n scenarios: { pipeline: 'Arquivo → decodifica → toca (round-robin, cauda)', criteria: 'Critérios C1–C9 no som de calibração', loop: 'Loop de arquivo: emenda sem buraco (crossfade)', panel: 'Painel: categorias, matriz, A/B e parede' },\n}\n\nconst EN: SoundCheckText = {\n ...PT,\n group: 'Sound',\n name: 'Sound panel (files and C1–C10)',\n controls: {\n category: '1 · Category',\n sound: '1 · Sound',\n play: '1 · ▶ Play',\n material: '2 · Material',\n blow: '2 · Blow',\n strike: '2 · ▶ Play blow',\n distance: '3 · Distance',\n bearing: '3 · Bearing',\n environment: '3 · Place',\n strength: '3 · Strength',\n wall: '3 · Behind the wall',\n burst: '3 · Burst 20×',\n play_a: '4 · ▶ A (current)',\n play_b: '4 · ▶ B (new)',\n ab_cycle: '4 · Alternate A/B',\n blind: '4 · Blind (X)',\n reveal: '4 · Reveal X',\n prefer_a: '4 · Prefer A',\n prefer_b: '4 · Prefer B',\n measure: '5 · Measure C1–C10',\n record: '5 · Record 3 s',\n approve: '5 · Approve (C10)',\n },\n hints: {\n sound: 'A = file, S = synthesized, F = missing. A file plays the file; the rest plays the current sound.',\n strike: 'Plays impact_<material>_<blow>: the file if there is one, else the current sound of that material.',\n wall: 'Puts the house wall between you and the source: the sound is muffled (C4).',\n burst: 'Twenty times every 60 ms, like a burst (C9).',\n ab_cycle: 'A, B, A, B, one a second.',\n blind: 'Plays A or B without saying which; reveal it after.',\n measure: 'Measures the chosen sound through the real engine (offline context): sync, right sound, distance, side, variation, level, strength, tail and burst.',\n record: 'Records 3 s of the output with the sound playing and downloads it (C10).',\n approve: 'Marks C10 of the chosen sound as approved by your ear.',\n },\n categories: { armas: 'Weapons', impactos: 'Impacts', 'explosoes-fogo': 'Explosions and fire', personagem: 'Character', veiculos: 'Vehicles', 'itens-portas': 'Items and doors', ambiente: 'Ambience', calibracao: 'Calibration (SDK)' },\n blows: { bullet: 'bullet', blunt: 'punch/kick', blade: 'knife', break: 'break', drag: 'drag/fall' },\n materials: {\n steel: 'steel',\n aluminium: 'aluminium',\n wood: 'wood',\n plywood: 'plywood',\n mdf: 'MDF',\n glass: 'glass',\n tempered_glass: 'tempered glass',\n ceramic: 'ceramic',\n concrete: 'concrete',\n brick: 'brick',\n plaster: 'plaster',\n plastic: 'plastic',\n fabric: 'fabric',\n paper: 'cardboard',\n foliage: 'foliage',\n soil: 'soil',\n flesh: 'flesh',\n },\n environments: { indoor: 'indoor', outdoor: 'outdoor' },\n strengths: { light: 'light', medium: 'medium', heavy: 'heavy' },\n criteria: { C1: 'C1 Sync', C2: 'C2 Right sound', C3: 'C3 Distance', C4: 'C4 Position', C5: 'C5 Variation', C6: 'C6 Level', C7: 'C7 Strength', C8: 'C8 Tail', C9: 'C9 Burst', C10: 'C10 Listening' },\n metrics: { chosen: '1 · Chosen', matrix: '2 · Matrix', last: 'Last played', ab: '4 · A × B', blind: '4 · X', verdicts: '4 · Preferences', state: '5 · Measurement', count: '1 · Catalogue' },\n run: { idle: 'idle', measuring: 'measuring…', done: 'done', failed: 'failed', recording: 'recording…', recorded: 'recording downloaded' },\n words: { none: '—', hidden: '? (reveal)', prefers: 'prefers', approved: 'approved by the owner' },\n probe: 'Sound source',\n wall: 'Wall (occlusion)',\n scenarios: { pipeline: 'File → decode → play (round-robin, tail)', criteria: 'Criteria C1–C9 on the calibration sound', loop: 'File loop: seamless joins (crossfade)', panel: 'Panel: categories, matrix, A/B and wall' },\n}\n\nexport const soundCheckTextOf = (locale: string): SoundCheckText => (locale.startsWith('en') ? EN : PT)\n","import type { AreaPlacement, BenchChange, ChoiceOption, ModuleControl, ModuleHost, ModuleMetric, ModuleScenario, TestableItem, TestableModule, Vec3 } from '@adatechnology/game-contracts'\n\nimport { SFX_BLOWS, SFX_CATALOG, SFX_CATEGORIES, SFX_IMPACTS, SFX_MATERIALS, impactSoundId, sfxState } from '../catalog'\nimport type { SfxBlow, SfxEntry, SfxMaterial, SfxState } from '../catalog'\nimport { CALIBRATION_ID, CALIBRATION_LOOP_ID, CALIBRATION_LOOP_SOUND, CALIBRATION_SOUND, calibrationLoader, isCalibrationUrl } from '../measure/calibration'\nimport { CRITERIA, measureCriteria, recordOutput } from '../measure/criteria'\nimport type { ActionCheck, CriterionId, CriterionResult } from '../measure/criteria'\nimport type { LoopRecipe, SampleSound, SoundBank, SoundRecipe } from '../recipe'\nimport { GAME_SFX_BANK } from '../recipes/noise-class'\nimport { SAMPLE_ENVIRONMENTS } from '../sample'\nimport type { SampleEnvironment } from '../sample'\nimport { createAudioEngine } from '../webaudio/engine'\nimport type { AudioEngine, PlayOptions, VoiceHandle, VoiceStartEvent } from '../webaudio/engine'\nimport { fetchLoader } from '../webaudio/sample-store'\nimport type { SampleLoader } from '../webaudio/sample-store'\nimport type { EngineSink } from '../webaudio/sink'\nimport type { AudioScenarioLike } from './module'\nimport { soundCheckTextOf } from './sound-check-locale'\nimport type { SoundCheckText } from './sound-check-locale'\n\nexport { soundCheckTextOf } from './sound-check-locale'\nexport type { SoundCheckText } from './sound-check-locale'\n\nexport const SOUND_STRENGTHS = { light: 0.1, medium: 0.5, heavy: 0.9 } as const\nexport type SoundStrength = keyof typeof SOUND_STRENGTHS\nexport const CALIBRATION_CATEGORY = 'calibracao'\n\nexport type SoundCheckModuleOptions = {\n /** Banks with sample sounds to try (the calibration sound is always there); registered in the host's engine when it lends one. */\n readonly banks?: readonly SoundBank[]\n /** Where the files of those banks come from (default `fetch`). */\n readonly loader?: SampleLoader\n /** The engine to use (default the host's in `host.audio`, else a new one). */\n readonly engine?: AudioEngine\n readonly locale?: string\n /** First sound chosen (default the calibration sound). */\n readonly sound?: string\n /** The sounds by category (default the kit's catalogue, `SFX_CATALOG` and the impact matrix). */\n readonly catalog?: readonly SfxEntry[]\n /** C2: the actions and the sounds they must play, per sound id. */\n readonly actions?: Readonly<Record<string, readonly ActionCheck[]>>\n /** C6: sound ids loudest first. */\n readonly loudnessOrder?: readonly string[]\n /** Draws the source of the sound in the area (a portable radio of the items catalogue, say). Without it no source is drawn. */\n readonly probe?: (THREE: unknown) => unknown\n /** Draws the wall that goes between the listener and the source (a house wall of the floorplan renderer). Without it no wall is drawn. */\n readonly wall?: (THREE: unknown) => unknown\n}\n\nexport type SoundCheckModule = TestableModule<AudioScenarioLike> & {\n readonly engine: AudioEngine\n /** Measures the chosen sound (every criterion) and keeps the results for the bench. */\n measure(): Promise<readonly CriterionResult[]>\n results(): readonly CriterionResult[]\n /** Every voice the engine started while the module was attached (the event log of C1/C2). */\n events(): readonly VoiceStartEvent[]\n /** Plays the chosen sound (its file when there is one, else the current sound). */\n play(): VoiceHandle | null\n /** The A/B pair of the chosen sound: the current (synthesized) id and the new (file) id. */\n pair(): { readonly a: string | null; readonly b: string | null }\n /** The preferences given (`sound → 'A' | 'B'`). */\n verdicts(): Readonly<Record<string, 'A' | 'B'>>\n /** The sounds whose C10 the owner approved. */\n approvals(): readonly string[]\n}\n\nconst CALIBRATION_BANK: SoundBank = { id: 'calibration', samples: [CALIBRATION_SOUND, CALIBRATION_LOOP_SOUND] }\nconst DISTANCES = [1, 10, 50, 200] as const\nconst PROBE_ID = 'sound_check_source'\nconst WALL_ID = 'sound_check_wall'\nconst EYE: Vec3 = [0, 1.7, 3]\n// the source is drawn at most this far (the arena is ~14 m); the sound plays at the real distance\nconst DRAWN_MAX = 11\nconst WALL_AT = -2.5\nconst LOG_LIMIT = 200\n\ntype Ears = { readonly position: Vec3; readonly yaw: number }\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 Placed = { userData?: Record<string, unknown>; visible?: boolean; position?: { set(x: number, y: number, z: number): void }; traverse(visit: (object: Placed) => void): void }\n\n/**\n * The sound panel of the test room, in the five blocks the owner approved: (1) the sounds of each category with their state (file, synthesized, missing),\n * (2) the material × blow matrix, (3) where and how it plays (distance, bearing, indoor or outdoor, strength, behind the house wall, a burst), (4) A/B of the\n * current sound against the new file (alternating and blind, with the preference kept) and (5) the measurement of C1–C10 with a 3 s recording and the\n * owner's approval for C10. It uses the host's engine (every bank of the room) and lends it the calibration takes.\n */\nexport function createSoundCheckModule(options: SoundCheckModuleOptions = {}): SoundCheckModule {\n const locale = options.locale ?? 'pt-BR'\n const text: SoundCheckText = soundCheckTextOf(locale)\n const ownBanks: SoundBank[] = [CALIBRATION_BANK, ...(options.banks ?? [])]\n const loader = calibrationLoader(options.loader ?? fetchLoader())\n const log: VoiceStartEvent[] = []\n const catalog: readonly SfxEntry[] = options.catalog ?? [...SFX_CATALOG, ...SFX_IMPACTS]\n let engine: AudioEngine = options.engine ?? createAudioEngine({ banks: [GAME_SFX_BANK, ...ownBanks], samples: { loader } })\n if (options.engine) for (const bank of ownBanks) engine.register(bank)\n let host: ModuleHost | null = null\n let results: CriterionResult[] = []\n let run: keyof SoundCheckText['run'] = 'idle'\n const verdicts: Record<string, 'A' | 'B'> = {}\n const approvals = new Set<string>()\n let blind: { readonly side: 'A' | 'B'; revealed: boolean } | null = null\n const chosen = {\n category: options.sound && options.sound !== CALIBRATION_ID ? (catalog.find((entry) => entry.id === options.sound)?.category ?? CALIBRATION_CATEGORY) : CALIBRATION_CATEGORY,\n sound: options.sound ?? CALIBRATION_ID,\n material: 'concrete' as SfxMaterial,\n blow: 'bullet' as SfxBlow,\n distance: 10,\n bearing: 0,\n environment: 'outdoor' as SampleEnvironment,\n strength: 'medium' as SoundStrength,\n wall: false,\n }\n\n const registry = () => engine.registry\n const entryOf = (id: string): SfxEntry | undefined => catalog.find((entry) => entry.id === id)\n const stateOf = (id: string): SfxState => {\n const entry = entryOf(id)\n if (entry) return sfxState(entry, registry())\n return registry().sample(id) ? 'file' : registry().sound(id) ? 'synth' : 'missing'\n }\n /** A = what plays today (the synthesized recipe), B = the file. */\n const pairOf = (id: string): { a: string | null; b: string | null } => {\n const entry = entryOf(id)\n const sample = registry().sample(id)\n const current = entry?.current ?? sample?.fallback ?? (registry().sound(id) ? id : undefined)\n return { a: current && registry().sound(current) ? current : null, b: sample ? id : null }\n }\n\n const soundsOf = (category: string): string[] => {\n if (category === CALIBRATION_CATEGORY) return registry().sampleIds().filter((id) => !entryOf(id))\n return catalog.filter((entry) => entry.category === category).map((entry) => entry.id)\n }\n const label = (id: string): string => {\n const entry = entryOf(id)\n return `${id}${entry ? ` · P${entry.priority}` : ''} · ${text.states[stateOf(id)]}`\n }\n const soundOptions = (): ChoiceOption[] => soundsOf(chosen.category).map((id) => ({ id, name: label(id) }))\n\n // the listener is the host camera; without one it stands at the eye of the area, looking along −z\n const listener = (): Ears => {\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: EYE, 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 // `bearing` is degrees to the right of straight ahead\n const offset = (ears: Ears, distance: number): Vec3 => {\n const bearing = (chosen.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] + distance * (forward[0] * Math.cos(bearing) + right[0] * Math.sin(bearing)), ears.position[1], ears.position[2] + distance * (forward[1] * Math.cos(bearing) + right[1] * Math.sin(bearing))]\n }\n const playOptions = (extra: PlayOptions = {}): PlayOptions => {\n const ears = listener()\n engine.setListener(ears)\n return { position: offset(ears, chosen.distance), environment: chosen.environment, intensity: SOUND_STRENGTHS[chosen.strength], occlusion: chosen.wall ? 1 : 0, ...extra }\n }\n\n const playId = (id: string | null, extra: PlayOptions = {}): VoiceHandle | null => {\n if (!id) return null\n engine.unlock()\n return engine.play(id, playOptions(extra))\n }\n const preferred = (id: string): string | null => pairOf(id).b ?? pairOf(id).a ?? (registry().sound(id) ? id : null)\n const play = (): VoiceHandle | null => playId(preferred(chosen.sound))\n // B is the file: while its files load a sample sound plays its fallback, so B waits for them\n const playB = async (delay = 0): Promise<void> => {\n const { b } = pairOf(chosen.sound)\n if (!b) return\n if (engine.sampleStatus(b) !== 'ready') await engine.loadSamples([b])\n playId(b, { delay })\n }\n const playA = (delay = 0): void => void playId(pairOf(chosen.sound).a, { delay })\n\n // the source and the wall drawn in the area follow the bench: the source at the bearing and distance (drawn at most DRAWN_MAX m), the wall only when asked\n const placeScenery = (): void => {\n const scene = host?.scene as Placed | undefined\n if (typeof scene?.traverse !== 'function') return\n const at = offset({ position: EYE, yaw: 0 }, Math.min(chosen.distance, DRAWN_MAX))\n scene.traverse((object) => {\n if (object.userData?.[PROBE_ID] === true) object.position?.set(at[0], 0, at[2])\n if (object.userData?.[WALL_ID] === true) object.visible = chosen.wall\n })\n }\n\n const measure = async (): Promise<readonly CriterionResult[]> => {\n run = 'measuring'\n try {\n engine.unlock()\n const context = (engine.output()?.context as AudioContext | undefined) ?? null\n // the measuring engines get every sound of this engine, so the fallbacks and the order of C6 are the room's\n const bank: SoundBank = {\n id: 'measured',\n sounds: registry().soundIds().map((id) => registry().sound(id)).filter((recipe): recipe is SoundRecipe => recipe !== undefined),\n loops: registry().loopIds().map((id) => registry().loop(id)).filter((recipe): recipe is LoopRecipe => recipe !== undefined),\n samples: registry().sampleIds().map((id) => registry().sample(id)).filter((sample): sample is SampleSound => sample !== undefined),\n }\n results = await measureCriteria({\n sound: chosen.sound,\n banks: [bank],\n loader,\n ...(context ? { baseLatency: context.baseLatency ?? 0, outputLatency: context.outputLatency ?? 0 } : {}),\n ...(options.actions?.[chosen.sound] ? { actions: options.actions[chosen.sound] as readonly ActionCheck[] } : {}),\n ...(options.loudnessOrder ? { loudnessOrder: options.loudnessOrder } : {}),\n })\n if (approvals.has(chosen.sound)) results = results.map((result) => (result.id === 'C10' ? { ...result, pass: true, value: text.words.approved } : result))\n run = 'done'\n } catch (error) {\n run = 'failed'\n results = [{ id: 'C1', pass: false, value: error instanceof Error ? error.message : String(error), target: '' }]\n }\n ;(globalThis as { __soundCheck?: unknown }).__soundCheck = { sound: chosen.sound, results }\n return results\n }\n\n const record = async (): Promise<void> => {\n run = 'recording'\n const blob = await recordOutput(engine, 3, () => {\n for (let index = 0; index < 4; index++) playId(preferred(chosen.sound), { delay: index * 0.6 })\n })\n const documentLike = (globalThis as { document?: Document }).document\n if (blob && documentLike && typeof URL.createObjectURL === 'function') {\n const link = documentLike.createElement('a')\n link.href = URL.createObjectURL(blob)\n link.download = `${chosen.sound}-${chosen.environment}-${chosen.distance}m.webm`\n link.click()\n run = 'recorded'\n } else run = blob ? 'recorded' : 'failed'\n }\n\n const approve = (): void => {\n approvals.add(chosen.sound)\n results = results.map((result) => (result.id === 'C10' ? { ...result, pass: true, value: text.words.approved } : result))\n host?.log(`${chosen.sound}: C10 ${text.words.approved}`, { sound: chosen.sound }, 'sound')\n }\n const prefer = (side: 'A' | 'B'): void => {\n verdicts[chosen.sound] = side\n const pair = pairOf(chosen.sound)\n host?.log(`${chosen.sound}: ${text.words.prefers} ${side}`, { sound: chosen.sound, prefers: side, a: pair.a, b: pair.b }, 'sound')\n }\n\n const controls = (): ModuleControl[] => [\n { kind: 'choice', id: 'category', name: text.controls.category, options: [...SFX_CATEGORIES, CALIBRATION_CATEGORY].map((id) => ({ id, name: text.categories[id] ?? id })), default: chosen.category },\n { kind: 'choice', id: 'sound', name: text.controls.sound, hint: text.hints.sound, options: soundOptions(), default: chosen.sound },\n { kind: 'action', id: 'play', name: text.controls.play },\n { kind: 'choice', id: 'material', name: text.controls.material, options: SFX_MATERIALS.map(([id]) => ({ id, name: text.materials[id] })), default: chosen.material },\n { kind: 'choice', id: 'blow', name: text.controls.blow, options: SFX_BLOWS.map((id) => ({ id, name: text.blows[id] })), default: chosen.blow },\n { kind: 'action', id: 'strike', name: text.controls.strike, hint: text.hints.strike },\n { kind: 'choice', id: 'distance', name: text.controls.distance, options: DISTANCES.map((meters) => ({ id: String(meters), name: `${meters} m` })), default: String(chosen.distance) },\n { kind: 'number', id: 'bearing', name: text.controls.bearing, unit: '°', min: -180, max: 180, step: 15, default: chosen.bearing },\n { kind: 'choice', id: 'environment', name: text.controls.environment, options: SAMPLE_ENVIRONMENTS.map((id) => ({ id, name: text.environments[id] })), default: chosen.environment },\n { kind: 'choice', id: 'strength', name: text.controls.strength, options: (Object.keys(SOUND_STRENGTHS) as SoundStrength[]).map((id) => ({ id, name: text.strengths[id] })), default: chosen.strength },\n { kind: 'boolean', id: 'wall', name: text.controls.wall, hint: text.hints.wall, default: chosen.wall },\n { kind: 'action', id: 'burst', name: text.controls.burst, hint: text.hints.burst },\n { kind: 'action', id: 'play_a', name: text.controls.play_a },\n { kind: 'action', id: 'play_b', name: text.controls.play_b },\n { kind: 'action', id: 'ab_cycle', name: text.controls.ab_cycle, hint: text.hints.ab_cycle },\n { kind: 'action', id: 'blind', name: text.controls.blind, hint: text.hints.blind },\n { kind: 'action', id: 'reveal', name: text.controls.reveal },\n { kind: 'action', id: 'prefer_a', name: text.controls.prefer_a },\n { kind: 'action', id: 'prefer_b', name: text.controls.prefer_b },\n { kind: 'action', id: 'measure', name: text.controls.measure, hint: text.hints.measure },\n { kind: 'action', id: 'record', name: text.controls.record, hint: text.hints.record },\n { kind: 'action', id: 'approve', name: text.controls.approve, hint: text.hints.approve },\n ]\n\n const line = (id: CriterionId): string => {\n const result = results.find((entry) => entry.id === id)\n if (!result) return text.words.none\n return `${result.pass === null ? '◌' : result.pass ? '✓' : '✗'} ${result.value}`\n }\n const counts = (): string => {\n const states = catalog.map((entry) => sfxState(entry, registry()))\n const count = (state: SfxState): number => states.filter((each) => each === state).length\n return `${text.states.file} ${count('file')} · ${text.states.synth} ${count('synth')} · ${text.states.missing} ${count('missing')}`\n }\n const strikeId = (): string => impactSoundId(chosen.material, chosen.blow)\n const lastLine = (): string => {\n const last = log.at(-1)\n if (!last) return text.words.none\n const files = last.layers?.map((layer) => layer.url.split('/').pop()).join('+') ?? ''\n return `${last.kind}:${last.fallbackOf ? `${last.fallbackOf}→` : ''}${last.id}${last.tier ? `/${last.tier}` : ''} ${files}`.trim()\n }\n\n const metrics = (): ModuleMetric[] => [\n { id: 'count', name: text.metrics.count, read: counts },\n { id: 'chosen', name: text.metrics.chosen, read: () => `${label(chosen.sound)}${registry().sample(chosen.sound) ? ` (${engine.sampleStatus(chosen.sound)})` : ''}` },\n { id: 'matrix', name: text.metrics.matrix, read: () => `${strikeId()} · ${text.states[stateOf(strikeId())]}${pairOf(strikeId()).a ? ` · A=${pairOf(strikeId()).a}` : ''}` },\n { id: 'ab', name: text.metrics.ab, read: () => `A ${pairOf(chosen.sound).a ?? text.words.none} · B ${pairOf(chosen.sound).b ?? text.words.none}` },\n { id: 'blind_state', name: text.metrics.blind, read: () => (!blind ? text.words.none : blind.revealed ? `X = ${blind.side}` : text.words.hidden) },\n { id: 'verdicts', name: text.metrics.verdicts, read: () => Object.entries(verdicts).map(([id, side]) => `${id} ${text.words.prefers} ${side}`).join('; ') || text.words.none },\n { id: 'last', name: text.metrics.last, read: lastLine },\n { id: 'state', name: text.metrics.state, read: () => text.run[run] },\n ...CRITERIA.map((id) => ({ id: `criterion_${id.toLowerCase()}`, name: text.criteria[id], read: () => line(id) })),\n ]\n\n const act = (id: string): void => {\n if (id === 'play') play()\n else if (id === 'strike') playId(preferred(strikeId()))\n else if (id === 'burst') for (let index = 0; index < 20; index++) playId(preferred(chosen.sound), { delay: index * 0.06 })\n else if (id === 'play_a') playA()\n else if (id === 'play_b') void playB()\n else if (id === 'ab_cycle') {\n playA(0)\n void playB(1).then(() => {\n playA(2)\n return playB(3)\n })\n } else if (id === 'blind') {\n const side = Math.random() < 0.5 ? 'A' : 'B'\n blind = { side, revealed: false }\n if (side === 'A') playA()\n else void playB()\n } else if (id === 'reveal' && blind) blind.revealed = true\n else if (id === 'prefer_a') prefer('A')\n else if (id === 'prefer_b') prefer('B')\n else if (id === 'measure') void measure()\n else if (id === 'record') void record()\n else if (id === 'approve') approve()\n }\n\n const onChange = (change: BenchChange): void => {\n if (change.kind === 'action') return act(change.id)\n if (change.kind !== 'control') return\n const { id, value } = change\n if (id === 'category' && typeof value === 'string') {\n chosen.category = value\n const first = soundsOf(value)[0]\n if (first) chosen.sound = first\n host?.bench?.patch('sound', { options: soundOptions(), value: chosen.sound })\n results = []\n } else if (id === 'sound' && typeof value === 'string') {\n chosen.sound = value\n results = []\n if (registry().sample(value)) void engine.loadSamples([value])\n } else if (id === 'material' && typeof value === 'string') chosen.material = value as SfxMaterial\n else if (id === 'blow' && typeof value === 'string') chosen.blow = value as SfxBlow\n else if (id === 'distance' && typeof value === 'string') chosen.distance = Number(value)\n else if (id === 'bearing' && typeof value === 'number') chosen.bearing = value\n else if (id === 'environment' && (value === 'indoor' || value === 'outdoor')) chosen.environment = value\n else if (id === 'strength' && typeof value === 'string' && value in SOUND_STRENGTHS) chosen.strength = value as SoundStrength\n else if (id === 'wall' && typeof value === 'boolean') chosen.wall = value\n placeScenery()\n }\n\n const items = (): TestableItem[] => {\n const marked = (id: string, name: string, build: (THREE: unknown) => unknown): TestableItem => ({\n id,\n name,\n movable: false,\n build: (THREE) => {\n const root = build(THREE) as { userData?: Record<string, unknown> }\n root.userData = root.userData ?? {}\n root.userData[id] = true\n return root\n },\n })\n return [...(options.probe ? [marked(PROBE_ID, text.probe, options.probe)] : []), ...(options.wall ? [marked(WALL_ID, text.wall, options.wall)] : [])]\n }\n\n const area = (): { placements: AreaPlacement[]; view: { eye: Vec3; target: Vec3 } } => ({\n placements: [\n ...(options.probe ? [{ item: PROBE_ID, position: offset({ position: EYE, yaw: 0 }, Math.min(chosen.distance, DRAWN_MAX)), asleep: true }] : []),\n ...(options.wall ? [{ item: WALL_ID, position: [0, 0, WALL_AT] as Vec3, asleep: true }] : []),\n ],\n view: { eye: EYE, target: [0, 1.2, -8] },\n })\n\n const scenarios = (): Array<ModuleScenario<AudioScenarioLike>> => [\n {\n id: 'sample_pipeline',\n name: text.scenarios.pipeline,\n run: async ({ expect }) => {\n const report = await engine.loadSamples([CALIBRATION_ID])\n expect(report.failed.length === 0 && engine.sampleStatus(CALIBRATION_ID) === 'ready', 'the calibration files load and decode', { failed: report.failed })\n // the voices of these six plays, watched on their own: the bench log keeps only its last LOG_LIMIT voices, so after a room full of sounds an\n // index into it no longer points at these\n const started: VoiceStartEvent[] = []\n const offStarted = engine.watchVoices((event) => void started.push(event))\n const handles = Array.from({ length: 6 }, (_, index) => engine.play(CALIBRATION_ID, { distance: 2, environment: index % 2 === 0 ? 'indoor' : 'outdoor' }))\n offStarted()\n expect(handles.every((handle) => handle?.kind === 'sample'), 'it plays the files, not the fallback', { kinds: handles.map((handle) => handle?.kind) })\n const events = started.filter((event) => event.id === CALIBRATION_ID)\n const takes = events.map((event) => event.layers?.find((layer) => layer.role === 'close')?.url)\n expect(takes.length === 6 && takes.every((take, index) => index === 0 || take !== takes[index - 1]), 'never the same take twice in a row', { takes })\n const tails = events.map((event) => event.layers?.find((layer) => layer.role === 'tail')?.url ?? '')\n expect(tails[0]?.endsWith('tail_in.wav') === true && tails[1]?.endsWith('tail_out.wav') === true, 'the tail follows the place', { tails })\n for (const handle of handles) handle?.stop()\n },\n },\n {\n id: 'sample_criteria',\n name: text.scenarios.criteria,\n run: async ({ expect }) => {\n const previous = chosen.sound\n chosen.sound = CALIBRATION_ID\n const measured = await measure()\n chosen.sound = previous\n for (const result of measured) if (result.id !== 'C10') expect(result.pass === true, `${result.id}: ${result.target}`, { value: result.value, data: result.data })\n },\n },\n {\n id: 'sample_loop',\n name: text.scenarios.loop,\n run: async ({ expect }) => {\n const report = await engine.loadSamples([CALIBRATION_LOOP_ID])\n expect(report.failed.length === 0, 'the loop takes load and decode', { failed: report.failed })\n engine.unlock()\n // read the bus once first: its analyser only hears what starts after\n engine.level('ambience')\n const loop = engine.loop(CALIBRATION_LOOP_ID, { volume: 0.8 })\n const wait = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))\n // the seams are measured once the loop sounds: the context may still be resuming and the bus analyser filling (on a busy machine\n // the first reads come back 0 and read as a dip that is only the start)\n const started = performance.now()\n while (engine.level('ambience') < 0.01 && performance.now() - started < 2000) await wait(20)\n expect(performance.now() - started < 2000, 'the loop starts within 2 s', { ms: Math.round(performance.now() - started) })\n await wait(200)\n const levels: number[] = []\n // 2.4 s: four seams of 0.6 s takes\n for (let index = 0; index < 60; index++) {\n levels.push(engine.level('ambience'))\n await wait(40)\n }\n loop.stop()\n const sorted = [...levels].sort((a, b) => a - b)\n const median = sorted[Math.floor(sorted.length / 2)] ?? 0\n const lowest = sorted[0] ?? 0\n const dip = lowest > 0 && median > 0 ? 20 * Math.log10(lowest / median) : -Infinity\n expect(median > 0.01, 'the loop sounds', { median })\n expect(dip >= -6, 'no seam: the level never dips more than 6 dB where two takes cross', { dip: Number(dip.toFixed(1)), lowest, median, levels: levels.map((level) => Number(level.toFixed(3))) })\n await wait(200)\n expect(engine.level('ambience') < median / 4, 'stop silences it', { after: engine.level('ambience') })\n },\n },\n {\n id: 'sound_panel',\n name: text.scenarios.panel,\n run: ({ expect }) => {\n const categories = SFX_CATEGORIES.map((category) => soundsOf(category).length)\n expect(categories.every((count) => count > 0), 'every category lists its sounds', { categories })\n expect(soundsOf('impactos').length >= SFX_MATERIALS.length * SFX_BLOWS.length, 'the matrix has every material × blow')\n const pair = pairOf(CALIBRATION_ID)\n expect(pair.a === 'sfx_pistol' && pair.b === CALIBRATION_ID, 'A is the synthesized sound and B the file', pair)\n const clear = engine.planSample(CALIBRATION_ID, { position: [0, 1.6, -10], occlusion: 0, seed: 1 })\n const walled = engine.planSample(CALIBRATION_ID, { position: [0, 1.6, -10], occlusion: 1, seed: 1 })\n expect((walled?.cutoff ?? Infinity) < (clear?.cutoff ?? 0) / 5, 'behind the wall the sound is muffled', { clear: clear?.cutoff, walled: walled?.cutoff })\n },\n },\n ]\n\n return {\n id: 'sound_check_area',\n name: text.name,\n group: text.group,\n ground: { surface: 'concrete_slab' },\n get engine() {\n return engine\n },\n items: () => items(),\n area: () => area(),\n controls: () => controls(),\n metrics: () => metrics(),\n scenarios: () => scenarios(),\n measure,\n results: () => results,\n events: () => log,\n play,\n pair: () => pairOf(chosen.sound),\n verdicts: () => ({ ...verdicts }),\n approvals: () => [...approvals],\n // the engine (and its context) is made on the first play or measure, not when the room loads the module\n attach(lent) {\n host = lent\n const shared = options.engine ? undefined : (lent.audio as EngineSink | undefined)?.engine\n if (shared && shared !== engine) {\n for (const bank of ownBanks) shared.register(bank)\n shared.addSampleSource(isCalibrationUrl, loader)\n engine = shared\n }\n const offVoices = engine.watchVoices((event) => {\n log.push(event)\n // the log keeps the last voices only (a burst a second would grow it for ever)\n if (log.length > LOG_LIMIT) log.splice(0, log.length - LOG_LIMIT)\n })\n lent.bench?.patch('sound', { options: soundOptions() })\n const off = lent.bench?.onChange(onChange)\n placeScenery()\n return () => {\n off?.()\n offVoices()\n host = null\n }\n },\n }\n}\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","// The calls of the animals, as recipes of @adatechnology/game-audio: the game's `bichoSom` (jogo L66625), which synthesizes four calls with the same\n// routine (the dog's two filtered clicks, the lapwing's shriek, the hen's cluck, the cow's moo). Linear fall to silence at 70 m, as the page has it; the\n// numbers are drawn and the nodes created in the page's order, so a recipe leaves the trace of the team code (tests/audio.test.ts against\n// tools/golden/fixtures/audio/game-audio.json).\nimport { DISTANCE_CURVES } from '@adatechnology/game-audio'\nimport type { SoundBank, SoundRecipe } from '@adatechnology/game-audio'\n\ntype Call = {\n readonly id: string\n /** The name in the team code. */\n readonly team: string\n readonly count: number\n readonly spacing: number\n readonly seconds: number\n readonly wave: OscillatorType\n readonly frequency: number\n /** The pitch falls to this fraction of itself over each pass. */\n readonly drop: number\n}\n\nconst call = (o: Call): SoundRecipe => ({\n id: o.id,\n bus: 'world',\n order: 'calls',\n duration: o.seconds,\n tail: 0.02,\n // the page makes its gain and filter before it looks at the level\n keepSilent: true,\n layers: [\n {\n repeat: { count: o.count, spacing: o.spacing },\n source: { kind: 'osc', wave: o.wave, frequency: { from: o.frequency, to: o.frequency * o.drop, curve: 'exp', over: 'duration' } },\n filters: { mode: 'serial', bank: [{ type: 'bandpass', frequency: { set: o.frequency * 1.2 }, q: 3 }] },\n envelope: [\n [0, 0.0001, 'set', 'fixed'],\n [0.015, 1, 'exp'],\n ['1d', 0.0001, 'exp', 'fixed'],\n ],\n },\n ],\n falloff: DISTANCE_CURVES.animals,\n sources: [`jogo:L66625-66639 bichoSom('${o.team}')`],\n})\n\n/** `latido`: two square clicks 0.22 s apart, 520 Hz falling to 0.6, 0.12 s each. */\nexport const DOG_BARK = call({ id: 'dog_bark', team: 'latido', count: 2, spacing: 0.22, seconds: 0.12, wave: 'square', frequency: 520, drop: 0.6 })\n/** `queroquero`: three squares 0.14 s apart, 2400 Hz falling to 0.6, 0.09 s each. */\nexport const LAPWING_CALL = call({ id: 'lapwing_call', team: 'queroquero', count: 3, spacing: 0.14, seconds: 0.09, wave: 'square', frequency: 2400, drop: 0.6 })\n/** `cacarejo`: four squares 0.14 s apart, 900 Hz falling to 0.6, 0.09 s each. */\nexport const HEN_CLUCK = call({ id: 'hen_cluck', team: 'cacarejo', count: 4, spacing: 0.14, seconds: 0.09, wave: 'square', frequency: 900, drop: 0.6 })\n/** `mugido`: one sawtooth, 140 Hz falling to 0.8, 1.2 s. */\nexport const COW_MOO = call({ id: 'cow_moo', team: 'mugido', count: 1, spacing: 0.14, seconds: 1.2, wave: 'sawtooth', frequency: 140, drop: 0.8 })\n\nexport const ANIMAL_SOUNDS: readonly SoundRecipe[] = [DOG_BARK, LAPWING_CALL, HEN_CLUCK, COW_MOO]\n\nexport const ANIMAL_SOUND_BANK: SoundBank = { id: 'animals_three', sounds: ANIMAL_SOUNDS }\n","{\n \"sources\": [\n \"jogo:L66677 galinha: cacarejo, 6 s between calls, noise 30 m\",\n \"jogo:L66680 quero-quero: queroquero when the player is near (alarme), 8 s, noise 40 m\",\n \"jogo:L66681 vaca: mugido, 30 s, noise 80 m\",\n \"jogo:L66683 cachorro: latido when a zombie is within 12 m, 3 s, noise 60 m\"\n ],\n \"species\": {\n \"chicken\": { \"sound\": \"hen_cluck\", \"cooldown\": 6, \"noiseMetres\": 30 },\n \"southern_lapwing\": { \"sound\": \"lapwing_call\", \"cooldown\": 8, \"noiseMetres\": 40 },\n \"cow\": { \"sound\": \"cow_moo\", \"cooldown\": 30, \"noiseMetres\": 80 },\n \"dog\": { \"sound\": \"dog_bark\", \"cooldown\": 3, \"noiseMetres\": 60 }\n }\n}\n","import type { SoundRequest, Vec3 } from '@adatechnology/game-contracts'\n\nimport { ANIMAL_SOUND_BANK } from './recipes'\nimport callsData from './calls.json'\n\n/** The call of a species: the recipe, the seconds the page waits before the same animal calls again, and the metres its noise reaches the AI (`emitNoise`). */\nexport type AnimalCall = { readonly sound: string; readonly cooldown: number; readonly noiseMetres: number }\n\ntype CallsData = { readonly sources: readonly string[]; readonly species: Readonly<Record<string, AnimalCall>> }\n\nconst DATA = callsData as unknown as CallsData\n\n/** The calls the page delivered, by species id. The other species (horse, pig, boar, capybara, jaguar, cat, vulture, snake, rat) have none: the page gives them no sound. */\nexport const ANIMAL_CALLS: Readonly<Record<string, AnimalCall>> = DATA.species\nexport const ANIMAL_CALL_SOURCES: readonly string[] = DATA.sources\n\n/** The part of the host's sound an animal needs: `play`, and `register` to put the bank in its engine (`createAudioSink(...)` has both). */\nexport type AnimalSoundSink = {\n play(request: SoundRequest): unknown\n register?(bank: unknown): void\n}\n\nexport type AnimalSoundOptions = {\n /** Replaces a species' call; `null` silences it. */\n readonly calls?: Readonly<Record<string, AnimalCall | null>>\n}\n\nexport type AnimalSound = {\n /** The call of a species, if it has one. */\n callOf(speciesId: string): AnimalCall | undefined\n /**\n * Makes the species call where the animal is. `key` is the animal (default the species): the same animal waits out its cooldown, and the call is\n * refused (false) until then. Returns whether a sound was asked for.\n */\n call(speciesId: string, position: Vec3, options?: { readonly volume?: number; readonly key?: string }): boolean\n /** Counts the cooldowns down: call it each frame. */\n update(dt: number): void\n dispose(): void\n}\n\n/**\n * Plays the calls of the animals on the host's sound: the four of the Bichos do Vale (hen, lapwing, cow, dog), positional with the page's linear 70 m\n * fall, each animal waiting out the page's cooldown. The bank is registered in the host's engine.\n */\nexport function createAnimalSound(sink: AnimalSoundSink, options: AnimalSoundOptions = {}): AnimalSound {\n sink.register?.(ANIMAL_SOUND_BANK)\n const calls: Record<string, AnimalCall | null | undefined> = { ...ANIMAL_CALLS, ...options.calls }\n const waiting = new Map<string, number>()\n const callOf = (speciesId: string): AnimalCall | undefined => calls[speciesId] ?? undefined\n return {\n callOf,\n call(speciesId, position, o = {}) {\n const found = callOf(speciesId)\n if (!found) return false\n const key = o.key ?? speciesId\n if ((waiting.get(key) ?? 0) > 0) return false\n waiting.set(key, found.cooldown)\n sink.play({ sound: found.sound, position, ...(o.volume === undefined ? {} : { volume: o.volume }) })\n return true\n },\n update(dt) {\n for (const [key, seconds] of waiting) {\n if (seconds - dt <= 0) waiting.delete(key)\n else waiting.set(key, seconds - dt)\n }\n },\n dispose: () => waiting.clear(),\n }\n}\n","import type * as THREE_NS from 'three'\n\nimport type { AnimalRig, AnimalsThreeOptions, Three } from './types'\n\nexport type MaterialExtras = Partial<THREE_NS.MeshStandardMaterialParameters>\n\n/** The mulberry32 stream of the page (`rng`): the same numbers for the same seed. */\nexport const createStream = (seed: number): (() => number) => {\n let state = seed\n return () => {\n state |= 0\n state = (state + 0x6d2b79f5) | 0\n let t = Math.imul(state ^ (state >>> 15), 1 | state)\n t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n }\n}\n\n/** The page's `add`: a mesh at a position and rotation under a parent, casting and receiving shadows. */\nexport const addMesh = (\n THREE: Three,\n parent: THREE_NS.Object3D,\n geometry: THREE_NS.BufferGeometry,\n material: THREE_NS.Material,\n x?: number,\n y?: number,\n z?: number,\n rx?: number,\n ry?: number,\n rz?: number,\n): THREE_NS.Mesh => {\n const mesh = new THREE.Mesh(geometry, material)\n mesh.position.set(x || 0, y || 0, z || 0)\n if (rx || ry || rz) mesh.rotation.set(rx || 0, ry || 0, rz || 0)\n mesh.castShadow = true\n mesh.receiveShadow = true\n parent.add(mesh)\n return mesh\n}\n\n/** The part builders of the page's `Animal` (`M`, `ball`, `limb`, `joint`, `pivot`), bound to one rig. */\nexport type RigTools = {\n material(color: number, extras?: MaterialExtras): THREE_NS.MeshStandardMaterial\n ball(parent: THREE_NS.Object3D, radius: number, material: THREE_NS.Material, x?: number, y?: number, z?: number, sx?: number, sy?: number, sz?: number): THREE_NS.Mesh\n /** A cylinder hanging from the origin; `radius1` is the one the page calls the top, but the geometry takes it as the bottom (kept as the page has it). */\n limb(parent: THREE_NS.Object3D, radius1: number, radius2: number, length: number, material: THREE_NS.Material): THREE_NS.Mesh\n joint(parent: THREE_NS.Object3D, radius: number, material: THREE_NS.Material, x?: number, y?: number, z?: number): THREE_NS.Mesh\n pivot(parent: THREE_NS.Object3D, x: number, y: number, z: number, offset?: THREE_NS.Vector3): THREE_NS.Group\n eyes(head: THREE_NS.Object3D, radius: number, x: number, y: number, z: number): void\n}\n\nexport const createRigTools = (THREE: Three, rig: AnimalRig): RigTools => {\n const tools: RigTools = {\n material(color, extras) {\n const material = new THREE.MeshStandardMaterial(Object.assign({ color, roughness: 0.8, metalness: 0, flatShading: false }, extras || {}))\n material.userData.base = material.color.clone()\n rig.materials.push(material)\n return material\n },\n ball(parent, radius, material, x, y, z, sx, sy, sz) {\n const mesh = addMesh(THREE, parent, new THREE.SphereGeometry(radius, 16, 12), material, x, y, z)\n if (sx) mesh.scale.set(sx, sy as number, sz as number)\n return mesh\n },\n limb: (parent, radius1, radius2, length, material) => addMesh(THREE, parent, new THREE.CylinderGeometry(radius2, radius1, length, 14, 1).translate(0, -length / 2, 0), material),\n joint(parent, radius, material, x, y, z) {\n const joint = tools.ball(parent, radius, material, x, y, z)\n joint.userData.joint = 1\n rig.joints.push(joint)\n return joint\n },\n pivot(parent, x, y, z, offset) {\n const group = new THREE.Group()\n group.position.set(x, y, z)\n parent.add(group)\n rig.pivots.push({ group, rest: group.position.clone(), offset: offset || new THREE.Vector3() })\n return group\n },\n eyes(head, radius, x, y, z) {\n const eyeMaterial = tools.material(0x111111, { roughness: 0.25 })\n rig.eyeMaterial = eyeMaterial\n for (const side of [-1, 1]) rig.eyes.push(tools.ball(head, radius, eyeMaterial, side * x, y, z))\n },\n }\n return tools\n}\n\nexport const createRig = (THREE: Three, species: AnimalRig['species'], options: AnimalsThreeOptions & { phase: number }): AnimalRig => {\n const rig: AnimalRig = {\n species,\n root: new THREE.Group(),\n trunk: new THREE.Group(),\n materials: [],\n joints: [],\n pivots: [],\n legs: [],\n tail: [],\n ears: [],\n infectionParts: [],\n eyes: [],\n wings: [],\n segments: [],\n phase: options.phase,\n size: 1,\n segmentLength: 0,\n segmentCount: 0,\n home: new THREE.Vector3(),\n infected: false,\n dead: false,\n neckAngle: 0,\n headAngle: 0,\n triangles: 0,\n }\n rig.root.add(rig.trunk)\n return rig\n}\n","import type * as THREE_NS from 'three'\n\nimport { addMesh } from './context'\nimport type { RigTools } from './context'\nimport type { HeadSpecOf, NeckSpecOf } from './extras-types'\nimport type { AnimalRig, Three } from './types'\n\nexport type ExtraContext = { readonly THREE: Three; readonly rig: AnimalRig; readonly tools: RigTools }\n\nexport type QuadrupedParts = {\n readonly bodyGroup: THREE_NS.Group\n readonly head: THREE_NS.Group\n readonly neck: THREE_NS.Group\n readonly main: THREE_NS.MeshStandardMaterial\n readonly belly: THREE_NS.MeshStandardMaterial\n readonly dark: THREE_NS.MeshStandardMaterial\n readonly L: number\n readonly Rb: number\n readonly bodyY: number\n readonly hd: HeadSpecOf\n readonly nk: NeckSpecOf\n}\n\nexport type BirdParts = {\n readonly bodyGroup: THREE_NS.Group\n readonly head: THREE_NS.Group\n readonly neck: THREE_NS.Group\n readonly main: THREE_NS.MeshStandardMaterial\n readonly dark: THREE_NS.MeshStandardMaterial\n}\n\nexport type QuadrupedExtra = (context: ExtraContext, parts: QuadrupedParts) => void\nexport type BirdExtra = (context: ExtraContext, parts: BirdParts) => void\n\nconst cow: QuadrupedExtra = ({ THREE, tools }, k) => {\n tools.ball(k.bodyGroup, 0.2, k.main, 0, k.Rb * 1.1, k.L * 0.33, 0.95, 1.1, 1.1) // hump\n tools.ball(k.neck, 0.13, k.main, 0, 0.12, 0.08, 0.55, 1.4, 1.05) // dewlap\n for (const side of [-1, 1]) addMesh(THREE, k.head, new THREE.ConeGeometry(0.025, 0.13, 8), tools.material(0xcfc6a8), side * 0.12, k.hd.r * 1.05, k.hd.r * 0.05, 0, 0, side * -0.9)\n}\n\nconst horse: QuadrupedExtra = ({ THREE, rig, tools }, k) => {\n for (let index = 0; index < 9; index++) addMesh(THREE, k.neck, new THREE.BoxGeometry(0.035, 0.16, 0.08), k.dark, 0, 0.06 + index * 0.065, -0.12 + index * 0.004, -0.25, 0, 0) // mane\n addMesh(THREE, k.head, new THREE.BoxGeometry(0.03, 0.12, 0.06), k.dark, 0, k.hd.r * 1.05, k.hd.r * 0.3, 0.6, 0, 0) // forelock\n const last = rig.tail[rig.tail.length - 1] as THREE_NS.Group\n tools.ball(last, 0.08, k.dark, 0, -0.42, 0, 0.7, 4.5, 0.7) // tail hair\n}\n\nconst wildBoar: QuadrupedExtra = ({ THREE, tools }, k) => {\n for (let index = 0; index < 12; index++) addMesh(THREE, k.bodyGroup, new THREE.BoxGeometry(0.03, 0.1, 0.06), k.dark, 0, k.Rb * 1.12 - index * 0.004, k.L * 0.38 - index * 0.06, -0.2, 0, 0) // bristles\n for (const side of [-1, 1])\n addMesh(THREE, k.head, new THREE.ConeGeometry(0.012, 0.09, 6), tools.material(0xe8e0cc), side * 0.07, k.hd.r * -0.25 + 0.03, k.hd.r * 0.25 + k.hd.r * k.hd.s[2] * 0.55 + 0.13, -0.6, 0, side * 0.5) // tusks\n}\n\nconst chicken: BirdExtra = ({ tools }, k) => {\n const red = tools.material(0xc0281e)\n for (let index = 0; index < 4; index++) tools.ball(k.head, 0.014, red, 0, 0.045, -0.012 + index * 0.014, 0.5, 1, 0.8) // comb\n tools.ball(k.head, 0.012, red, 0, -0.04, 0.04, 0.5, 1.3, 0.7) // wattle\n}\n\nconst southernLapwing: BirdExtra = ({ THREE, tools }, k) => {\n const black = tools.material(0x161616)\n tools.ball(k.bodyGroup, 0.05, black, 0, 0.03, 0.08, 1.1, 0.9, 0.6) // breast patch\n addMesh(THREE, k.head, new THREE.BoxGeometry(0.006, 0.006, 0.06).translate(0, 0, -0.03), black, 0, 0.025, -0.01, 0.5, 0, 0) // crest\n tools.ball(k.head, 0.026, black, 0, -0.005, 0.02, 1.05, 0.7, 0.9) // cap\n}\n\n/** The parts the page builds with code for five species (the page's `extra` closures), by species id. */\nexport const QUADRUPED_EXTRAS: Readonly<Record<string, QuadrupedExtra>> = { cow, horse, wild_boar: wildBoar }\nexport const BIRD_EXTRAS: Readonly<Record<string, BirdExtra>> = { chicken, southern_lapwing: southernLapwing }\n","{\n \"walk\": [\n 1.570796327,\n 4.71238898,\n 0,\n 3.141592654\n ],\n \"trot\": [\n 0,\n 3.141592654,\n 3.141592654,\n 0\n ],\n \"gallop\": [\n 2.890265241,\n 3.707079331,\n 0,\n 0.879645943\n ]\n}\n","import gaitsData from './data/gaits.json'\nimport type { AnimalGaitName, GaitPhases } from './types'\n\n/** The phase of each leg per gait, in radians, from the Bichos do Vale page (`GAIT`); the leg order is front right, front left, hind right, hind left. */\nexport const GAIT_PHASES = gaitsData as unknown as Readonly<Record<AnimalGaitName, GaitPhases>>\n","import type { BirdSpec, QuadrupedSpec } from './types'\n\nexport type QuadrupedLayout = {\n /** Height of the front and the hind hips above the ground. */\n readonly frontHipHeight: number\n readonly hindHipHeight: number\n readonly footHeight: number\n /** Height of the body centre. */\n readonly bodyHeight: number\n /** Distance between the shoulders and the hips. */\n readonly span: number\n /** Largest dimension, used to scale the offsets of the split view and the blood pool. */\n readonly size: number\n}\n\n/** The rest-pose geometry of a quadruped: leg lengths and angles decide how high the body stands. */\nexport const quadrupedLayout = (spec: QuadrupedSpec): QuadrupedLayout => {\n const leg = spec.leg\n const frontAngle1 = leg.fa1 ?? 0.06\n const frontAngle2 = leg.fa2 ?? -0.08\n const hindAngle1 = leg.ha1 ?? -0.32\n const hindAngle2 = leg.ha2 ?? 0.58\n const footHeight = leg.hoof ? leg.r * 1.1 : leg.r * 0.7\n const frontHipHeight = leg.fu * Math.cos(frontAngle1) + leg.fl * Math.cos(frontAngle1 + frontAngle2) + footHeight\n const hindHipHeight = leg.hu * Math.cos(hindAngle1) + leg.hl * Math.cos(hindAngle1 + hindAngle2) + footHeight\n const bodyHeight = (frontHipHeight + hindHipHeight) / 2 + (spec.up ?? 0) * spec.R\n return { frontHipHeight, hindHipHeight, footHeight, bodyHeight, span: spec.L * spec.span, size: Math.max(spec.L, bodyHeight + spec.R) }\n}\n\n/** The size of a bird: the largest of its body and its standing height. */\nexport const birdSize = (spec: BirdSpec): number => Math.max(spec.r * 3, spec.tars + spec.shank * 0.9 + spec.r * 2)\n\n/** The length a species takes in a line-up: used to space the animals apart. */\nexport const lineUpLength = (body: QuadrupedSpec | BirdSpec | { readonly kind: 'snake' }): number => {\n if (body.kind === 'quadruped') return body.L * 1.3\n if (body.kind === 'snake') return 0.9\n return body.r * 4\n}\n","type Entry = { readonly id: string }\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 shallow merge); a patch for an id that does not exist is ignored. */\nexport const applyPatches = <TEntry extends Entry>(entries: Map<string, TEntry>, patches: Readonly<Record<string, 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, { ...entry, ...patch, id })\n }\n}\n\nexport const removeIds = (entries: Map<string, unknown>, ids: readonly string[] | undefined): void => {\n for (const id of ids ?? []) entries.delete(id)\n}\n","import { GAIT_PHASES } from './gaits'\nimport { applyPatches, mergeById, removeIds } from './registry'\nimport type { AnimalGaitName, AnimalOutcome, AnimalSpecies, AnimalVisualState, AnimalsOptions, GaitPhases } from './types'\n\nexport type AnimalKit = {\n readonly species: readonly AnimalSpecies[]\n get(id: string): AnimalSpecies | undefined\n gait(name: AnimalGaitName): GaitPhases\n}\n\n/**\n * What a bite does to the look of an animal: a species that turns shows the infected look, one that dies shows the dead look, and an immune one shows no\n * change. `forceDead` shows the dead look for any species (a carcass, an animal killed by something else).\n */\nexport const visualStateOf = (outcome: AnimalOutcome, isBitten: boolean, forceDead = false): AnimalVisualState => {\n if (forceDead) return 'dead'\n if (!isBitten) return 'alive'\n if (outcome === 'turns') return 'infected'\n return outcome === 'dies' ? 'dead' : 'alive'\n}\n\nexport const createAnimals = (options: AnimalsOptions = {}): AnimalKit => {\n const entries = mergeById([], options.packs?.flatMap((pack) => pack.species ?? []))\n applyPatches(entries, options.patch)\n removeIds(entries, options.remove)\n const species = [...entries.values()].sort((first, second) => first.order - second.order)\n const gaits = { ...GAIT_PHASES, ...options.gaits }\n return { species, get: (id) => entries.get(id), gait: (name) => gaits[name] }\n}\n\n","import { lineUpLength } from '@adatechnology/game-animals'\n\nimport type { AnimalRig } from './types'\n\nexport type LineUpOptions = {\n /** X where the row starts (page: -19). */\n readonly start?: number\n /** Gap between animals (page: 0.85). */\n readonly gap?: number\n /** Heading of the animals, radians (page: 1.1; snakes 2.2). */\n readonly yaw?: number\n readonly snakeYaw?: number\n}\n\n/** Puts the animals in a row on the x axis, each given room by its length, the row centred on the origin, and keeps each place as `home` (page: the line-up). */\nexport const lineUpAnimals = (rigs: readonly AnimalRig[], options: LineUpOptions = {}): void => {\n const start = options.start ?? -19\n const gap = options.gap ?? 0.85\n let cursor = start\n for (const rig of rigs) {\n const length = lineUpLength(rig.species.body)\n cursor += length / 2\n rig.root.position.set(cursor, 0, 0)\n rig.home = rig.root.position.clone()\n rig.root.rotation.y = rig.species.body.kind === 'snake' ? (options.snakeYaw ?? 2.2) : (options.yaw ?? 1.1)\n cursor += length / 2 + gap\n }\n const offset = -(cursor - gap + start) / 2\n for (const rig of rigs) {\n rig.root.position.x += offset\n rig.home.x += offset\n }\n}\n","import type { Color } from './types'\n\n/** A colour as `0xRRGGBB`: numbers as given (floored, as three's `setHex`), `'#rrggbb'` strings parsed. */\nexport function colorHex(color: Color | undefined, fallback = 0xffffff): number {\n if (color === undefined) return fallback\n if (typeof color === 'number') return Math.floor(color)\n const match = /^#([0-9a-fA-F]{6})$/.exec(color)\n if (!match) throw new Error(`game-shapes: colour \"${color}\" is not 0xRRGGBB or #rrggbb`)\n return parseInt(match[1] as string, 16)\n}\n\n/** three's `SRGBToLinear` (r160 `ColorManagement`), the same constants. */\nexport const srgbToLinear = (c: number): number => (c < 0.04045 ? c * 0.0773993808 : Math.pow(c * 0.9478672986 + 0.0521327014, 2.4))\n\n/**\n * The rgb floats of a hex colour as three's `Color.setHex` stores them: the channels over 255 (`'srgb'`), converted to the linear working space\n * when colour management is on (`'linear'`).\n */\nexport function colorChannels(hex: number, space: 'linear' | 'srgb'): [number, number, number] {\n const r = ((hex >> 16) & 255) / 255\n const g = ((hex >> 8) & 255) / 255\n const b = (hex & 255) / 255\n return space === 'linear' ? [srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)] : [r, g, b]\n}\n","/**\n * Canonical JSON (keys sorted, `undefined` left out, `-0` kept as `-0`, typed arrays as plain lists) and a 64-bit FNV-1a of it in two 32-bit\n * lanes: the key a shape is cached and shared by.\n */\nexport function canonicalJson(value: unknown): string {\n if (value === null) return 'null'\n if (typeof value === 'number') return Object.is(value, -0) ? '-0' : Number.isFinite(value) ? String(value) : JSON.stringify(String(value))\n if (typeof value === 'string') return JSON.stringify(value)\n if (typeof value === 'boolean') return value ? 'true' : 'false'\n if (typeof value === 'function') throw new Error('game-shapes: a function cannot be part of a shape (register it as a generator)')\n if (ArrayBuffer.isView(value)) return canonicalJson(Array.from(value as unknown as ArrayLike<number>))\n if (Array.isArray(value)) return `[${value.map((item) => (item === undefined ? 'null' : canonicalJson(item))).join(',')}]`\n if (typeof value === 'object') {\n const record = value as Record<string, unknown>\n const keys = Object.keys(record).filter((key) => record[key] !== undefined).sort()\n return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`\n }\n return 'null'\n}\n\nconst lane = (text: string, offset: number, prime: number): string => {\n let hash = offset\n for (let index = 0; index < text.length; index++) {\n hash ^= text.charCodeAt(index)\n hash = Math.imul(hash, prime)\n }\n return (hash >>> 0).toString(16).padStart(8, '0')\n}\n\n/** 64-bit FNV-1a of a text as 16 hex digits (two independent 32-bit lanes). */\nexport const hash64 = (text: string): string => lane(text, 0x811c9dc5, 0x01000193) + lane(text, 0x050c5d1f, 0x5bd1e995)\n\n/** `hash64(canonicalJson(value))`. */\nexport const hashValue = (value: unknown): string => hash64(canonicalJson(value))\n","/*\n * Matrix and quaternion arithmetic as three.js r160 does it (MIT, https://github.com/mrdoob/three.js): the same expressions in the same order, so a\n * vertex baked here has the same bits as one baked by `Matrix4.compose` / `multiplyMatrices` / `Matrix3.getNormalMatrix` / `Vector3.normalize`.\n * Matrices are column-major arrays of 16 (4 x 4) or 9 (3 x 3) numbers, the layout of three's `elements`.\n */\nimport type { Mat4, RotationOrder, Transform, Vec3 } from './types'\n\nexport type Matrix4 = number[]\nexport type Matrix3 = number[]\n\nexport const identity4 = (): Matrix4 => [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]\n\n/** `Quaternion.setFromEuler` (x, y, z, w). */\nexport function quaternionFromEuler(x: number, y: number, z: number, order: RotationOrder = 'XYZ'): [number, number, number, number] {\n const cos = Math.cos\n const sin = Math.sin\n const c1 = cos(x / 2)\n const c2 = cos(y / 2)\n const c3 = cos(z / 2)\n const s1 = sin(x / 2)\n const s2 = sin(y / 2)\n const s3 = sin(z / 2)\n switch (order) {\n case 'XYZ':\n return [s1 * c2 * c3 + c1 * s2 * s3, c1 * s2 * c3 - s1 * c2 * s3, c1 * c2 * s3 + s1 * s2 * c3, c1 * c2 * c3 - s1 * s2 * s3]\n case 'YXZ':\n return [s1 * c2 * c3 + c1 * s2 * s3, c1 * s2 * c3 - s1 * c2 * s3, c1 * c2 * s3 - s1 * s2 * c3, c1 * c2 * c3 + s1 * s2 * s3]\n case 'ZXY':\n return [s1 * c2 * c3 - c1 * s2 * s3, c1 * s2 * c3 + s1 * c2 * s3, c1 * c2 * s3 + s1 * s2 * c3, c1 * c2 * c3 - s1 * s2 * s3]\n case 'ZYX':\n return [s1 * c2 * c3 - c1 * s2 * s3, c1 * s2 * c3 + s1 * c2 * s3, c1 * c2 * s3 - s1 * s2 * c3, c1 * c2 * c3 + s1 * s2 * s3]\n case 'YZX':\n return [s1 * c2 * c3 + c1 * s2 * s3, c1 * s2 * c3 + s1 * c2 * s3, c1 * c2 * s3 - s1 * s2 * c3, c1 * c2 * c3 - s1 * s2 * s3]\n case 'XZY':\n return [s1 * c2 * c3 - c1 * s2 * s3, c1 * s2 * c3 - s1 * c2 * s3, c1 * c2 * s3 + s1 * s2 * c3, c1 * c2 * c3 + s1 * s2 * s3]\n default:\n throw new Error(`game-shapes: unknown rotation order \"${String(order)}\"`)\n }\n}\n\n/** `Matrix4.compose(position, quaternion, scale)`. */\nexport function compose(px: number, py: number, pz: number, q: readonly [number, number, number, number], sx: number, sy: number, sz: number): Matrix4 {\n const [x, y, z, w] = q\n const x2 = x + x\n const y2 = y + y\n const z2 = z + z\n const xx = x * x2\n const xy = x * y2\n const xz = x * z2\n const yy = y * y2\n const yz = y * z2\n const zz = z * z2\n const wx = w * x2\n const wy = w * y2\n const wz = w * z2\n return [\n (1 - (yy + zz)) * sx, (xy + wz) * sx, (xz - wy) * sx, 0,\n (xy - wz) * sy, (1 - (xx + zz)) * sy, (yz + wx) * sy, 0,\n (xz + wy) * sz, (yz - wx) * sz, (1 - (xx + yy)) * sz, 0,\n px, py, pz, 1,\n ]\n}\n\n/** `Matrix4.makeRotationY(theta)` followed by `setPosition(x, y, z)`. */\nexport function headingMatrix(theta: number, x = 0, y = 0, z = 0): Matrix4 {\n const c = Math.cos(theta)\n const s = Math.sin(theta)\n return [c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, x, y, z, 1]\n}\n\n/** `Matrix4.multiplyMatrices(a, b)`: a x b. */\nexport function multiply(ae: readonly number[], be: readonly number[]): Matrix4 {\n const a11 = ae[0]!, a12 = ae[4]!, a13 = ae[8]!, a14 = ae[12]!\n const a21 = ae[1]!, a22 = ae[5]!, a23 = ae[9]!, a24 = ae[13]!\n const a31 = ae[2]!, a32 = ae[6]!, a33 = ae[10]!, a34 = ae[14]!\n const a41 = ae[3]!, a42 = ae[7]!, a43 = ae[11]!, a44 = ae[15]!\n const b11 = be[0]!, b12 = be[4]!, b13 = be[8]!, b14 = be[12]!\n const b21 = be[1]!, b22 = be[5]!, b23 = be[9]!, b24 = be[13]!\n const b31 = be[2]!, b32 = be[6]!, b33 = be[10]!, b34 = be[14]!\n const b41 = be[3]!, b42 = be[7]!, b43 = be[11]!, b44 = be[15]!\n return [\n a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41,\n a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41,\n a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41,\n a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41,\n a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42,\n a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42,\n a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42,\n a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42,\n a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43,\n a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43,\n a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43,\n a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43,\n a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44,\n a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44,\n a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44,\n a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44,\n ]\n}\n\n/** `Matrix3.getNormalMatrix(m)`: the inverse transpose of the upper 3 x 3 (all zeros when singular, as three). */\nexport function normalMatrix(me: readonly number[]): Matrix3 {\n // setFromMatrix4\n const n11 = me[0]!, n21 = me[1]!, n31 = me[2]!\n const n12 = me[4]!, n22 = me[5]!, n32 = me[6]!\n const n13 = me[8]!, n23 = me[9]!, n33 = me[10]!\n // invert\n const t11 = n33 * n22 - n32 * n23\n const t12 = n32 * n13 - n33 * n12\n const t13 = n23 * n12 - n22 * n13\n const det = n11 * t11 + n21 * t12 + n31 * t13\n if (det === 0) return [0, 0, 0, 0, 0, 0, 0, 0, 0]\n const detInv = 1 / det\n const te = [\n t11 * detInv,\n (n31 * n23 - n33 * n21) * detInv,\n (n32 * n21 - n31 * n22) * detInv,\n t12 * detInv,\n (n33 * n11 - n31 * n13) * detInv,\n (n31 * n12 - n32 * n11) * detInv,\n t13 * detInv,\n (n21 * n13 - n23 * n11) * detInv,\n (n22 * n11 - n21 * n12) * detInv,\n ]\n // transpose\n let tmp = te[1]!\n te[1] = te[3]!\n te[3] = tmp\n tmp = te[2]!\n te[2] = te[6]!\n te[6] = tmp\n tmp = te[5]!\n te[5] = te[7]!\n te[7] = tmp\n return te\n}\n\n/** True when the last row is (0, 0, 0, 1): `applyMatrix4`'s divide by w is then a multiply by exactly 1. */\nexport const isAffine = (e: readonly number[]): boolean => e[3] === 0 && e[7] === 0 && e[11] === 0 && e[15] === 1\n\n/** `Vector3.applyMatrix4`. */\nexport function transformPoint(e: readonly number[], x: number, y: number, z: number): [number, number, number] {\n const w = 1 / (e[3]! * x + e[7]! * y + e[11]! * z + e[15]!)\n return [(e[0]! * x + e[4]! * y + e[8]! * z + e[12]!) * w, (e[1]! * x + e[5]! * y + e[9]! * z + e[13]!) * w, (e[2]! * x + e[6]! * y + e[10]! * z + e[14]!) * w]\n}\n\nconst toVec3 = (scale: number | Vec3 | undefined): Vec3 => (scale === undefined ? [1, 1, 1] : typeof scale === 'number' ? [scale, scale, scale] : scale)\n\n/** True when the transform has no field set. */\nexport const isIdentityTransform = (t: Transform): boolean =>\n t.matrix === undefined && t.heading === undefined && t.position === undefined && t.rotation === undefined && t.scale === undefined\n\n/** The local matrix of a transform, or `null` when it has none. */\nexport function matrixOf(t: Transform): Matrix4 | null {\n if (t.matrix !== undefined) return [...t.matrix]\n if (t.heading !== undefined) {\n const p = t.position ?? [0, 0, 0]\n return headingMatrix(t.heading, p[0], p[1], p[2])\n }\n if (t.position === undefined && t.rotation === undefined && t.scale === undefined) return null\n const p = t.position ?? [0, 0, 0]\n const r = t.rotation ?? [0, 0, 0]\n const s = toVec3(t.scale)\n return compose(p[0], p[1], p[2], quaternionFromEuler(r[0], r[1], r[2], t.rotationOrder ?? 'XYZ'), s[0], s[1], s[2])\n}\n\n/** The matrix as given, as a plain array (for `ShapePart.matrix`). */\nexport const asMat4 = (m: Matrix4 | null): Mat4 => (m ? [...m] : identity4())\n","/*\n * The geometry generators of three.js r160 (MIT, https://github.com/mrdoob/three.js, src/geometries/*.js), ported line by line: the same loops,\n * the same expressions in the same order, numbers pushed into plain arrays and stored as Float32 at the end, exactly as `Float32BufferAttribute`\n * does. A test compares every array of every generator with three's, bit for bit, over a grid of parameters.\n */\nimport type { GeometryGroup, PrimitiveBuffers } from '../types'\n\nexport interface Arrays {\n vertices: number[]\n normals: number[]\n uvs: number[]\n indices: number[] | null\n groups: GeometryGroup[]\n}\n\n/** three's `arrayNeedsUint32`: 32-bit indices once any index reaches 65535. */\nexport function needsUint32(array: ArrayLike<number>): boolean {\n for (let i = array.length - 1; i >= 0; --i) if ((array[i] as number) >= 65535) return true\n return false\n}\n\nexport const indexArray = (indices: ArrayLike<number>): Uint16Array | Uint32Array => (needsUint32(indices) ? new Uint32Array(indices) : new Uint16Array(indices))\n\nexport function pack(type: string, parameters: Record<string, unknown>, a: Arrays): PrimitiveBuffers {\n return {\n type,\n parameters,\n positions: new Float32Array(a.vertices),\n normals: new Float32Array(a.normals),\n uvs: new Float32Array(a.uvs),\n ...(a.indices ? { indices: indexArray(a.indices) } : {}),\n groups: a.groups,\n }\n}\n\nconst normalize3 = (x: number, y: number, z: number): [number, number, number] => {\n const length = Math.sqrt(x * x + y * y + z * z)\n const s = 1 / (length || 1)\n return [x * s, y * s, z * s]\n}\n\nexport function box(width = 1, height = 1, depth = 1, widthSegments = 1, heightSegments = 1, depthSegments = 1): PrimitiveBuffers {\n const parameters = { width, height, depth, widthSegments, heightSegments, depthSegments }\n widthSegments = Math.floor(widthSegments)\n heightSegments = Math.floor(heightSegments)\n depthSegments = Math.floor(depthSegments)\n const indices: number[] = []\n const vertices: number[] = []\n const normals: number[] = []\n const uvs: number[] = []\n const groups: GeometryGroup[] = []\n let numberOfVertices = 0\n let groupStart = 0\n type Axis = 'x' | 'y' | 'z'\n const buildPlane = (u: Axis, v: Axis, w: Axis, udir: number, vdir: number, width: number, height: number, depth: number, gridX: number, gridY: number, materialIndex: number): void => {\n const segmentWidth = width / gridX\n const segmentHeight = height / gridY\n const widthHalf = width / 2\n const heightHalf = height / 2\n const depthHalf = depth / 2\n const gridX1 = gridX + 1\n const gridY1 = gridY + 1\n let vertexCounter = 0\n let groupCount = 0\n const vector = { x: 0, y: 0, z: 0 }\n for (let iy = 0; iy < gridY1; iy++) {\n const y = iy * segmentHeight - heightHalf\n for (let ix = 0; ix < gridX1; ix++) {\n const x = ix * segmentWidth - widthHalf\n vector[u] = x * udir\n vector[v] = y * vdir\n vector[w] = depthHalf\n vertices.push(vector.x, vector.y, vector.z)\n vector[u] = 0\n vector[v] = 0\n vector[w] = depth > 0 ? 1 : -1\n normals.push(vector.x, vector.y, vector.z)\n uvs.push(ix / gridX)\n uvs.push(1 - iy / gridY)\n vertexCounter += 1\n }\n }\n for (let iy = 0; iy < gridY; iy++) {\n for (let ix = 0; ix < gridX; ix++) {\n const a = numberOfVertices + ix + gridX1 * iy\n const b = numberOfVertices + ix + gridX1 * (iy + 1)\n const c = numberOfVertices + (ix + 1) + gridX1 * (iy + 1)\n const d = numberOfVertices + (ix + 1) + gridX1 * iy\n indices.push(a, b, d)\n indices.push(b, c, d)\n groupCount += 6\n }\n }\n groups.push({ start: groupStart, count: groupCount, materialIndex })\n groupStart += groupCount\n numberOfVertices += vertexCounter\n }\n buildPlane('z', 'y', 'x', -1, -1, depth, height, width, depthSegments, heightSegments, 0)\n buildPlane('z', 'y', 'x', 1, -1, depth, height, -width, depthSegments, heightSegments, 1)\n buildPlane('x', 'z', 'y', 1, 1, width, depth, height, widthSegments, depthSegments, 2)\n buildPlane('x', 'z', 'y', 1, -1, width, depth, -height, widthSegments, depthSegments, 3)\n buildPlane('x', 'y', 'z', 1, -1, width, height, depth, widthSegments, heightSegments, 4)\n buildPlane('x', 'y', 'z', -1, -1, width, height, -depth, widthSegments, heightSegments, 5)\n return pack('BoxGeometry', parameters, { vertices, normals, uvs, indices, groups })\n}\n\nfunction cylinderArrays(radiusTop: number, radiusBottom: number, height: number, radialSegments: number, heightSegments: number, openEnded: boolean, thetaStart: number, thetaLength: number): Arrays {\n radialSegments = Math.floor(radialSegments)\n heightSegments = Math.floor(heightSegments)\n const indices: number[] = []\n const vertices: number[] = []\n const normals: number[] = []\n const uvs: number[] = []\n const groups: GeometryGroup[] = []\n let index = 0\n const indexArray: number[][] = []\n const halfHeight = height / 2\n let groupStart = 0\n\n const generateTorso = (): void => {\n let groupCount = 0\n const slope = (radiusBottom - radiusTop) / height\n for (let y = 0; y <= heightSegments; y++) {\n const indexRow: number[] = []\n const v = y / heightSegments\n const radius = v * (radiusBottom - radiusTop) + radiusTop\n for (let x = 0; x <= radialSegments; x++) {\n const u = x / radialSegments\n const theta = u * thetaLength + thetaStart\n const sinTheta = Math.sin(theta)\n const cosTheta = Math.cos(theta)\n vertices.push(radius * sinTheta, -v * height + halfHeight, radius * cosTheta)\n const n = normalize3(sinTheta, slope, cosTheta)\n normals.push(n[0], n[1], n[2])\n uvs.push(u, 1 - v)\n indexRow.push(index++)\n }\n indexArray.push(indexRow)\n }\n for (let x = 0; x < radialSegments; x++) {\n for (let y = 0; y < heightSegments; y++) {\n const a = indexArray[y]![x]!\n const b = indexArray[y + 1]![x]!\n const c = indexArray[y + 1]![x + 1]!\n const d = indexArray[y]![x + 1]!\n indices.push(a, b, d)\n indices.push(b, c, d)\n groupCount += 6\n }\n }\n groups.push({ start: groupStart, count: groupCount, materialIndex: 0 })\n groupStart += groupCount\n }\n\n const generateCap = (top: boolean): void => {\n const centerIndexStart = index\n let groupCount = 0\n const radius = top === true ? radiusTop : radiusBottom\n const sign = top === true ? 1 : -1\n for (let x = 1; x <= radialSegments; x++) {\n vertices.push(0, halfHeight * sign, 0)\n normals.push(0, sign, 0)\n uvs.push(0.5, 0.5)\n index++\n }\n const centerIndexEnd = index\n for (let x = 0; x <= radialSegments; x++) {\n const u = x / radialSegments\n const theta = u * thetaLength + thetaStart\n const cosTheta = Math.cos(theta)\n const sinTheta = Math.sin(theta)\n vertices.push(radius * sinTheta, halfHeight * sign, radius * cosTheta)\n normals.push(0, sign, 0)\n uvs.push(cosTheta * 0.5 + 0.5, sinTheta * 0.5 * sign + 0.5)\n index++\n }\n for (let x = 0; x < radialSegments; x++) {\n const c = centerIndexStart + x\n const i = centerIndexEnd + x\n if (top === true) indices.push(i, i + 1, c)\n else indices.push(i + 1, i, c)\n groupCount += 3\n }\n groups.push({ start: groupStart, count: groupCount, materialIndex: top === true ? 1 : 2 })\n groupStart += groupCount\n }\n\n generateTorso()\n if (openEnded === false) {\n if (radiusTop > 0) generateCap(true)\n if (radiusBottom > 0) generateCap(false)\n }\n return { vertices, normals, uvs, indices, groups }\n}\n\nexport function cylinder(radiusTop = 1, radiusBottom = 1, height = 1, radialSegments = 32, heightSegments = 1, openEnded = false, thetaStart = 0, thetaLength = Math.PI * 2): PrimitiveBuffers {\n const parameters = { radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength }\n return pack('CylinderGeometry', parameters, cylinderArrays(radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength))\n}\n\nexport function cone(radius = 1, height = 1, radialSegments = 32, heightSegments = 1, openEnded = false, thetaStart = 0, thetaLength = Math.PI * 2): PrimitiveBuffers {\n const parameters = { radius, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength }\n return pack('ConeGeometry', parameters, cylinderArrays(0, radius, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength))\n}\n\nexport function sphere(radius = 1, widthSegments = 32, heightSegments = 16, phiStart = 0, phiLength = Math.PI * 2, thetaStart = 0, thetaLength = Math.PI): PrimitiveBuffers {\n const parameters = { radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength }\n widthSegments = Math.max(3, Math.floor(widthSegments))\n heightSegments = Math.max(2, Math.floor(heightSegments))\n const thetaEnd = Math.min(thetaStart + thetaLength, Math.PI)\n let index = 0\n const grid: number[][] = []\n const indices: number[] = []\n const vertices: number[] = []\n const normals: number[] = []\n const uvs: number[] = []\n for (let iy = 0; iy <= heightSegments; iy++) {\n const verticesRow: number[] = []\n const v = iy / heightSegments\n let uOffset = 0\n if (iy === 0 && thetaStart === 0) uOffset = 0.5 / widthSegments\n else if (iy === heightSegments && thetaEnd === Math.PI) uOffset = -0.5 / widthSegments\n for (let ix = 0; ix <= widthSegments; ix++) {\n const u = ix / widthSegments\n const x = -radius * Math.cos(phiStart + u * phiLength) * Math.sin(thetaStart + v * thetaLength)\n const y = radius * Math.cos(thetaStart + v * thetaLength)\n const z = radius * Math.sin(phiStart + u * phiLength) * Math.sin(thetaStart + v * thetaLength)\n vertices.push(x, y, z)\n const n = normalize3(x, y, z)\n normals.push(n[0], n[1], n[2])\n uvs.push(u + uOffset, 1 - v)\n verticesRow.push(index++)\n }\n grid.push(verticesRow)\n }\n for (let iy = 0; iy < heightSegments; iy++) {\n for (let ix = 0; ix < widthSegments; ix++) {\n const a = grid[iy]![ix + 1]!\n const b = grid[iy]![ix]!\n const c = grid[iy + 1]![ix]!\n const d = grid[iy + 1]![ix + 1]!\n if (iy !== 0 || thetaStart > 0) indices.push(a, b, d)\n if (iy !== heightSegments - 1 || thetaEnd < Math.PI) indices.push(b, c, d)\n }\n }\n return pack('SphereGeometry', parameters, { vertices, normals, uvs, indices, groups: [] })\n}\n\nexport function plane(width = 1, height = 1, widthSegments = 1, heightSegments = 1): PrimitiveBuffers {\n const parameters = { width, height, widthSegments, heightSegments }\n const widthHalf = width / 2\n const heightHalf = height / 2\n const gridX = Math.floor(widthSegments)\n const gridY = Math.floor(heightSegments)\n const gridX1 = gridX + 1\n const gridY1 = gridY + 1\n const segmentWidth = width / gridX\n const segmentHeight = height / gridY\n const indices: number[] = []\n const vertices: number[] = []\n const normals: number[] = []\n const uvs: number[] = []\n for (let iy = 0; iy < gridY1; iy++) {\n const y = iy * segmentHeight - heightHalf\n for (let ix = 0; ix < gridX1; ix++) {\n const x = ix * segmentWidth - widthHalf\n vertices.push(x, -y, 0)\n normals.push(0, 0, 1)\n uvs.push(ix / gridX)\n uvs.push(1 - iy / gridY)\n }\n }\n for (let iy = 0; iy < gridY; iy++) {\n for (let ix = 0; ix < gridX; ix++) {\n const a = ix + gridX1 * iy\n const b = ix + gridX1 * (iy + 1)\n const c = ix + 1 + gridX1 * (iy + 1)\n const d = ix + 1 + gridX1 * iy\n indices.push(a, b, d)\n indices.push(b, c, d)\n }\n }\n return pack('PlaneGeometry', parameters, { vertices, normals, uvs, indices, groups: [] })\n}\n\nexport function circle(radius = 1, segments = 32, thetaStart = 0, thetaLength = Math.PI * 2): PrimitiveBuffers {\n const parameters = { radius, segments, thetaStart, thetaLength }\n segments = Math.max(3, segments)\n const indices: number[] = []\n const vertices: number[] = []\n const normals: number[] = []\n const uvs: number[] = []\n vertices.push(0, 0, 0)\n normals.push(0, 0, 1)\n uvs.push(0.5, 0.5)\n for (let s = 0, i = 3; s <= segments; s++, i += 3) {\n const segment = thetaStart + (s / segments) * thetaLength\n // the vector's z stays 0, as three's Vector3\n vertices.push(radius * Math.cos(segment), radius * Math.sin(segment), 0)\n normals.push(0, 0, 1)\n uvs.push(((vertices[i] as number) / radius + 1) / 2, ((vertices[i + 1] as number) / radius + 1) / 2)\n }\n for (let i = 1; i <= segments; i++) indices.push(i, i + 1, 0)\n return pack('CircleGeometry', parameters, { vertices, normals, uvs, indices, groups: [] })\n}\n\nexport function ring(innerRadius = 0.5, outerRadius = 1, thetaSegments = 32, phiSegments = 1, thetaStart = 0, thetaLength = Math.PI * 2): PrimitiveBuffers {\n const parameters = { innerRadius, outerRadius, thetaSegments, phiSegments, thetaStart, thetaLength }\n thetaSegments = Math.max(3, thetaSegments)\n phiSegments = Math.max(1, phiSegments)\n const indices: number[] = []\n const vertices: number[] = []\n const normals: number[] = []\n const uvs: number[] = []\n let radius = innerRadius\n const radiusStep = (outerRadius - innerRadius) / phiSegments\n for (let j = 0; j <= phiSegments; j++) {\n for (let i = 0; i <= thetaSegments; i++) {\n const segment = thetaStart + (i / thetaSegments) * thetaLength\n const x = radius * Math.cos(segment)\n const y = radius * Math.sin(segment)\n vertices.push(x, y, 0)\n normals.push(0, 0, 1)\n uvs.push((x / outerRadius + 1) / 2, (y / outerRadius + 1) / 2)\n }\n radius += radiusStep\n }\n for (let j = 0; j < phiSegments; j++) {\n const thetaSegmentLevel = j * (thetaSegments + 1)\n for (let i = 0; i < thetaSegments; i++) {\n const segment = i + thetaSegmentLevel\n const a = segment\n const b = segment + thetaSegments + 1\n const c = segment + thetaSegments + 2\n const d = segment + 1\n indices.push(a, b, d)\n indices.push(b, c, d)\n }\n }\n return pack('RingGeometry', parameters, { vertices, normals, uvs, indices, groups: [] })\n}\n\nexport function lathe(points: ReadonlyArray<readonly [number, number]>, segments = 12, phiStart = 0, phiLength = Math.PI * 2): PrimitiveBuffers {\n const parameters = { points: points.map(([x, y]) => ({ x, y })), segments, phiStart, phiLength }\n segments = Math.floor(segments)\n phiLength = Math.max(0, Math.min(Math.PI * 2, phiLength))\n const indices: number[] = []\n const vertices: number[] = []\n const uvs: number[] = []\n const initNormals: number[] = []\n const normals: number[] = []\n const inverseSegments = 1.0 / segments\n const normal = { x: 0, y: 0, z: 0 }\n const curNormal = { x: 0, y: 0, z: 0 }\n const prevNormal = { x: 0, y: 0, z: 0 }\n let dx = 0\n let dy = 0\n const last = points.length - 1\n const px = (j: number): number => (points[j] as readonly [number, number])[0]\n const py = (j: number): number => (points[j] as readonly [number, number])[1]\n const setNormalized = (): void => {\n const n = normalize3(normal.x, normal.y, normal.z)\n normal.x = n[0]\n normal.y = n[1]\n normal.z = n[2]\n }\n for (let j = 0; j <= last; j++) {\n if (j === 0) {\n dx = px(j + 1) - px(j)\n dy = py(j + 1) - py(j)\n normal.x = dy * 1.0\n normal.y = -dx\n normal.z = dy * 0.0\n prevNormal.x = normal.x\n prevNormal.y = normal.y\n prevNormal.z = normal.z\n setNormalized()\n initNormals.push(normal.x, normal.y, normal.z)\n } else if (j === last) {\n initNormals.push(prevNormal.x, prevNormal.y, prevNormal.z)\n } else {\n dx = px(j + 1) - px(j)\n dy = py(j + 1) - py(j)\n normal.x = dy * 1.0\n normal.y = -dx\n normal.z = dy * 0.0\n curNormal.x = normal.x\n curNormal.y = normal.y\n curNormal.z = normal.z\n normal.x += prevNormal.x\n normal.y += prevNormal.y\n normal.z += prevNormal.z\n setNormalized()\n initNormals.push(normal.x, normal.y, normal.z)\n prevNormal.x = curNormal.x\n prevNormal.y = curNormal.y\n prevNormal.z = curNormal.z\n }\n }\n for (let i = 0; i <= segments; i++) {\n const phi = phiStart + i * inverseSegments * phiLength\n const sin = Math.sin(phi)\n const cos = Math.cos(phi)\n for (let j = 0; j <= last; j++) {\n vertices.push(px(j) * sin, py(j), px(j) * cos)\n uvs.push(i / segments, j / (points.length - 1))\n const x = (initNormals[3 * j + 0] as number) * sin\n const y = initNormals[3 * j + 1] as number\n const z = (initNormals[3 * j + 0] as number) * cos\n normals.push(x, y, z)\n }\n }\n for (let i = 0; i < segments; i++) {\n for (let j = 0; j < points.length - 1; j++) {\n const base = j + i * points.length\n const a = base\n const b = base + points.length\n const c = base + points.length + 1\n const d = base + 1\n indices.push(a, b, d)\n indices.push(c, d, b)\n }\n }\n return pack('LatheGeometry', parameters, { vertices, normals, uvs, indices, groups: [] })\n}\n\nexport function torus(radius = 1, tube = 0.4, radialSegments = 12, tubularSegments = 48, arc = Math.PI * 2): PrimitiveBuffers {\n const parameters = { radius, tube, radialSegments, tubularSegments, arc }\n radialSegments = Math.floor(radialSegments)\n tubularSegments = Math.floor(tubularSegments)\n const indices: number[] = []\n const vertices: number[] = []\n const normals: number[] = []\n const uvs: number[] = []\n for (let j = 0; j <= radialSegments; j++) {\n for (let i = 0; i <= tubularSegments; i++) {\n const u = (i / tubularSegments) * arc\n const v = (j / radialSegments) * Math.PI * 2\n const x = (radius + tube * Math.cos(v)) * Math.cos(u)\n const y = (radius + tube * Math.cos(v)) * Math.sin(u)\n const z = tube * Math.sin(v)\n vertices.push(x, y, z)\n const cx = radius * Math.cos(u)\n const cy = radius * Math.sin(u)\n const n = normalize3(x - cx, y - cy, z - 0)\n normals.push(n[0], n[1], n[2])\n uvs.push(i / tubularSegments)\n uvs.push(j / radialSegments)\n }\n }\n for (let j = 1; j <= radialSegments; j++) {\n for (let i = 1; i <= tubularSegments; i++) {\n const a = (tubularSegments + 1) * j + i - 1\n const b = (tubularSegments + 1) * (j - 1) + i - 1\n const c = (tubularSegments + 1) * (j - 1) + i\n const d = (tubularSegments + 1) * j + i\n indices.push(a, b, d)\n indices.push(b, c, d)\n }\n }\n return pack('TorusGeometry', parameters, { vertices, normals, uvs, indices, groups: [] })\n}\n","import type { HoleSpec, PrimitiveBuffers, Vec3 } from '../types'\nimport { indexArray } from './three-generators'\n\n/**\n * A box (centred, `size = [w, h, d]`) minus rectangular prisms that go all the way through it along z: the cheap, exact boolean (walls with\n * windows and doors). The face is cut into a grid at every hole edge; each solid cell gets its front and back quads, and every cell edge between\n * solid and empty (a hole or the outside) gets a quad over the depth facing the empty side, so the outer sides and the reveals come out of the\n * same rule. Front uvs map the face to [0, 1]; side uvs run along the edge and the depth.\n */\nexport function difference(size: Vec3, subtract: readonly HoleSpec[]): PrimitiveBuffers {\n const [w, h, d] = size\n const hw = w / 2\n const hh = h / 2\n const hd = d / 2\n const holes = subtract\n .map((hole) => ({ x0: Math.max(-hw, Math.min(hole.min[0], hole.max[0])), x1: Math.min(hw, Math.max(hole.min[0], hole.max[0])), y0: Math.max(-hh, Math.min(hole.min[1], hole.max[1])), y1: Math.min(hh, Math.max(hole.min[1], hole.max[1])) }))\n .filter((hole) => hole.x1 > hole.x0 && hole.y1 > hole.y0)\n const cuts = (lo: number, hi: number, values: number[]): number[] => [...new Set([lo, hi, ...values.filter((v) => v > lo && v < hi)])].sort((a, b) => a - b)\n const xs = cuts(-hw, hw, holes.flatMap((hole) => [hole.x0, hole.x1]))\n const ys = cuts(-hh, hh, holes.flatMap((hole) => [hole.y0, hole.y1]))\n const nx = xs.length - 1\n const ny = ys.length - 1\n const solid = (i: number, j: number): boolean => {\n if (i < 0 || j < 0 || i >= nx || j >= ny) return false\n const cx = ((xs[i] as number) + (xs[i + 1] as number)) / 2\n const cy = ((ys[j] as number) + (ys[j + 1] as number)) / 2\n return !holes.some((hole) => cx > hole.x0 && cx < hole.x1 && cy > hole.y0 && cy < hole.y1)\n }\n const positions: number[] = []\n const normals: number[] = []\n const uvs: number[] = []\n const indices: number[] = []\n const quad = (corners: readonly (readonly [number, number, number])[], normal: readonly [number, number, number], uv: readonly (readonly [number, number])[]): void => {\n const base = positions.length / 3\n for (let k = 0; k < 4; k++) {\n positions.push(...(corners[k] as [number, number, number]))\n normals.push(...normal)\n uvs.push(...(uv[k] as [number, number]))\n }\n indices.push(base, base + 1, base + 2, base, base + 2, base + 3)\n }\n const fu = (x: number): number => (x + hw) / w\n const fv = (y: number): number => (y + hh) / h\n for (let j = 0; j < ny; j++) {\n for (let i = 0; i < nx; i++) {\n if (!solid(i, j)) continue\n const x0 = xs[i] as number\n const x1 = xs[i + 1] as number\n const y0 = ys[j] as number\n const y1 = ys[j + 1] as number\n quad([[x0, y0, hd], [x1, y0, hd], [x1, y1, hd], [x0, y1, hd]], [0, 0, 1], [[fu(x0), fv(y0)], [fu(x1), fv(y0)], [fu(x1), fv(y1)], [fu(x0), fv(y1)]])\n quad([[x1, y0, -hd], [x0, y0, -hd], [x0, y1, -hd], [x1, y1, -hd]], [0, 0, -1], [[1 - fu(x1), fv(y0)], [1 - fu(x0), fv(y0)], [1 - fu(x0), fv(y1)], [1 - fu(x1), fv(y1)]])\n const zu = (z: number): number => (z + hd) / d\n if (!solid(i - 1, j)) quad([[x0, y0, -hd], [x0, y0, hd], [x0, y1, hd], [x0, y1, -hd]], [-1, 0, 0], [[zu(-hd), fv(y0)], [zu(hd), fv(y0)], [zu(hd), fv(y1)], [zu(-hd), fv(y1)]])\n if (!solid(i + 1, j)) quad([[x1, y0, hd], [x1, y0, -hd], [x1, y1, -hd], [x1, y1, hd]], [1, 0, 0], [[zu(hd), fv(y0)], [zu(-hd), fv(y0)], [zu(-hd), fv(y1)], [zu(hd), fv(y1)]])\n if (!solid(i, j - 1)) quad([[x0, y0, -hd], [x1, y0, -hd], [x1, y0, hd], [x0, y0, hd]], [0, -1, 0], [[fu(x0), zu(-hd)], [fu(x1), zu(-hd)], [fu(x1), zu(hd)], [fu(x0), zu(hd)]])\n if (!solid(i, j + 1)) quad([[x0, y1, hd], [x1, y1, hd], [x1, y1, -hd], [x0, y1, -hd]], [0, 1, 0], [[fu(x0), zu(hd)], [fu(x1), zu(hd)], [fu(x1), zu(-hd)], [fu(x0), zu(-hd)]])\n }\n }\n return {\n type: 'BufferGeometry',\n parameters: { size: [w, h, d], subtract: holes.map((hole) => ({ min: [hole.x0, hole.y0], max: [hole.x1, hole.y1] })) },\n positions: new Float32Array(positions),\n normals: new Float32Array(normals),\n uvs: new Float32Array(uvs),\n indices: indexArray(indices),\n groups: [],\n }\n}\n","/* eslint-disable */\n// @ts-nocheck -- verbatim copy of three.js r160 src/extras/Earcut.js (MIT), itself a port of mapbox/earcut v2.2.4 (ISC); kept as is so triangulation matches three\n/**\n * Port from https://github.com/mapbox/earcut (v2.2.4)\n */\n\nconst Earcut = {\n\n\ttriangulate: function ( data, holeIndices, dim = 2 ) {\n\n\t\tconst hasHoles = holeIndices && holeIndices.length;\n\t\tconst outerLen = hasHoles ? holeIndices[ 0 ] * dim : data.length;\n\t\tlet outerNode = linkedList( data, 0, outerLen, dim, true );\n\t\tconst triangles = [];\n\n\t\tif ( ! outerNode || outerNode.next === outerNode.prev ) return triangles;\n\n\t\tlet minX, minY, maxX, maxY, x, y, invSize;\n\n\t\tif ( hasHoles ) outerNode = eliminateHoles( data, holeIndices, outerNode, dim );\n\n\t\t// if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox\n\t\tif ( data.length > 80 * dim ) {\n\n\t\t\tminX = maxX = data[ 0 ];\n\t\t\tminY = maxY = data[ 1 ];\n\n\t\t\tfor ( let i = dim; i < outerLen; i += dim ) {\n\n\t\t\t\tx = data[ i ];\n\t\t\t\ty = data[ i + 1 ];\n\t\t\t\tif ( x < minX ) minX = x;\n\t\t\t\tif ( y < minY ) minY = y;\n\t\t\t\tif ( x > maxX ) maxX = x;\n\t\t\t\tif ( y > maxY ) maxY = y;\n\n\t\t\t}\n\n\t\t\t// minX, minY and invSize are later used to transform coords into integers for z-order calculation\n\t\t\tinvSize = Math.max( maxX - minX, maxY - minY );\n\t\t\tinvSize = invSize !== 0 ? 32767 / invSize : 0;\n\n\t\t}\n\n\t\tearcutLinked( outerNode, triangles, dim, minX, minY, invSize, 0 );\n\n\t\treturn triangles;\n\n\t}\n\n};\n\n// create a circular doubly linked list from polygon points in the specified winding order\nfunction linkedList( data, start, end, dim, clockwise ) {\n\n\tlet i, last;\n\n\tif ( clockwise === ( signedArea( data, start, end, dim ) > 0 ) ) {\n\n\t\tfor ( i = start; i < end; i += dim ) last = insertNode( i, data[ i ], data[ i + 1 ], last );\n\n\t} else {\n\n\t\tfor ( i = end - dim; i >= start; i -= dim ) last = insertNode( i, data[ i ], data[ i + 1 ], last );\n\n\t}\n\n\tif ( last && equals( last, last.next ) ) {\n\n\t\tremoveNode( last );\n\t\tlast = last.next;\n\n\t}\n\n\treturn last;\n\n}\n\n// eliminate colinear or duplicate points\nfunction filterPoints( start, end ) {\n\n\tif ( ! start ) return start;\n\tif ( ! end ) end = start;\n\n\tlet p = start,\n\t\tagain;\n\tdo {\n\n\t\tagain = false;\n\n\t\tif ( ! p.steiner && ( equals( p, p.next ) || area( p.prev, p, p.next ) === 0 ) ) {\n\n\t\t\tremoveNode( p );\n\t\t\tp = end = p.prev;\n\t\t\tif ( p === p.next ) break;\n\t\t\tagain = true;\n\n\t\t} else {\n\n\t\t\tp = p.next;\n\n\t\t}\n\n\t} while ( again || p !== end );\n\n\treturn end;\n\n}\n\n// main ear slicing loop which triangulates a polygon (given as a linked list)\nfunction earcutLinked( ear, triangles, dim, minX, minY, invSize, pass ) {\n\n\tif ( ! ear ) return;\n\n\t// interlink polygon nodes in z-order\n\tif ( ! pass && invSize ) indexCurve( ear, minX, minY, invSize );\n\n\tlet stop = ear,\n\t\tprev, next;\n\n\t// iterate through ears, slicing them one by one\n\twhile ( ear.prev !== ear.next ) {\n\n\t\tprev = ear.prev;\n\t\tnext = ear.next;\n\n\t\tif ( invSize ? isEarHashed( ear, minX, minY, invSize ) : isEar( ear ) ) {\n\n\t\t\t// cut off the triangle\n\t\t\ttriangles.push( prev.i / dim | 0 );\n\t\t\ttriangles.push( ear.i / dim | 0 );\n\t\t\ttriangles.push( next.i / dim | 0 );\n\n\t\t\tremoveNode( ear );\n\n\t\t\t// skipping the next vertex leads to less sliver triangles\n\t\t\tear = next.next;\n\t\t\tstop = next.next;\n\n\t\t\tcontinue;\n\n\t\t}\n\n\t\tear = next;\n\n\t\t// if we looped through the whole remaining polygon and can't find any more ears\n\t\tif ( ear === stop ) {\n\n\t\t\t// try filtering points and slicing again\n\t\t\tif ( ! pass ) {\n\n\t\t\t\tearcutLinked( filterPoints( ear ), triangles, dim, minX, minY, invSize, 1 );\n\n\t\t\t\t// if this didn't work, try curing all small self-intersections locally\n\n\t\t\t} else if ( pass === 1 ) {\n\n\t\t\t\tear = cureLocalIntersections( filterPoints( ear ), triangles, dim );\n\t\t\t\tearcutLinked( ear, triangles, dim, minX, minY, invSize, 2 );\n\n\t\t\t\t// as a last resort, try splitting the remaining polygon into two\n\n\t\t\t} else if ( pass === 2 ) {\n\n\t\t\t\tsplitEarcut( ear, triangles, dim, minX, minY, invSize );\n\n\t\t\t}\n\n\t\t\tbreak;\n\n\t\t}\n\n\t}\n\n}\n\n// check whether a polygon node forms a valid ear with adjacent nodes\nfunction isEar( ear ) {\n\n\tconst a = ear.prev,\n\t\tb = ear,\n\t\tc = ear.next;\n\n\tif ( area( a, b, c ) >= 0 ) return false; // reflex, can't be an ear\n\n\t// now make sure we don't have other points inside the potential ear\n\tconst ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;\n\n\t// triangle bbox; min & max are calculated like this for speed\n\tconst x0 = ax < bx ? ( ax < cx ? ax : cx ) : ( bx < cx ? bx : cx ),\n\t\ty0 = ay < by ? ( ay < cy ? ay : cy ) : ( by < cy ? by : cy ),\n\t\tx1 = ax > bx ? ( ax > cx ? ax : cx ) : ( bx > cx ? bx : cx ),\n\t\ty1 = ay > by ? ( ay > cy ? ay : cy ) : ( by > cy ? by : cy );\n\n\tlet p = c.next;\n\twhile ( p !== a ) {\n\n\t\tif ( p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 &&\n\t\t\tpointInTriangle( ax, ay, bx, by, cx, cy, p.x, p.y ) &&\n\t\t\tarea( p.prev, p, p.next ) >= 0 ) return false;\n\t\tp = p.next;\n\n\t}\n\n\treturn true;\n\n}\n\nfunction isEarHashed( ear, minX, minY, invSize ) {\n\n\tconst a = ear.prev,\n\t\tb = ear,\n\t\tc = ear.next;\n\n\tif ( area( a, b, c ) >= 0 ) return false; // reflex, can't be an ear\n\n\tconst ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;\n\n\t// triangle bbox; min & max are calculated like this for speed\n\tconst x0 = ax < bx ? ( ax < cx ? ax : cx ) : ( bx < cx ? bx : cx ),\n\t\ty0 = ay < by ? ( ay < cy ? ay : cy ) : ( by < cy ? by : cy ),\n\t\tx1 = ax > bx ? ( ax > cx ? ax : cx ) : ( bx > cx ? bx : cx ),\n\t\ty1 = ay > by ? ( ay > cy ? ay : cy ) : ( by > cy ? by : cy );\n\n\t// z-order range for the current triangle bbox;\n\tconst minZ = zOrder( x0, y0, minX, minY, invSize ),\n\t\tmaxZ = zOrder( x1, y1, minX, minY, invSize );\n\n\tlet p = ear.prevZ,\n\t\tn = ear.nextZ;\n\n\t// look for points inside the triangle in both directions\n\twhile ( p && p.z >= minZ && n && n.z <= maxZ ) {\n\n\t\tif ( p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&\n\t\t\tpointInTriangle( ax, ay, bx, by, cx, cy, p.x, p.y ) && area( p.prev, p, p.next ) >= 0 ) return false;\n\t\tp = p.prevZ;\n\n\t\tif ( n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&\n\t\t\tpointInTriangle( ax, ay, bx, by, cx, cy, n.x, n.y ) && area( n.prev, n, n.next ) >= 0 ) return false;\n\t\tn = n.nextZ;\n\n\t}\n\n\t// look for remaining points in decreasing z-order\n\twhile ( p && p.z >= minZ ) {\n\n\t\tif ( p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&\n\t\t\tpointInTriangle( ax, ay, bx, by, cx, cy, p.x, p.y ) && area( p.prev, p, p.next ) >= 0 ) return false;\n\t\tp = p.prevZ;\n\n\t}\n\n\t// look for remaining points in increasing z-order\n\twhile ( n && n.z <= maxZ ) {\n\n\t\tif ( n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&\n\t\t\tpointInTriangle( ax, ay, bx, by, cx, cy, n.x, n.y ) && area( n.prev, n, n.next ) >= 0 ) return false;\n\t\tn = n.nextZ;\n\n\t}\n\n\treturn true;\n\n}\n\n// go through all polygon nodes and cure small local self-intersections\nfunction cureLocalIntersections( start, triangles, dim ) {\n\n\tlet p = start;\n\tdo {\n\n\t\tconst a = p.prev,\n\t\t\tb = p.next.next;\n\n\t\tif ( ! equals( a, b ) && intersects( a, p, p.next, b ) && locallyInside( a, b ) && locallyInside( b, a ) ) {\n\n\t\t\ttriangles.push( a.i / dim | 0 );\n\t\t\ttriangles.push( p.i / dim | 0 );\n\t\t\ttriangles.push( b.i / dim | 0 );\n\n\t\t\t// remove two nodes involved\n\t\t\tremoveNode( p );\n\t\t\tremoveNode( p.next );\n\n\t\t\tp = start = b;\n\n\t\t}\n\n\t\tp = p.next;\n\n\t} while ( p !== start );\n\n\treturn filterPoints( p );\n\n}\n\n// try splitting polygon into two and triangulate them independently\nfunction splitEarcut( start, triangles, dim, minX, minY, invSize ) {\n\n\t// look for a valid diagonal that divides the polygon into two\n\tlet a = start;\n\tdo {\n\n\t\tlet b = a.next.next;\n\t\twhile ( b !== a.prev ) {\n\n\t\t\tif ( a.i !== b.i && isValidDiagonal( a, b ) ) {\n\n\t\t\t\t// split the polygon in two by the diagonal\n\t\t\t\tlet c = splitPolygon( a, b );\n\n\t\t\t\t// filter colinear points around the cuts\n\t\t\t\ta = filterPoints( a, a.next );\n\t\t\t\tc = filterPoints( c, c.next );\n\n\t\t\t\t// run earcut on each half\n\t\t\t\tearcutLinked( a, triangles, dim, minX, minY, invSize, 0 );\n\t\t\t\tearcutLinked( c, triangles, dim, minX, minY, invSize, 0 );\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t\tb = b.next;\n\n\t\t}\n\n\t\ta = a.next;\n\n\t} while ( a !== start );\n\n}\n\n// link every hole into the outer loop, producing a single-ring polygon without holes\nfunction eliminateHoles( data, holeIndices, outerNode, dim ) {\n\n\tconst queue = [];\n\tlet i, len, start, end, list;\n\n\tfor ( i = 0, len = holeIndices.length; i < len; i ++ ) {\n\n\t\tstart = holeIndices[ i ] * dim;\n\t\tend = i < len - 1 ? holeIndices[ i + 1 ] * dim : data.length;\n\t\tlist = linkedList( data, start, end, dim, false );\n\t\tif ( list === list.next ) list.steiner = true;\n\t\tqueue.push( getLeftmost( list ) );\n\n\t}\n\n\tqueue.sort( compareX );\n\n\t// process holes from left to right\n\tfor ( i = 0; i < queue.length; i ++ ) {\n\n\t\touterNode = eliminateHole( queue[ i ], outerNode );\n\n\t}\n\n\treturn outerNode;\n\n}\n\nfunction compareX( a, b ) {\n\n\treturn a.x - b.x;\n\n}\n\n// find a bridge between vertices that connects hole with an outer ring and link it\nfunction eliminateHole( hole, outerNode ) {\n\n\tconst bridge = findHoleBridge( hole, outerNode );\n\tif ( ! bridge ) {\n\n\t\treturn outerNode;\n\n\t}\n\n\tconst bridgeReverse = splitPolygon( bridge, hole );\n\n\t// filter collinear points around the cuts\n\tfilterPoints( bridgeReverse, bridgeReverse.next );\n\treturn filterPoints( bridge, bridge.next );\n\n}\n\n// David Eberly's algorithm for finding a bridge between hole and outer polygon\nfunction findHoleBridge( hole, outerNode ) {\n\n\tlet p = outerNode,\n\t\tqx = - Infinity,\n\t\tm;\n\n\tconst hx = hole.x, hy = hole.y;\n\n\t// find a segment intersected by a ray from the hole's leftmost point to the left;\n\t// segment's endpoint with lesser x will be potential connection point\n\tdo {\n\n\t\tif ( hy <= p.y && hy >= p.next.y && p.next.y !== p.y ) {\n\n\t\t\tconst x = p.x + ( hy - p.y ) * ( p.next.x - p.x ) / ( p.next.y - p.y );\n\t\t\tif ( x <= hx && x > qx ) {\n\n\t\t\t\tqx = x;\n\t\t\t\tm = p.x < p.next.x ? p : p.next;\n\t\t\t\tif ( x === hx ) return m; // hole touches outer segment; pick leftmost endpoint\n\n\t\t\t}\n\n\t\t}\n\n\t\tp = p.next;\n\n\t} while ( p !== outerNode );\n\n\tif ( ! m ) return null;\n\n\t// look for points inside the triangle of hole point, segment intersection and endpoint;\n\t// if there are no points found, we have a valid connection;\n\t// otherwise choose the point of the minimum angle with the ray as connection point\n\n\tconst stop = m,\n\t\tmx = m.x,\n\t\tmy = m.y;\n\tlet tanMin = Infinity, tan;\n\n\tp = m;\n\n\tdo {\n\n\t\tif ( hx >= p.x && p.x >= mx && hx !== p.x &&\n\t\t\t\tpointInTriangle( hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y ) ) {\n\n\t\t\ttan = Math.abs( hy - p.y ) / ( hx - p.x ); // tangential\n\n\t\t\tif ( locallyInside( p, hole ) && ( tan < tanMin || ( tan === tanMin && ( p.x > m.x || ( p.x === m.x && sectorContainsSector( m, p ) ) ) ) ) ) {\n\n\t\t\t\tm = p;\n\t\t\t\ttanMin = tan;\n\n\t\t\t}\n\n\t\t}\n\n\t\tp = p.next;\n\n\t} while ( p !== stop );\n\n\treturn m;\n\n}\n\n// whether sector in vertex m contains sector in vertex p in the same coordinates\nfunction sectorContainsSector( m, p ) {\n\n\treturn area( m.prev, m, p.prev ) < 0 && area( p.next, m, m.next ) < 0;\n\n}\n\n// interlink polygon nodes in z-order\nfunction indexCurve( start, minX, minY, invSize ) {\n\n\tlet p = start;\n\tdo {\n\n\t\tif ( p.z === 0 ) p.z = zOrder( p.x, p.y, minX, minY, invSize );\n\t\tp.prevZ = p.prev;\n\t\tp.nextZ = p.next;\n\t\tp = p.next;\n\n\t} while ( p !== start );\n\n\tp.prevZ.nextZ = null;\n\tp.prevZ = null;\n\n\tsortLinked( p );\n\n}\n\n// Simon Tatham's linked list merge sort algorithm\n// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html\nfunction sortLinked( list ) {\n\n\tlet i, p, q, e, tail, numMerges, pSize, qSize,\n\t\tinSize = 1;\n\n\tdo {\n\n\t\tp = list;\n\t\tlist = null;\n\t\ttail = null;\n\t\tnumMerges = 0;\n\n\t\twhile ( p ) {\n\n\t\t\tnumMerges ++;\n\t\t\tq = p;\n\t\t\tpSize = 0;\n\t\t\tfor ( i = 0; i < inSize; i ++ ) {\n\n\t\t\t\tpSize ++;\n\t\t\t\tq = q.nextZ;\n\t\t\t\tif ( ! q ) break;\n\n\t\t\t}\n\n\t\t\tqSize = inSize;\n\n\t\t\twhile ( pSize > 0 || ( qSize > 0 && q ) ) {\n\n\t\t\t\tif ( pSize !== 0 && ( qSize === 0 || ! q || p.z <= q.z ) ) {\n\n\t\t\t\t\te = p;\n\t\t\t\t\tp = p.nextZ;\n\t\t\t\t\tpSize --;\n\n\t\t\t\t} else {\n\n\t\t\t\t\te = q;\n\t\t\t\t\tq = q.nextZ;\n\t\t\t\t\tqSize --;\n\n\t\t\t\t}\n\n\t\t\t\tif ( tail ) tail.nextZ = e;\n\t\t\t\telse list = e;\n\n\t\t\t\te.prevZ = tail;\n\t\t\t\ttail = e;\n\n\t\t\t}\n\n\t\t\tp = q;\n\n\t\t}\n\n\t\ttail.nextZ = null;\n\t\tinSize *= 2;\n\n\t} while ( numMerges > 1 );\n\n\treturn list;\n\n}\n\n// z-order of a point given coords and inverse of the longer side of data bbox\nfunction zOrder( x, y, minX, minY, invSize ) {\n\n\t// coords are transformed into non-negative 15-bit integer range\n\tx = ( x - minX ) * invSize | 0;\n\ty = ( y - minY ) * invSize | 0;\n\n\tx = ( x | ( x << 8 ) ) & 0x00FF00FF;\n\tx = ( x | ( x << 4 ) ) & 0x0F0F0F0F;\n\tx = ( x | ( x << 2 ) ) & 0x33333333;\n\tx = ( x | ( x << 1 ) ) & 0x55555555;\n\n\ty = ( y | ( y << 8 ) ) & 0x00FF00FF;\n\ty = ( y | ( y << 4 ) ) & 0x0F0F0F0F;\n\ty = ( y | ( y << 2 ) ) & 0x33333333;\n\ty = ( y | ( y << 1 ) ) & 0x55555555;\n\n\treturn x | ( y << 1 );\n\n}\n\n// find the leftmost node of a polygon ring\nfunction getLeftmost( start ) {\n\n\tlet p = start,\n\t\tleftmost = start;\n\tdo {\n\n\t\tif ( p.x < leftmost.x || ( p.x === leftmost.x && p.y < leftmost.y ) ) leftmost = p;\n\t\tp = p.next;\n\n\t} while ( p !== start );\n\n\treturn leftmost;\n\n}\n\n// check if a point lies within a convex triangle\nfunction pointInTriangle( ax, ay, bx, by, cx, cy, px, py ) {\n\n\treturn ( cx - px ) * ( ay - py ) >= ( ax - px ) * ( cy - py ) &&\n ( ax - px ) * ( by - py ) >= ( bx - px ) * ( ay - py ) &&\n ( bx - px ) * ( cy - py ) >= ( cx - px ) * ( by - py );\n\n}\n\n// check if a diagonal between two polygon nodes is valid (lies in polygon interior)\nfunction isValidDiagonal( a, b ) {\n\n\treturn a.next.i !== b.i && a.prev.i !== b.i && ! intersectsPolygon( a, b ) && // dones't intersect other edges\n ( locallyInside( a, b ) && locallyInside( b, a ) && middleInside( a, b ) && // locally visible\n ( area( a.prev, a, b.prev ) || area( a, b.prev, b ) ) || // does not create opposite-facing sectors\n equals( a, b ) && area( a.prev, a, a.next ) > 0 && area( b.prev, b, b.next ) > 0 ); // special zero-length case\n\n}\n\n// signed area of a triangle\nfunction area( p, q, r ) {\n\n\treturn ( q.y - p.y ) * ( r.x - q.x ) - ( q.x - p.x ) * ( r.y - q.y );\n\n}\n\n// check if two points are equal\nfunction equals( p1, p2 ) {\n\n\treturn p1.x === p2.x && p1.y === p2.y;\n\n}\n\n// check if two segments intersect\nfunction intersects( p1, q1, p2, q2 ) {\n\n\tconst o1 = sign( area( p1, q1, p2 ) );\n\tconst o2 = sign( area( p1, q1, q2 ) );\n\tconst o3 = sign( area( p2, q2, p1 ) );\n\tconst o4 = sign( area( p2, q2, q1 ) );\n\n\tif ( o1 !== o2 && o3 !== o4 ) return true; // general case\n\n\tif ( o1 === 0 && onSegment( p1, p2, q1 ) ) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1\n\tif ( o2 === 0 && onSegment( p1, q2, q1 ) ) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1\n\tif ( o3 === 0 && onSegment( p2, p1, q2 ) ) return true; // p2, q2 and p1 are collinear and p1 lies on p2q2\n\tif ( o4 === 0 && onSegment( p2, q1, q2 ) ) return true; // p2, q2 and q1 are collinear and q1 lies on p2q2\n\n\treturn false;\n\n}\n\n// for collinear points p, q, r, check if point q lies on segment pr\nfunction onSegment( p, q, r ) {\n\n\treturn q.x <= Math.max( p.x, r.x ) && q.x >= Math.min( p.x, r.x ) && q.y <= Math.max( p.y, r.y ) && q.y >= Math.min( p.y, r.y );\n\n}\n\nfunction sign( num ) {\n\n\treturn num > 0 ? 1 : num < 0 ? - 1 : 0;\n\n}\n\n// check if a polygon diagonal intersects any polygon segments\nfunction intersectsPolygon( a, b ) {\n\n\tlet p = a;\n\tdo {\n\n\t\tif ( p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&\n\t\t\tintersects( p, p.next, a, b ) ) return true;\n\t\tp = p.next;\n\n\t} while ( p !== a );\n\n\treturn false;\n\n}\n\n// check if a polygon diagonal is locally inside the polygon\nfunction locallyInside( a, b ) {\n\n\treturn area( a.prev, a, a.next ) < 0 ?\n\t\tarea( a, b, a.next ) >= 0 && area( a, a.prev, b ) >= 0 :\n\t\tarea( a, b, a.prev ) < 0 || area( a, a.next, b ) < 0;\n\n}\n\n// check if the middle point of a polygon diagonal is inside the polygon\nfunction middleInside( a, b ) {\n\n\tlet p = a,\n\t\tinside = false;\n\tconst px = ( a.x + b.x ) / 2,\n\t\tpy = ( a.y + b.y ) / 2;\n\tdo {\n\n\t\tif ( ( ( p.y > py ) !== ( p.next.y > py ) ) && p.next.y !== p.y &&\n\t\t\t( px < ( p.next.x - p.x ) * ( py - p.y ) / ( p.next.y - p.y ) + p.x ) )\n\t\t\tinside = ! inside;\n\t\tp = p.next;\n\n\t} while ( p !== a );\n\n\treturn inside;\n\n}\n\n// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;\n// if one belongs to the outer ring and another to a hole, it merges it into a single ring\nfunction splitPolygon( a, b ) {\n\n\tconst a2 = new Node( a.i, a.x, a.y ),\n\t\tb2 = new Node( b.i, b.x, b.y ),\n\t\tan = a.next,\n\t\tbp = b.prev;\n\n\ta.next = b;\n\tb.prev = a;\n\n\ta2.next = an;\n\tan.prev = a2;\n\n\tb2.next = a2;\n\ta2.prev = b2;\n\n\tbp.next = b2;\n\tb2.prev = bp;\n\n\treturn b2;\n\n}\n\n// create a node and optionally link it with previous one (in a circular doubly linked list)\nfunction insertNode( i, x, y, last ) {\n\n\tconst p = new Node( i, x, y );\n\n\tif ( ! last ) {\n\n\t\tp.prev = p;\n\t\tp.next = p;\n\n\t} else {\n\n\t\tp.next = last.next;\n\t\tp.prev = last;\n\t\tlast.next.prev = p;\n\t\tlast.next = p;\n\n\t}\n\n\treturn p;\n\n}\n\nfunction removeNode( p ) {\n\n\tp.next.prev = p.prev;\n\tp.prev.next = p.next;\n\n\tif ( p.prevZ ) p.prevZ.nextZ = p.nextZ;\n\tif ( p.nextZ ) p.nextZ.prevZ = p.prevZ;\n\n}\n\nfunction Node( i, x, y ) {\n\n\t// vertex index in coordinates array\n\tthis.i = i;\n\n\t// vertex coordinates\n\tthis.x = x;\n\tthis.y = y;\n\n\t// previous and next vertex nodes in a polygon ring\n\tthis.prev = null;\n\tthis.next = null;\n\n\t// z-order curve value\n\tthis.z = 0;\n\n\t// previous and next nodes in z-order\n\tthis.prevZ = null;\n\tthis.nextZ = null;\n\n\t// indicates whether this is a steiner point\n\tthis.steiner = false;\n\n}\n\nfunction signedArea( data, start, end, dim ) {\n\n\tlet sum = 0;\n\tfor ( let i = start, j = end - dim; i < end; i += dim ) {\n\n\t\tsum += ( data[ j ] - data[ i ] ) * ( data[ i + 1 ] + data[ j + 1 ] );\n\t\tj = i;\n\n\t}\n\n\treturn sum;\n\n}\n\nexport { Earcut };\n","/*\n * three.js r160 `ExtrudeGeometry` (MIT) for straight-edged shapes, ported line by line: the outline is sampled as `Shape.extractPoints` samples a\n * path of `LineCurve`s, triangulated by three's `ShapeUtils.triangulateShape` (Earcut), bevelled and walled in the same order, with the\n * `WorldUVGenerator` uvs and `computeVertexNormals` on the Float32 positions. Path extrusion (`extrudePath`) is not supported.\n */\nimport type { BevelSpec, GeometryGroup, PrimitiveBuffers, ShapeOutline, Vec2 } from '../types'\nimport { Earcut } from './earcut'\n\ninterface P {\n x: number\n y: number\n}\n\nconst equals = (a: P, b: P): boolean => a.x === b.x && a.y === b.y\n\n/** `LineCurve.getPoint(t)`. */\nconst linePoint = (v1: P, v2: P, t: number): P => (t === 1 ? { x: v2.x, y: v2.y } : { x: (v2.x - v1.x) * t + v1.x, y: (v2.y - v1.y) * t + v1.y })\n\n/** `Path.setFromPoints(points).getPoints()`: one `LineCurve` per edge, resolution 1, consecutive duplicates dropped, not closed. */\nfunction pathPoints(points: readonly Vec2[]): P[] {\n const out: P[] = []\n let last: P | undefined\n for (let i = 1; i < points.length; i++) {\n const v1 = { x: (points[i - 1] as Vec2)[0], y: (points[i - 1] as Vec2)[1] }\n const v2 = { x: (points[i] as Vec2)[0], y: (points[i] as Vec2)[1] }\n for (let d = 0; d <= 1; d++) {\n const point = linePoint(v1, v2, d / 1)\n if (last && equals(last, point)) continue\n out.push(point)\n last = point\n }\n }\n return out\n}\n\nfunction area(contour: readonly P[]): number {\n const n = contour.length\n let a = 0.0\n for (let p = n - 1, q = 0; q < n; p = q++) a += (contour[p] as P).x * (contour[q] as P).y - (contour[q] as P).x * (contour[p] as P).y\n return a * 0.5\n}\nconst isClockWise = (pts: readonly P[]): boolean => area(pts) < 0\n\nfunction removeDupEndPts(points: P[]): void {\n const l = points.length\n if (l > 2 && equals(points[l - 1] as P, points[0] as P)) points.pop()\n}\n\nfunction triangulateShape(contour: P[], holes: P[][]): number[][] {\n const vertices: number[] = []\n const holeIndices: number[] = []\n const faces: number[][] = []\n removeDupEndPts(contour)\n for (const p of contour) vertices.push(p.x, p.y)\n let holeIndex = contour.length\n holes.forEach(removeDupEndPts)\n for (let i = 0; i < holes.length; i++) {\n holeIndices.push(holeIndex)\n holeIndex += (holes[i] as P[]).length\n for (const p of holes[i] as P[]) vertices.push(p.x, p.y)\n }\n const triangles: number[] = Earcut.triangulate(vertices, holeIndices)\n for (let i = 0; i < triangles.length; i += 3) faces.push(triangles.slice(i, i + 3))\n return faces\n}\n\n/** `BufferGeometry.computeVertexNormals` for non-indexed Float32 positions, then `normalizeNormals`, both stored as Float32. */\nexport function computeVertexNormals(positions: Float32Array, indices?: ArrayLike<number>): Float32Array {\n const normals = new Float32Array(positions.length)\n const get = (array: Float32Array, i: number): [number, number, number] => [array[i * 3] as number, array[i * 3 + 1] as number, array[i * 3 + 2] as number]\n const face = (vA: number, vB: number, vC: number): [number, number, number] => {\n const pA = get(positions, vA)\n const pB = get(positions, vB)\n const pC = get(positions, vC)\n const cb = [pC[0] - pB[0], pC[1] - pB[1], pC[2] - pB[2]] as const\n const ab = [pA[0] - pB[0], pA[1] - pB[1], pA[2] - pB[2]] as const\n // Vector3.cross\n return [cb[1] * ab[2] - cb[2] * ab[1], cb[2] * ab[0] - cb[0] * ab[2], cb[0] * ab[1] - cb[1] * ab[0]]\n }\n const set = (i: number, x: number, y: number, z: number): void => {\n normals[i * 3] = x\n normals[i * 3 + 1] = y\n normals[i * 3 + 2] = z\n }\n if (indices) {\n for (let i = 0; i < indices.length; i += 3) {\n const vA = indices[i] as number\n const vB = indices[i + 1] as number\n const vC = indices[i + 2] as number\n const cb = face(vA, vB, vC)\n for (const v of [vA, vB, vC]) {\n const n = get(normals, v)\n set(v, n[0] + cb[0], n[1] + cb[1], n[2] + cb[2])\n }\n }\n } else {\n for (let i = 0; i < positions.length / 3; i += 3) {\n const cb = face(i, i + 1, i + 2)\n set(i, cb[0], cb[1], cb[2])\n set(i + 1, cb[0], cb[1], cb[2])\n set(i + 2, cb[0], cb[1], cb[2])\n }\n }\n for (let i = 0; i < normals.length / 3; i++) {\n const [x, y, z] = get(normals, i)\n const s = 1 / (Math.sqrt(x * x + y * y + z * z) || 1)\n set(i, x * s, y * s, z * s)\n }\n return normals\n}\n\nexport function extrude(outline: ShapeOutline, depth = 1, steps = 1, bevel: false | BevelSpec | undefined = undefined): PrimitiveBuffers {\n const parameters = { shape: outline, depth, steps, bevel: bevel ?? {} }\n const verticesArray: number[] = []\n const uvArray: number[] = []\n const groups: GeometryGroup[] = []\n const placeholder: number[] = []\n\n const bevelEnabled = bevel !== false\n const spec: BevelSpec = bevel === false || bevel === undefined ? {} : bevel\n let bevelThickness = spec.thickness !== undefined ? spec.thickness : 0.2\n let bevelSize = spec.size !== undefined ? spec.size : bevelThickness - 0.1\n let bevelOffset = spec.offset !== undefined ? spec.offset : 0\n let bevelSegments = spec.segments !== undefined ? spec.segments : 3\n if (!bevelEnabled) {\n bevelSegments = 0\n bevelThickness = 0\n bevelSize = 0\n bevelOffset = 0\n }\n\n let vertices = pathPoints(outline.outer)\n const holes = (outline.holes ?? []).map(pathPoints)\n const reverse = !isClockWise(vertices)\n if (reverse) {\n vertices = vertices.reverse()\n for (let h = 0; h < holes.length; h++) {\n const ahole = holes[h] as P[]\n if (isClockWise(ahole)) holes[h] = ahole.reverse()\n }\n }\n const faces = triangulateShape(vertices, holes)\n const contour = vertices\n for (const ahole of holes) vertices = vertices.concat(ahole)\n\n const scalePt2 = (pt: P, vec: P, size: number): P => ({ x: pt.x + vec.x * size, y: pt.y + vec.y * size })\n const vlen = vertices.length\n const flen = faces.length\n\n const getBevelVec = (inPt: P, inPrev: P, inNext: P): P => {\n let vTransX: number\n let vTransY: number\n let shrinkBy: number\n const vPrevX = inPt.x - inPrev.x\n const vPrevY = inPt.y - inPrev.y\n const vNextX = inNext.x - inPt.x\n const vNextY = inNext.y - inPt.y\n const vPrevLensq = vPrevX * vPrevX + vPrevY * vPrevY\n const collinear0 = vPrevX * vNextY - vPrevY * vNextX\n if (Math.abs(collinear0) > Number.EPSILON) {\n const vPrevLen = Math.sqrt(vPrevLensq)\n const vNextLen = Math.sqrt(vNextX * vNextX + vNextY * vNextY)\n const ptPrevShiftX = inPrev.x - vPrevY / vPrevLen\n const ptPrevShiftY = inPrev.y + vPrevX / vPrevLen\n const ptNextShiftX = inNext.x - vNextY / vNextLen\n const ptNextShiftY = inNext.y + vNextX / vNextLen\n const sf = ((ptNextShiftX - ptPrevShiftX) * vNextY - (ptNextShiftY - ptPrevShiftY) * vNextX) / (vPrevX * vNextY - vPrevY * vNextX)\n vTransX = ptPrevShiftX + vPrevX * sf - inPt.x\n vTransY = ptPrevShiftY + vPrevY * sf - inPt.y\n const vTransLensq = vTransX * vTransX + vTransY * vTransY\n if (vTransLensq <= 2) return { x: vTransX, y: vTransY }\n shrinkBy = Math.sqrt(vTransLensq / 2)\n } else {\n let directionEq = false\n if (vPrevX > Number.EPSILON) {\n if (vNextX > Number.EPSILON) directionEq = true\n } else if (vPrevX < -Number.EPSILON) {\n if (vNextX < -Number.EPSILON) directionEq = true\n } else if (Math.sign(vPrevY) === Math.sign(vNextY)) {\n directionEq = true\n }\n if (directionEq) {\n vTransX = -vPrevY\n vTransY = vPrevX\n shrinkBy = Math.sqrt(vPrevLensq)\n } else {\n vTransX = vPrevX\n vTransY = vPrevY\n shrinkBy = Math.sqrt(vPrevLensq / 2)\n }\n }\n return { x: vTransX / shrinkBy, y: vTransY / shrinkBy }\n }\n\n const movements = (ring: readonly P[]): P[] => {\n const out: P[] = []\n for (let i = 0, il = ring.length, j = il - 1, k = i + 1; i < il; i++, j++, k++) {\n if (j === il) j = 0\n if (k === il) k = 0\n out[i] = getBevelVec(ring[i] as P, ring[j] as P, ring[k] as P)\n }\n return out\n }\n const contourMovements = movements(contour)\n const holesMovements: P[][] = []\n let verticesMovements = contourMovements.concat()\n for (const ahole of holes) {\n const oneHoleMovements = movements(ahole)\n holesMovements.push(oneHoleMovements)\n verticesMovements = verticesMovements.concat(oneHoleMovements)\n }\n\n const v = (x: number, y: number, z: number): void => {\n placeholder.push(x)\n placeholder.push(y)\n placeholder.push(z)\n }\n const bevelLayer = (b: number, zOf: (z: number) => number): void => {\n const t = b / bevelSegments\n const z = bevelThickness * Math.cos((t * Math.PI) / 2)\n const bs = bevelSize * Math.sin((t * Math.PI) / 2) + bevelOffset\n for (let i = 0; i < contour.length; i++) {\n const vert = scalePt2(contour[i] as P, contourMovements[i] as P, bs)\n v(vert.x, vert.y, zOf(z))\n }\n for (let h = 0; h < holes.length; h++) {\n const ahole = holes[h] as P[]\n const oneHoleMovements = holesMovements[h] as P[]\n for (let i = 0; i < ahole.length; i++) {\n const vert = scalePt2(ahole[i] as P, oneHoleMovements[i] as P, bs)\n v(vert.x, vert.y, zOf(z))\n }\n }\n }\n for (let b = 0; b < bevelSegments; b++) bevelLayer(b, (z) => -z)\n const bs = bevelSize + bevelOffset\n for (let i = 0; i < vlen; i++) {\n const vert = bevelEnabled ? scalePt2(vertices[i] as P, verticesMovements[i] as P, bs) : (vertices[i] as P)\n v(vert.x, vert.y, 0)\n }\n for (let s = 1; s <= steps; s++) {\n for (let i = 0; i < vlen; i++) {\n const vert = bevelEnabled ? scalePt2(vertices[i] as P, verticesMovements[i] as P, bs) : (vertices[i] as P)\n v(vert.x, vert.y, (depth / steps) * s)\n }\n }\n for (let b = bevelSegments - 1; b >= 0; b--) bevelLayer(b, (z) => depth + z)\n\n const addVertex = (index: number): void => {\n verticesArray.push(placeholder[index * 3 + 0] as number)\n verticesArray.push(placeholder[index * 3 + 1] as number)\n verticesArray.push(placeholder[index * 3 + 2] as number)\n }\n const addUV = (x: number, y: number): void => {\n uvArray.push(x)\n uvArray.push(y)\n }\n const at = (index: number, axis: number): number => verticesArray[index * 3 + axis] as number\n const f3 = (a: number, b: number, c: number): void => {\n addVertex(a)\n addVertex(b)\n addVertex(c)\n const nextIndex = verticesArray.length / 3\n for (const index of [nextIndex - 3, nextIndex - 2, nextIndex - 1]) addUV(at(index, 0), at(index, 1))\n }\n const f4 = (a: number, b: number, c: number, d: number): void => {\n addVertex(a)\n addVertex(b)\n addVertex(d)\n addVertex(b)\n addVertex(c)\n addVertex(d)\n const nextIndex = verticesArray.length / 3\n const iA = nextIndex - 6\n const iB = nextIndex - 3\n const iC = nextIndex - 2\n const iD = nextIndex - 1\n const byX = Math.abs(at(iA, 1) - at(iB, 1)) < Math.abs(at(iA, 0) - at(iB, 0))\n const uv = (index: number): [number, number] => [byX ? at(index, 0) : at(index, 1), 1 - at(index, 2)]\n const uvs = [uv(iA), uv(iB), uv(iC), uv(iD)]\n for (const k of [0, 1, 3, 1, 2, 3]) addUV(...(uvs[k] as [number, number]))\n }\n\n // lid faces\n let start = verticesArray.length / 3\n if (bevelEnabled) {\n let offset = 0\n for (let i = 0; i < flen; i++) {\n const face = faces[i] as number[]\n f3((face[2] as number) + offset, (face[1] as number) + offset, (face[0] as number) + offset)\n }\n const layer = steps + bevelSegments * 2\n offset = vlen * layer\n for (let i = 0; i < flen; i++) {\n const face = faces[i] as number[]\n f3((face[0] as number) + offset, (face[1] as number) + offset, (face[2] as number) + offset)\n }\n } else {\n for (let i = 0; i < flen; i++) {\n const face = faces[i] as number[]\n f3(face[2] as number, face[1] as number, face[0] as number)\n }\n for (let i = 0; i < flen; i++) {\n const face = faces[i] as number[]\n f3((face[0] as number) + vlen * steps, (face[1] as number) + vlen * steps, (face[2] as number) + vlen * steps)\n }\n }\n groups.push({ start, count: verticesArray.length / 3 - start, materialIndex: 0 })\n\n // side faces\n start = verticesArray.length / 3\n const sidewalls = (ring: readonly P[], layeroffset: number): void => {\n let i = ring.length\n while (--i >= 0) {\n const j = i\n let k = i - 1\n if (k < 0) k = ring.length - 1\n for (let s = 0, sl = steps + bevelSegments * 2; s < sl; s++) {\n const slen1 = vlen * s\n const slen2 = vlen * (s + 1)\n f4(layeroffset + j + slen1, layeroffset + k + slen1, layeroffset + k + slen2, layeroffset + j + slen2)\n }\n }\n }\n let layeroffset = 0\n sidewalls(contour, layeroffset)\n layeroffset += contour.length\n for (const ahole of holes) {\n sidewalls(ahole, layeroffset)\n layeroffset += ahole.length\n }\n groups.push({ start, count: verticesArray.length / 3 - start, materialIndex: 1 })\n\n const positions = new Float32Array(verticesArray)\n return { type: 'ExtrudeGeometry', parameters, positions, normals: computeVertexNormals(positions), uvs: new Float32Array(uvArray), groups }\n}\n","import { canonicalJson } from '../hash'\nimport type { MeshNode, PrimitiveBuffers, PrimitiveNode } from '../types'\nimport { difference } from './difference'\nimport { computeVertexNormals, extrude } from './extrude'\nimport { box, circle, cone, cylinder, indexArray, lathe, plane, ring, sphere, torus } from './three-generators'\n\nexport { box, circle, cone, cylinder, lathe, plane, ring, sphere, torus } from './three-generators'\nexport { computeVertexNormals, extrude } from './extrude'\nexport { difference } from './difference'\n\n/** How a level of detail changes a primitive. */\nexport interface DetailOptions {\n /** Multiplies the segment counts of round primitives (1 leaves them as given). */\n readonly detail?: number\n /** Floor of the scaled segment counts (default 3). */\n readonly minSegments?: number\n}\n\nconst PARAMETER_KEYS: Readonly<Record<PrimitiveNode['kind'], readonly string[]>> = {\n box: ['size', 'segments'],\n cylinder: ['radiusTop', 'radiusBottom', 'height', 'radialSegments', 'heightSegments', 'openEnded', 'thetaStart', 'thetaLength'],\n cone: ['radius', 'height', 'radialSegments', 'heightSegments', 'openEnded', 'thetaStart', 'thetaLength'],\n sphere: ['radius', 'widthSegments', 'heightSegments', 'phiStart', 'phiLength', 'thetaStart', 'thetaLength'],\n plane: ['width', 'height', 'widthSegments', 'heightSegments'],\n circle: ['radius', 'segments', 'thetaStart', 'thetaLength'],\n ring: ['innerRadius', 'outerRadius', 'thetaSegments', 'phiSegments', 'thetaStart', 'thetaLength'],\n lathe: ['points', 'segments', 'phiStart', 'phiLength'],\n torus: ['radius', 'tube', 'radialSegments', 'tubularSegments', 'arc'],\n extrude: ['shape', 'depth', 'steps', 'bevel'],\n difference: ['size', 'subtract'],\n mesh: ['positions', 'normals', 'uvs', 'indices'],\n}\n\n/** The segment fields `detail` scales, per kind. */\nconst SEGMENT_KEYS: Readonly<Partial<Record<PrimitiveNode['kind'], readonly string[]>>> = {\n cylinder: ['radialSegments'],\n cone: ['radialSegments'],\n sphere: ['widthSegments', 'heightSegments'],\n circle: ['segments'],\n ring: ['thetaSegments'],\n lathe: ['segments'],\n torus: ['radialSegments', 'tubularSegments'],\n}\n/** three's defaults of the segment fields, so a scaled default is scaled from the right number. */\nconst SEGMENT_DEFAULTS: Readonly<Record<string, number>> = { radialSegments: 32, widthSegments: 32, heightSegments: 16, segments: 12, thetaSegments: 32, tubularSegments: 48 }\nconst segmentDefault = (kind: string, key: string): number => (kind === 'circle' && key === 'segments' ? 32 : kind === 'torus' && key === 'radialSegments' ? 12 : (SEGMENT_DEFAULTS[key] as number))\n\n/** The parameters of a primitive node (no transform, colour or name), with the level of detail applied: what its geometry depends on. */\nexport function primitiveParameters(node: PrimitiveNode, options: DetailOptions = {}): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n const source = node as unknown as Record<string, unknown>\n for (const key of PARAMETER_KEYS[node.kind]) if (source[key] !== undefined) out[key] = source[key]\n const detail = options.detail ?? 1\n if (detail !== 1) {\n const floor = options.minSegments ?? 3\n for (const key of SEGMENT_KEYS[node.kind] ?? []) {\n const given = (out[key] as number | undefined) ?? segmentDefault(node.kind, key)\n out[key] = Math.max(floor, Math.round(given * detail))\n }\n }\n return out\n}\n\nfunction mesh(p: MeshNode): PrimitiveBuffers {\n const positions = new Float32Array(p.positions as number[])\n const indices = p.indices ? indexArray(p.indices as number[]) : undefined\n return {\n type: 'BufferGeometry',\n parameters: {},\n positions,\n normals: p.normals ? new Float32Array(p.normals as number[]) : computeVertexNormals(positions, indices),\n uvs: p.uvs ? new Float32Array(p.uvs as number[]) : new Float32Array((positions.length / 3) * 2),\n ...(indices ? { indices } : {}),\n groups: [],\n }\n}\n\n/** Builds a node whose parameters were already resolved (`primitiveParameters` merged over it, so the detail level is applied). */\nfunction make(p: PrimitiveNode): PrimitiveBuffers {\n switch (p.kind) {\n case 'box': {\n const size = p.size ?? [1, 1, 1]\n const segments = p.segments ?? [1, 1, 1]\n return box(size[0], size[1], size[2], segments[0], segments[1], segments[2])\n }\n case 'cylinder':\n return cylinder(p.radiusTop, p.radiusBottom, p.height, p.radialSegments, p.heightSegments, p.openEnded, p.thetaStart, p.thetaLength)\n case 'cone':\n return cone(p.radius, p.height, p.radialSegments, p.heightSegments, p.openEnded, p.thetaStart, p.thetaLength)\n case 'sphere':\n return sphere(p.radius, p.widthSegments, p.heightSegments, p.phiStart, p.phiLength, p.thetaStart, p.thetaLength)\n case 'plane':\n return plane(p.width, p.height, p.widthSegments, p.heightSegments)\n case 'circle':\n return circle(p.radius, p.segments, p.thetaStart, p.thetaLength)\n case 'ring':\n return ring(p.innerRadius, p.outerRadius, p.thetaSegments, p.phiSegments, p.thetaStart, p.thetaLength)\n case 'lathe':\n return lathe(p.points, p.segments, p.phiStart, p.phiLength)\n case 'torus':\n return torus(p.radius, p.tube, p.radialSegments, p.tubularSegments, p.arc)\n case 'extrude':\n return extrude(p.shape, p.depth, p.steps, p.bevel)\n case 'difference':\n return difference(p.size, p.subtract ?? [])\n case 'mesh':\n return mesh(p)\n default:\n throw new Error(`game-shapes: unknown primitive kind \"${String((p as { kind: unknown }).kind)}\"`)\n }\n}\n\n/** Templates by their parameter text: the same cylinder is generated once per process. Read-only by contract. */\nconst templates = new Map<string, PrimitiveBuffers>()\nlet templateLimit = 4096\nconst stats = { hits: 0, misses: 0 }\n\n/** Sets how many primitive templates are kept (least recently used out; 0 turns the cache off). */\nexport function setTemplateCacheSize(size: number): void {\n templateLimit = Math.max(0, Math.floor(size))\n while (templates.size > templateLimit) templates.delete(templates.keys().next().value as string)\n}\nexport function clearTemplateCache(): void {\n templates.clear()\n stats.hits = 0\n stats.misses = 0\n}\nexport const templateCacheStats = (): { size: number; hits: number; misses: number } => ({ size: templates.size, ...stats })\n\n/**\n * The geometry of one primitive on its own, not baked: the arrays three's generator of the same kind builds, bit for bit (meshes and differences\n * are `BufferGeometry`). The result comes from a shared template cache and must not be written to (copy it with `clonePrimitive`).\n */\nexport function buildPrimitive(node: PrimitiveNode, options: DetailOptions = {}): PrimitiveBuffers {\n const parameters = primitiveParameters(node, options)\n // raw meshes are not worth a key of their whole arrays; they are cheap to wrap\n if (node.kind === 'mesh' || templateLimit === 0) return make({ ...node, ...parameters } as PrimitiveNode)\n const key = `${node.kind}:${canonicalJson(parameters)}`\n const cached = templates.get(key)\n if (cached) {\n stats.hits++\n templates.delete(key)\n templates.set(key, cached)\n return cached\n }\n stats.misses++\n const built = make({ ...node, ...parameters } as PrimitiveNode)\n templates.set(key, built)\n if (templates.size > templateLimit) templates.delete(templates.keys().next().value as string)\n return built\n}\n\n/** A copy that owns its arrays. */\nexport function clonePrimitive(p: PrimitiveBuffers): PrimitiveBuffers {\n return {\n type: p.type,\n parameters: p.parameters,\n positions: p.positions.slice(),\n normals: p.normals.slice(),\n uvs: p.uvs.slice(),\n ...(p.indices ? { indices: p.indices.slice() } : {}),\n groups: p.groups.map((group) => ({ ...group })),\n }\n}\n","import { hash64 } from './hash'\n\n/** mulberry32: a small, fast 32-bit seeded stream in [0, 1). */\nexport function mulberry32(seed: number): () => number {\n let state = seed >>> 0\n return () => {\n state = (state + 0x6d2b79f5) >>> 0\n let t = state\n t = Math.imul(t ^ (t >>> 15), t | 1)\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n }\n}\n\n/** The stream of one node: its own per (seed, generator, path), so adding a node never shifts another node's draws. */\nexport const streamFor = (seed: number | string, generator: string, path: string): (() => number) =>\n mulberry32(parseInt(hash64(`${String(seed)}\\u0000${generator}\\u0000${path}`).slice(0, 8), 16))\n","import type { ShapeGenerator } from './types'\n\nconst GENERATOR_NAME = /^[a-zA-Z][\\w.:-]*$/\n\n/** The generators of one set: what `{ kind: 'custom', generator: name, params }` nodes look up. */\nexport interface ShapeGenerators {\n /**\n * Registers a generator: a pure function of its params and a seeded stream that returns a node. A worker that generates shapes with custom nodes must\n * register the same generators (import the same module in the worker script).\n */\n register(name: string, generator: ShapeGenerator): void\n unregister(name: string): boolean\n get(name: string): ShapeGenerator | undefined\n list(): string[]\n /** The generators by name, as `GenerateOptions.generators` takes them. */\n record(): Readonly<Record<string, ShapeGenerator>>\n}\n\nexport const generatorsOver = (generators: Map<string, ShapeGenerator>): ShapeGenerators => ({\n register(name, generator) {\n if (!GENERATOR_NAME.test(name)) throw new Error(`game-shapes: generator name \"${name}\" must start with a letter and use letters, digits, \".\", \":\", \"-\" or \"_\"`)\n generators.set(name, generator)\n },\n unregister: (name) => generators.delete(name),\n get: (name) => generators.get(name),\n list: () => [...generators.keys()].sort(),\n record: () => Object.fromEntries(generators),\n})\n\nlet defaultGenerators: ShapeGenerators | undefined\n\n/** The set behind `registerGenerator`, `getGenerator` and the rest of the global functions, created on first use. */\nexport const defaultShapeGenerators = (): ShapeGenerators => (defaultGenerators ??= generatorsOver(new Map()))\n\n/** @deprecated use `createShapes(...).register`. */\nexport const registerGenerator = (name: string, generator: ShapeGenerator): void => defaultShapeGenerators().register(name, generator)\n/** @deprecated use `createShapes(...).unregister`. */\nexport const unregisterGenerator = (name: string): boolean => defaultShapeGenerators().unregister(name)\n/** @deprecated use `createShapes(...).get`. */\nexport const getGenerator = (name: string): ShapeGenerator | undefined => defaultShapeGenerators().get(name)\n/** @deprecated use `createShapes(...).list`. */\nexport const listGenerators = (): string[] => defaultShapeGenerators().list()\n","{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://github.com/Andersonfrfilho/adatechnology-game-kit/packages/game-shapes/schema/shape.schema.json\",\n \"title\": \"game-shapes shape\",\n \"type\": \"object\",\n \"required\": [\n \"version\",\n \"root\"\n ],\n \"additionalProperties\": false,\n \"properties\": {\n \"version\": {\n \"const\": 1\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"materials\": {\n \"type\": \"object\",\n \"additionalProperties\": {\n \"$ref\": \"#/$defs/material\"\n }\n },\n \"root\": {\n \"$ref\": \"#/$defs/node\"\n },\n \"lod\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/lodLevel\"\n }\n },\n \"cullDistance\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n }\n },\n \"$defs\": {\n \"number\": {\n \"type\": \"number\"\n },\n \"count\": {\n \"type\": \"integer\",\n \"minimum\": 0\n },\n \"vec2\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"number\"\n },\n \"minItems\": 2,\n \"maxItems\": 2\n },\n \"vec3\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"number\"\n },\n \"minItems\": 3,\n \"maxItems\": 3\n },\n \"mat4\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"number\"\n },\n \"minItems\": 16,\n \"maxItems\": 16\n },\n \"color\": {\n \"oneOf\": [\n {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 16777215\n },\n {\n \"type\": \"string\",\n \"pattern\": \"^#[0-9a-fA-F]{6}$\"\n }\n ]\n },\n \"uvRect\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\n \"u0\",\n \"v0\",\n \"u1\",\n \"v1\"\n ],\n \"properties\": {\n \"u0\": {\n \"type\": \"number\"\n },\n \"v0\": {\n \"type\": \"number\"\n },\n \"u1\": {\n \"type\": \"number\"\n },\n \"v1\": {\n \"type\": \"number\"\n }\n }\n },\n \"rotationOrder\": {\n \"enum\": [\n \"XYZ\",\n \"YXZ\",\n \"ZXY\",\n \"ZYX\",\n \"YZX\",\n \"XZY\"\n ]\n },\n \"transform\": {\n \"type\": \"object\",\n \"properties\": {\n \"position\": {\n \"$ref\": \"#/$defs/vec3\"\n },\n \"rotation\": {\n \"$ref\": \"#/$defs/vec3\"\n },\n \"rotationOrder\": {\n \"$ref\": \"#/$defs/rotationOrder\"\n },\n \"scale\": {\n \"oneOf\": [\n {\n \"type\": \"number\"\n },\n {\n \"$ref\": \"#/$defs/vec3\"\n }\n ]\n },\n \"heading\": {\n \"type\": \"number\"\n },\n \"matrix\": {\n \"$ref\": \"#/$defs/mat4\"\n }\n }\n },\n \"instance\": {\n \"allOf\": [\n {\n \"$ref\": \"#/$defs/transform\"\n }\n ],\n \"properties\": {\n \"position\": true,\n \"rotation\": true,\n \"rotationOrder\": true,\n \"scale\": true,\n \"heading\": true,\n \"matrix\": true,\n \"color\": {\n \"$ref\": \"#/$defs/color\"\n }\n },\n \"unevaluatedProperties\": false\n },\n \"material\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"properties\": {\n \"color\": {\n \"$ref\": \"#/$defs/color\"\n },\n \"roughness\": {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n \"metalness\": {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n \"opacity\": {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n \"transparent\": {\n \"type\": \"boolean\"\n },\n \"emissive\": {\n \"$ref\": \"#/$defs/color\"\n },\n \"emissiveIntensity\": {\n \"type\": \"number\",\n \"minimum\": 0\n },\n \"side\": {\n \"enum\": [\n \"front\",\n \"back\",\n \"double\"\n ]\n },\n \"flatShading\": {\n \"type\": \"boolean\"\n },\n \"vertexColors\": {\n \"type\": \"boolean\"\n },\n \"depthWrite\": {\n \"type\": \"boolean\"\n }\n }\n },\n \"lodLevel\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\n \"distance\",\n \"detail\"\n ],\n \"properties\": {\n \"distance\": {\n \"type\": \"number\",\n \"minimum\": 0\n },\n \"detail\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0,\n \"maximum\": 1\n },\n \"minSize\": {\n \"type\": \"number\",\n \"minimum\": 0\n },\n \"minSegments\": {\n \"type\": \"integer\",\n \"minimum\": 3\n }\n }\n },\n \"common\": {\n \"allOf\": [\n {\n \"$ref\": \"#/$defs/transform\"\n }\n ],\n \"properties\": {\n \"kind\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"part\": {\n \"type\": \"string\"\n },\n \"material\": {\n \"type\": \"string\"\n },\n \"color\": {\n \"$ref\": \"#/$defs/color\"\n },\n \"uv\": {\n \"$ref\": \"#/$defs/uvRect\"\n },\n \"instances\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/instance\"\n }\n },\n \"minDetail\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0,\n \"maximum\": 1\n },\n \"tags\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n },\n \"node\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\"\n ],\n \"allOf\": [\n {\n \"$ref\": \"#/$defs/common\"\n }\n ],\n \"oneOf\": [\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"group\"\n },\n \"children\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/node\"\n }\n }\n }\n },\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"box\"\n },\n \"size\": {\n \"$ref\": \"#/$defs/vec3\"\n },\n \"segments\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"minItems\": 3,\n \"maxItems\": 3\n }\n }\n },\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"cylinder\"\n },\n \"radiusTop\": {\n \"type\": \"number\"\n },\n \"radiusBottom\": {\n \"type\": \"number\"\n },\n \"height\": {\n \"type\": \"number\"\n },\n \"radialSegments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"heightSegments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"openEnded\": {\n \"type\": \"boolean\"\n },\n \"thetaStart\": {\n \"type\": \"number\"\n },\n \"thetaLength\": {\n \"type\": \"number\"\n }\n }\n },\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"cone\"\n },\n \"radius\": {\n \"type\": \"number\"\n },\n \"height\": {\n \"type\": \"number\"\n },\n \"radialSegments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"heightSegments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"openEnded\": {\n \"type\": \"boolean\"\n },\n \"thetaStart\": {\n \"type\": \"number\"\n },\n \"thetaLength\": {\n \"type\": \"number\"\n }\n }\n },\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"sphere\"\n },\n \"radius\": {\n \"type\": \"number\"\n },\n \"widthSegments\": {\n \"type\": \"integer\",\n \"minimum\": 3\n },\n \"heightSegments\": {\n \"type\": \"integer\",\n \"minimum\": 2\n },\n \"phiStart\": {\n \"type\": \"number\"\n },\n \"phiLength\": {\n \"type\": \"number\"\n },\n \"thetaStart\": {\n \"type\": \"number\"\n },\n \"thetaLength\": {\n \"type\": \"number\"\n }\n }\n },\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"plane\"\n },\n \"width\": {\n \"type\": \"number\"\n },\n \"height\": {\n \"type\": \"number\"\n },\n \"widthSegments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"heightSegments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n }\n }\n },\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"circle\"\n },\n \"radius\": {\n \"type\": \"number\"\n },\n \"segments\": {\n \"type\": \"integer\",\n \"minimum\": 3\n },\n \"thetaStart\": {\n \"type\": \"number\"\n },\n \"thetaLength\": {\n \"type\": \"number\"\n }\n }\n },\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"ring\"\n },\n \"innerRadius\": {\n \"type\": \"number\"\n },\n \"outerRadius\": {\n \"type\": \"number\"\n },\n \"thetaSegments\": {\n \"type\": \"integer\",\n \"minimum\": 3\n },\n \"phiSegments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"thetaStart\": {\n \"type\": \"number\"\n },\n \"thetaLength\": {\n \"type\": \"number\"\n }\n }\n },\n {\n \"required\": [\n \"points\"\n ],\n \"properties\": {\n \"kind\": {\n \"const\": \"lathe\"\n },\n \"points\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/vec2\"\n },\n \"minItems\": 2\n },\n \"segments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"phiStart\": {\n \"type\": \"number\"\n },\n \"phiLength\": {\n \"type\": \"number\"\n }\n }\n },\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"torus\"\n },\n \"radius\": {\n \"type\": \"number\"\n },\n \"tube\": {\n \"type\": \"number\"\n },\n \"radialSegments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"tubularSegments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"arc\": {\n \"type\": \"number\"\n }\n }\n },\n {\n \"required\": [\n \"shape\"\n ],\n \"properties\": {\n \"kind\": {\n \"const\": \"extrude\"\n },\n \"shape\": {\n \"type\": \"object\",\n \"required\": [\n \"outer\"\n ],\n \"additionalProperties\": false,\n \"properties\": {\n \"outer\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/vec2\"\n },\n \"minItems\": 3\n },\n \"holes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/vec2\"\n },\n \"minItems\": 3\n }\n }\n }\n },\n \"depth\": {\n \"type\": \"number\"\n },\n \"steps\": {\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"bevel\": {\n \"oneOf\": [\n {\n \"const\": false\n },\n {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"properties\": {\n \"thickness\": {\n \"type\": \"number\"\n },\n \"size\": {\n \"type\": \"number\"\n },\n \"offset\": {\n \"type\": \"number\"\n },\n \"segments\": {\n \"type\": \"integer\",\n \"minimum\": 1\n }\n }\n }\n ]\n }\n }\n },\n {\n \"required\": [\n \"size\",\n \"subtract\"\n ],\n \"properties\": {\n \"kind\": {\n \"const\": \"difference\"\n },\n \"size\": {\n \"$ref\": \"#/$defs/vec3\"\n },\n \"subtract\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"min\",\n \"max\"\n ],\n \"additionalProperties\": false,\n \"properties\": {\n \"min\": {\n \"$ref\": \"#/$defs/vec2\"\n },\n \"max\": {\n \"$ref\": \"#/$defs/vec2\"\n }\n }\n }\n }\n }\n },\n {\n \"required\": [\n \"positions\"\n ],\n \"properties\": {\n \"kind\": {\n \"const\": \"mesh\"\n },\n \"positions\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"number\"\n }\n },\n \"normals\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"number\"\n }\n },\n \"uvs\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"number\"\n }\n },\n \"indices\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/count\"\n }\n }\n }\n },\n {\n \"properties\": {\n \"kind\": {\n \"const\": \"point\"\n },\n \"data\": {}\n }\n },\n {\n \"required\": [\n \"generator\"\n ],\n \"properties\": {\n \"kind\": {\n \"const\": \"custom\"\n },\n \"generator\": {\n \"type\": \"string\"\n },\n \"params\": {}\n }\n }\n ]\n }\n }\n}\n","import schemaJson from '../schema/shape.schema.json'\nimport type { ShapeSchema } from './types'\n\n/** The JSON Schema (draft 2020-12) of a shape document; the same file ships as `@adatechnology/game-shapes/schema.json`. */\nexport const SHAPE_SCHEMA: Readonly<Record<string, unknown>> = schemaJson\n\ntype Check = (value: unknown) => string | null\n\nconst num: Check = (v) => (typeof v === 'number' && Number.isFinite(v) ? null : 'expected a finite number')\nconst bool: Check = (v) => (typeof v === 'boolean' ? null : 'expected a boolean')\nconst str: Check = (v) => (typeof v === 'string' ? null : 'expected a string')\nconst int = (min: number): Check => (v) => (typeof v === 'number' && Number.isInteger(v) && v >= min ? null : `expected an integer >= ${min}`)\nconst range = (lo: number, hi: number, loOpen = false): Check => (v) =>\n typeof v === 'number' && (loOpen ? v > lo : v >= lo) && v <= hi ? null : `expected a number in ${loOpen ? '(' : '['}${lo}, ${hi}]`\nconst tuple = (n: number, item: Check = num): Check => (v) => {\n if (!Array.isArray(v) || v.length !== n) return `expected an array of ${n}`\n for (const x of v) if (item(x)) return `expected an array of ${n} numbers`\n return null\n}\nconst list = (item: Check, min = 0): Check => (v) => {\n if (!Array.isArray(v)) return 'expected an array'\n if (v.length < min) return `expected at least ${min} items`\n for (let i = 0; i < v.length; i++) {\n const e = item(v[i])\n if (e) return `[${i}]: ${e}`\n }\n return null\n}\nconst color: Check = (v) =>\n (typeof v === 'number' && Number.isInteger(v) && v >= 0 && v <= 0xffffff) || (typeof v === 'string' && /^#[0-9a-fA-F]{6}$/.test(v)) ? null : 'expected 0xRRGGBB or \"#rrggbb\"'\nconst oneOf = (values: readonly unknown[]): Check => (v) => (values.includes(v) ? null : `expected one of ${values.map((x) => JSON.stringify(x)).join(', ')}`)\nconst object = (fields: Readonly<Record<string, Check>>, required: readonly string[] = []): Check => (v) => {\n if (!v || typeof v !== 'object' || Array.isArray(v)) return 'expected an object'\n const record = v as Record<string, unknown>\n for (const key of required) if (record[key] === undefined) return `${key}: required`\n for (const key of Object.keys(record)) {\n if (record[key] === undefined) continue\n const check = fields[key]\n if (!check) return `${key}: unknown field`\n const e = check(record[key])\n if (e) return `${key}: ${e}`\n }\n return null\n}\n\nconst ORDERS = ['XYZ', 'YXZ', 'ZXY', 'ZYX', 'YZX', 'XZY'] as const\nconst TRANSFORM: Record<string, Check> = {\n position: tuple(3),\n rotation: tuple(3),\n rotationOrder: oneOf(ORDERS),\n scale: (v) => (typeof v === 'number' ? num(v) : tuple(3)(v)),\n heading: num,\n matrix: tuple(16),\n}\nconst UV = object({ u0: num, v0: num, u1: num, v1: num }, ['u0', 'v0', 'u1', 'v1'])\nconst INSTANCE = object({ ...TRANSFORM, color })\nconst COMMON: Record<string, Check> = {\n ...TRANSFORM,\n kind: str,\n name: str,\n part: str,\n material: str,\n color,\n uv: UV,\n instances: list(INSTANCE),\n minDetail: range(0, 1, true),\n tags: list(str),\n}\nconst vec2 = tuple(2)\nconst BEVEL: Check = (v) => (v === false ? null : object({ thickness: num, size: num, offset: num, segments: int(1) })(v))\nconst KINDS: Readonly<Record<string, { fields: Record<string, Check>; required?: string[] }>> = {\n group: { fields: {} },\n box: { fields: { size: tuple(3), segments: tuple(3, int(1)) } },\n cylinder: { fields: { radiusTop: num, radiusBottom: num, height: num, radialSegments: int(1), heightSegments: int(1), openEnded: bool, thetaStart: num, thetaLength: num } },\n cone: { fields: { radius: num, height: num, radialSegments: int(1), heightSegments: int(1), openEnded: bool, thetaStart: num, thetaLength: num } },\n sphere: { fields: { radius: num, widthSegments: int(3), heightSegments: int(2), phiStart: num, phiLength: num, thetaStart: num, thetaLength: num } },\n plane: { fields: { width: num, height: num, widthSegments: int(1), heightSegments: int(1) } },\n circle: { fields: { radius: num, segments: int(3), thetaStart: num, thetaLength: num } },\n ring: { fields: { innerRadius: num, outerRadius: num, thetaSegments: int(3), phiSegments: int(1), thetaStart: num, thetaLength: num } },\n lathe: { fields: { points: list(vec2, 2), segments: int(1), phiStart: num, phiLength: num }, required: ['points'] },\n torus: { fields: { radius: num, tube: num, radialSegments: int(1), tubularSegments: int(1), arc: num } },\n extrude: {\n fields: { shape: object({ outer: list(vec2, 3), holes: list(list(vec2, 3)) }, ['outer']), depth: num, steps: int(1), bevel: BEVEL },\n required: ['shape'],\n },\n difference: { fields: { size: tuple(3), subtract: list(object({ min: vec2, max: vec2 }, ['min', 'max'])) }, required: ['size', 'subtract'] },\n mesh: {\n fields: {\n positions: (v) => list(num)(v) ?? ((v as unknown[]).length % 3 === 0 ? null : 'expected a multiple of 3 numbers'),\n normals: list(num),\n uvs: list(num),\n indices: list(int(0)),\n },\n required: ['positions'],\n },\n point: { fields: { data: () => null } },\n custom: { fields: { generator: str, params: () => null }, required: ['generator'] },\n}\n\nfunction validateNode(node: unknown, path: string, errors: string[]): void {\n if (!node || typeof node !== 'object' || Array.isArray(node)) {\n errors.push(`${path}: expected a node object`)\n return\n }\n const record = node as Record<string, unknown>\n const spec = typeof record.kind === 'string' ? KINDS[record.kind] : undefined\n if (!spec) {\n errors.push(`${path}.kind: expected one of ${Object.keys(KINDS).join(', ')}`)\n return\n }\n for (const key of spec.required ?? []) if (record[key] === undefined) errors.push(`${path}.${key}: required`)\n for (const key of Object.keys(record)) {\n if (record[key] === undefined || key === 'children') continue\n const check = spec.fields[key] ?? COMMON[key]\n if (!check) {\n errors.push(`${path}.${key}: unknown field for a ${String(record.kind)}`)\n continue\n }\n const e = check(record[key])\n if (e) errors.push(`${path}.${key}: ${e}`)\n }\n const exclusive = ['matrix', 'heading'].filter((key) => record[key] !== undefined)\n if (record.matrix !== undefined && (record.position !== undefined || record.rotation !== undefined || record.scale !== undefined || record.heading !== undefined))\n errors.push(`${path}.matrix: cannot be combined with position, rotation, scale or heading`)\n else if (exclusive.includes('heading') && (record.rotation !== undefined || record.scale !== undefined)) errors.push(`${path}.heading: cannot be combined with rotation or scale`)\n if (record.children !== undefined) {\n if (record.kind !== 'group') errors.push(`${path}.children: only a group has children`)\n else if (!Array.isArray(record.children)) errors.push(`${path}.children: expected an array`)\n else record.children.forEach((child, index) => validateNode(child, `${path}.children[${index}]`, errors))\n }\n}\n\n/** Every rule of the JSON Schema, checked without a schema engine: one message per problem, with its path (`root.children[3].radialSegments: ...`). */\nexport function validateShape(schema: unknown): string[] {\n const errors: string[] = []\n if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return ['expected a shape object']\n const record = schema as Record<string, unknown>\n for (const key of Object.keys(record)) if (!['version', 'name', 'materials', 'root', 'lod', 'cullDistance'].includes(key) && record[key] !== undefined) errors.push(`${key}: unknown field`)\n if (record.version !== 1) errors.push('version: expected 1')\n if (record.name !== undefined && typeof record.name !== 'string') errors.push('name: expected a string')\n if (record.materials !== undefined) {\n if (!record.materials || typeof record.materials !== 'object' || Array.isArray(record.materials)) errors.push('materials: expected an object')\n else {\n const material = object({\n color,\n roughness: range(0, 1),\n metalness: range(0, 1),\n opacity: range(0, 1),\n transparent: bool,\n emissive: color,\n emissiveIntensity: (v) => (typeof v === 'number' && v >= 0 ? null : 'expected a number >= 0'),\n side: oneOf(['front', 'back', 'double']),\n flatShading: bool,\n vertexColors: bool,\n depthWrite: bool,\n })\n for (const [id, hint] of Object.entries(record.materials)) {\n const e = material(hint)\n if (e) errors.push(`materials.${id}: ${e}`)\n }\n }\n }\n if (record.lod !== undefined) {\n const e = list(object({ distance: (v) => (typeof v === 'number' && v >= 0 ? null : 'expected a number >= 0'), detail: range(0, 1, true), minSize: (v) => (typeof v === 'number' && v >= 0 ? null : 'expected a number >= 0'), minSegments: int(3) }, ['distance', 'detail']))(record.lod)\n if (e) errors.push(`lod${e.startsWith('[') ? '' : ': '}${e}`)\n }\n if (record.cullDistance !== undefined && !(typeof record.cullDistance === 'number' && record.cullDistance > 0)) errors.push('cullDistance: expected a number > 0')\n if (record.root === undefined) errors.push('root: required')\n else validateNode(record.root, 'root', errors)\n return errors\n}\n\n/** Throws with every problem when the schema is not valid. */\nexport function assertValidShape(schema: unknown): asserts schema is ShapeSchema {\n const errors = validateShape(schema)\n if (errors.length) throw new Error(`game-shapes: invalid shape\\n ${errors.slice(0, 20).join('\\n ')}${errors.length > 20 ? `\\n ... ${errors.length - 20} more` : ''}`)\n}\n","import { colorChannels, colorHex } from './color'\nimport { hashValue } from './hash'\nimport { asMat4, identity4, isAffine, matrixOf, multiply, normalMatrix, transformPoint } from './math'\nimport type { Matrix4 } from './math'\nimport { buildPrimitive, primitiveParameters } from './primitives'\nimport { canonicalJson } from './hash'\nimport { streamFor } from './random'\nimport { getGenerator } from './registry'\nimport { assertValidShape } from './schema'\nimport type {\n Bounds,\n GenerateOptions,\n InstancedBatch,\n NamedRange,\n PrimitiveBuffers,\n PrimitiveNode,\n ShapeBatch,\n ShapeBuffers,\n ShapeLevel,\n ShapeNode,\n ShapePart,\n ShapePoint,\n ShapeSchema,\n UvRect,\n} from './types'\n\nconst PRIMITIVE_KINDS = new Set(['box', 'cylinder', 'cone', 'sphere', 'plane', 'circle', 'ring', 'lathe', 'torus', 'extrude', 'difference', 'mesh'])\nexport const isPrimitiveNode = (node: ShapeNode): node is PrimitiveNode => PRIMITIVE_KINDS.has(node.kind)\n\n/** The options that change the output, as they enter the cache key. */\nexport function keyOptions(options: GenerateOptions = {}): Record<string, unknown> {\n return {\n seed: options.seed ?? 0,\n colors: options.colors ?? 'linear',\n indexed: options.indexed ?? true,\n materials: options.materials,\n fallbackMaterial: options.fallbackMaterial,\n materialAlias: options.materialAlias,\n instancing: options.instancing,\n levels: options.levels,\n }\n}\n\n/** The cache key of a shape under some options: 64-bit FNV-1a of the canonical schema text and the options that change the output. */\nexport const shapeKey = (schema: ShapeSchema, options: GenerateOptions = {}): string => hashValue({ schema, options: keyOptions(options) })\n\nconst IDENTITY_UV: UvRect = { u0: 0, v0: 0, u1: 1, v1: 1 }\n\nclass FloatList {\n array: Float32Array\n length = 0\n constructor(capacity = 256) {\n this.array = new Float32Array(capacity)\n }\n reserve(extra: number): void {\n if (this.length + extra <= this.array.length) return\n let capacity = this.array.length * 2\n while (capacity < this.length + extra) capacity *= 2\n const next = new Float32Array(capacity)\n next.set(this.array.subarray(0, this.length))\n this.array = next\n }\n done(): Float32Array {\n return this.array.slice(0, this.length)\n }\n}\nclass IndexList {\n array: Uint32Array\n length = 0\n constructor(capacity = 256) {\n this.array = new Uint32Array(capacity)\n }\n reserve(extra: number): void {\n if (this.length + extra <= this.array.length) return\n let capacity = this.array.length * 2\n while (capacity < this.length + extra) capacity *= 2\n const next = new Uint32Array(capacity)\n next.set(this.array.subarray(0, this.length))\n this.array = next\n }\n}\n\n/** Growing buffers of one (target, material). */\nclass Accumulator {\n readonly positions = new FloatList()\n readonly normals = new FloatList()\n readonly uvs = new FloatList(170)\n readonly colors = new FloatList()\n readonly indices = new IndexList()\n readonly ranges: NamedRange[] = []\n vertices = 0\n constructor(\n readonly material: string,\n readonly part: string | null,\n ) {}\n}\n\ninterface Target {\n readonly part: string | null\n readonly accs: Map<string, Accumulator>\n}\n\ninterface Settings {\n readonly colors: 'linear' | 'srgb' | 'none'\n readonly indexed: boolean\n readonly materials?: ReadonlySet<string>\n readonly fallback?: string\n readonly alias?: Readonly<Record<string, string>>\n readonly seed: number | string\n readonly generators?: GenerateOptions['generators']\n readonly isolatedGenerators?: boolean\n readonly autoMin?: number\n}\n\ninterface LevelSpec {\n readonly distance: number\n readonly detail: number\n readonly minSize?: number\n readonly minSegments?: number\n readonly index: number\n}\n\ninterface Inherited {\n readonly material: string\n readonly color: number\n readonly uv: UvRect\n readonly target: Target\n}\n\n/**\n * Bakes a template into an accumulator: positions by the world matrix, normals by its normal matrix and normalised, uvs into the atlas cell,\n * one colour for all, indices offset. The arithmetic is three's (`createMeshApi`, `BufferGeometry.applyMatrix4`), so the bits match.\n */\nfunction bake(acc: Accumulator, t: PrimitiveBuffers, world: readonly number[], rgb: readonly number[] | null, u: UvRect, indexed: boolean): void {\n const e = world\n const nm = normalMatrix(world)\n const affine = isAffine(world)\n const source = t.positions\n const count = source.length / 3\n const order: ArrayLike<number> | null = indexed ? null : t.indices ?? null\n const emitted = order ? order.length : count\n acc.positions.reserve(emitted * 3)\n acc.normals.reserve(emitted * 3)\n acc.uvs.reserve(emitted * 2)\n if (rgb) acc.colors.reserve(emitted * 3)\n const P = acc.positions.array\n const N = acc.normals.array\n const U = acc.uvs.array\n const C = acc.colors.array\n let pi = acc.positions.length\n let ni = acc.normals.length\n let ui = acc.uvs.length\n let ci = acc.colors.length\n const du = u.u1 - u.u0\n const dv = u.v1 - u.v0\n const tn = t.normals\n const tu = t.uvs\n for (let k = 0; k < emitted; k++) {\n const i = order ? (order[k] as number) : k\n const x = source[i * 3] as number\n const y = source[i * 3 + 1] as number\n const z = source[i * 3 + 2] as number\n if (affine) {\n P[pi++] = e[0]! * x + e[4]! * y + e[8]! * z + e[12]!\n P[pi++] = e[1]! * x + e[5]! * y + e[9]! * z + e[13]!\n P[pi++] = e[2]! * x + e[6]! * y + e[10]! * z + e[14]!\n } else {\n const p = transformPoint(e, x, y, z)\n P[pi++] = p[0]\n P[pi++] = p[1]\n P[pi++] = p[2]\n }\n const nx = tn[i * 3] as number\n const ny = tn[i * 3 + 1] as number\n const nz = tn[i * 3 + 2] as number\n const vx = nm[0]! * nx + nm[3]! * ny + nm[6]! * nz\n const vy = nm[1]! * nx + nm[4]! * ny + nm[7]! * nz\n const vz = nm[2]! * nx + nm[5]! * ny + nm[8]! * nz\n const s = 1 / (Math.sqrt(vx * vx + vy * vy + vz * vz) || 1)\n N[ni++] = vx * s\n N[ni++] = vy * s\n N[ni++] = vz * s\n U[ui++] = u.u0 + (tu[i * 2] as number) * du\n U[ui++] = u.v0 + (tu[i * 2 + 1] as number) * dv\n if (rgb) {\n C[ci++] = rgb[0]!\n C[ci++] = rgb[1]!\n C[ci++] = rgb[2]!\n }\n }\n acc.positions.length = pi\n acc.normals.length = ni\n acc.uvs.length = ui\n acc.colors.length = ci\n if (indexed) {\n const base = acc.vertices\n const idx = t.indices\n const n = idx ? idx.length : count\n acc.indices.reserve(n)\n const I = acc.indices.array\n let ii = acc.indices.length\n for (let k = 0; k < n; k++) I[ii++] = base + (idx ? (idx[k] as number) : k)\n acc.indices.length = ii\n }\n acc.vertices += emitted\n}\n\nfunction boundsOf(positions: Float32Array): Bounds {\n const min: [number, number, number] = [Infinity, Infinity, Infinity]\n const max: [number, number, number] = [-Infinity, -Infinity, -Infinity]\n for (let i = 0; i < positions.length; i += 3) {\n for (let axis = 0; axis < 3; axis++) {\n const v = positions[i + axis] as number\n if (v < min[axis]!) min[axis] = v\n if (v > max[axis]!) max[axis] = v\n }\n }\n return { min, max }\n}\n\nfunction finish(acc: Accumulator, indexed: boolean, withColors: boolean): ShapeBatch {\n const positions = acc.positions.done()\n const indices = indexed ? (acc.vertices > 65535 ? acc.indices.array.slice(0, acc.indices.length) : Uint16Array.from(acc.indices.array.subarray(0, acc.indices.length))) : undefined\n return {\n material: acc.material,\n part: acc.part,\n positions,\n normals: acc.normals.done(),\n uvs: acc.uvs.done(),\n ...(withColors ? { colors: acc.colors.done() } : {}),\n ...(indices ? { indices } : {}),\n ranges: acc.ranges,\n bounds: boundsOf(positions),\n }\n}\n\nconst radiusOf = (t: PrimitiveBuffers, world: readonly number[]): number => {\n let r = 0\n const p = t.positions\n for (let i = 0; i < p.length; i += 3) r = Math.max(r, Math.hypot(p[i] as number, p[i + 1] as number, p[i + 2] as number))\n const sx = Math.hypot(world[0]!, world[1]!, world[2]!)\n const sy = Math.hypot(world[4]!, world[5]!, world[6]!)\n const sz = Math.hypot(world[8]!, world[9]!, world[10]!)\n return r * Math.max(sx, sy, sz)\n}\n\ninterface LevelResult {\n readonly level: ShapeLevel\n readonly parts: ShapePart[]\n readonly points: ShapePoint[]\n readonly primitives: number\n}\n\n/** Expands a custom node into the node its generator returns (wrapped so that the custom node's own fields still apply). */\nfunction expandCustom(node: Extract<ShapeNode, { kind: 'custom' }>, settings: Settings, detail: number, path: string): ShapeNode {\n const generator = settings.generators?.[node.generator] ?? (settings.isolatedGenerators ? undefined : getGenerator(node.generator))\n if (!generator) throw new Error(`game-shapes: no generator \"${node.generator}\" (register it with registerGenerator, in the worker too) at ${path}`)\n const generated = generator(node.params, { random: streamFor(settings.seed, node.generator, path), seed: settings.seed, detail, path })\n const { kind: _kind, generator: _generator, params: _params, ...common } = node\n return { ...common, kind: 'group', children: [generated] }\n}\n\n/** The key two leaf primitives share when one can stand for the other as an instance. */\nconst leafKey = (node: PrimitiveNode, material: string, color: number, uv: UvRect, level: LevelSpec): string =>\n `${node.kind}|${canonicalJson(primitiveParameters(node, level))}|${material}|${color}|${uv.u0},${uv.v0},${uv.u1},${uv.v1}`\n\nfunction runLevel(schema: ShapeSchema, settings: Settings, level: LevelSpec): LevelResult {\n const bakeParts = level.index > 0\n const root: Target = { part: null, accs: new Map() }\n const targets: Target[] = [root]\n const partTargets = new Map<string, Target>()\n const parts: ShapePart[] = []\n const points: ShapePoint[] = []\n const instanced: InstancedBatch[] = []\n let primitives = 0\n const space = settings.colors === 'none' ? null : settings.colors\n const channels = new Map<number, readonly number[]>()\n const rgbOf = (hex: number): readonly number[] | null => {\n if (!space) return null\n let c = channels.get(hex)\n if (!c) {\n c = colorChannels(hex, space)\n channels.set(hex, c)\n }\n return c\n }\n const resolveMaterial = (id: string): string => {\n let out = id\n if (settings.materials && !settings.materials.has(out)) out = settings.fallback ?? out\n return settings.alias?.[out] ?? out\n }\n const accOf = (target: Target, material: string): Accumulator => {\n let acc = target.accs.get(material)\n if (!acc) {\n acc = new Accumulator(material, target.part)\n target.accs.set(material, acc)\n }\n return acc\n }\n\n // automatic instancing: count the leaves first\n const autoCounts = new Map<string, number>()\n const autoGroups = new Map<string, { node: PrimitiveNode; material: string; color: number; part: string | null; matrices: number[] }>()\n\n const count = (node: ShapeNode, inherited: { material: string; color: number; uv: UvRect; inPart: boolean }, path: string): void => {\n if (node.minDetail !== undefined && level.detail < node.minDetail) return\n if (node.kind === 'custom') return count(expandCustom(node, settings, level.detail, path), inherited, path)\n const next = {\n material: node.material !== undefined ? resolveMaterial(node.material) : inherited.material,\n color: node.color !== undefined ? colorHex(node.color) : inherited.color,\n uv: node.uv ?? inherited.uv,\n inPart: inherited.inPart || (node.part !== undefined && !bakeParts),\n }\n if (node.instances) return\n if (node.kind === 'group') {\n node.children?.forEach((child, index) => count(child, next, `${path}/${index}`))\n return\n }\n if (isPrimitiveNode(node) && node.name === undefined && !next.inPart) {\n const key = leafKey(node, next.material, next.color, next.uv, level)\n autoCounts.set(key, (autoCounts.get(key) ?? 0) + 1)\n }\n }\n const rootMaterial = resolveMaterial('default')\n if (settings.autoMin !== undefined) count(schema.root, { material: rootMaterial, color: 0xffffff, uv: IDENTITY_UV, inPart: false }, 'root')\n\n /** Draws a subtree into `collector` (a target) under `parent` (null: identity). */\n const visit = (node: ShapeNode, parent: Matrix4 | null, inherited: Inherited, path: string): void => {\n if (node.minDetail !== undefined && level.detail < node.minDetail) return\n if (node.kind === 'custom') return visit(expandCustom(node, settings, level.detail, path), parent, inherited, path)\n let target = inherited.target\n let parentWorld = parent\n if (node.part !== undefined) {\n if (bakeParts) {\n // far levels freeze the parts at rest: the part's pivot is relative to the root\n parentWorld = null\n } else {\n let partTarget = partTargets.get(node.part)\n if (!partTarget) {\n partTarget = { part: node.part, accs: new Map() }\n partTargets.set(node.part, partTarget)\n targets.push(partTarget)\n const { position, rotation, rotationOrder, scale, heading, matrix } = node\n parts.push({\n id: node.part,\n ...(position ? { position } : {}),\n ...(rotation ? { rotation } : {}),\n ...(rotationOrder ? { rotationOrder } : {}),\n ...(scale !== undefined ? { scale } : {}),\n ...(heading !== undefined ? { heading } : {}),\n ...(matrix ? { matrix } : { matrix: asMat4(matrixOf(node)) }),\n })\n }\n target = partTarget\n }\n }\n const isPart = node.part !== undefined && !bakeParts\n const local = isPart ? null : matrixOf(node)\n const world = isPart ? null : local ? (parentWorld ? multiply(parentWorld, local) : local) : parentWorld\n const next: Inherited = {\n material: node.material !== undefined ? resolveMaterial(node.material) : inherited.material,\n color: node.color !== undefined ? colorHex(node.color) : inherited.color,\n uv: node.uv ?? inherited.uv,\n target,\n }\n\n if (node.instances) {\n drawInstanced(node, parentWorld, next, path)\n return\n }\n\n const snapshot = node.name !== undefined ? new Map([...target.accs].map(([m, a]) => [m, { v: a.vertices, i: a.indices.length }])) : null\n\n if (node.kind === 'group') {\n node.children?.forEach((child, index) => visit(child, world, next, `${path}/${index}`))\n } else if (node.kind === 'point') {\n const p = node.position ?? [0, 0, 0]\n points.push({\n ...(node.name !== undefined ? { name: node.name } : {}),\n part: target.part,\n position: parent ? transformPoint(parent, p[0], p[1], p[2]) : [p[0], p[1], p[2]],\n ...(node.data !== undefined ? { data: node.data } : {}),\n ...(node.tags ? { tags: node.tags } : {}),\n })\n } else if (isPrimitiveNode(node)) {\n const template = buildPrimitive(node, level)\n const m = world ?? identity4()\n if (level.minSize !== undefined && radiusOf(template, m) < level.minSize) return\n primitives++\n if (settings.autoMin !== undefined && node.name === undefined && target.part === null) {\n const key = leafKey(node, next.material, next.color, next.uv, level)\n if ((autoCounts.get(key) ?? 0) >= settings.autoMin) {\n let group = autoGroups.get(key)\n if (!group) {\n group = { node, material: next.material, color: next.color, part: null, matrices: [] }\n autoGroups.set(key, group)\n }\n group.matrices.push(...m)\n return\n }\n }\n bake(accOf(target, next.material), template, m, rgbOf(next.color), next.uv, settings.indexed)\n }\n\n if (snapshot) {\n for (const [material, acc] of target.accs) {\n const before = snapshot.get(material) ?? { v: 0, i: 0 }\n if (acc.vertices === before.v) continue\n acc.ranges.push({\n name: node.name as string,\n ...(node.tags ? { tags: node.tags } : {}),\n vertexStart: before.v,\n vertexCount: acc.vertices - before.v,\n indexStart: before.i,\n indexCount: acc.indices.length - before.i,\n })\n }\n }\n }\n\n const drawInstanced = (node: ShapeNode, parentWorld: Matrix4 | null, inherited: Inherited, path: string): void => {\n const collector: Target = { part: inherited.target.part, accs: new Map() }\n const { instances, ...single } = node as ShapeNode & { instances: NonNullable<ShapeNode['instances']> }\n const before = parts.length\n visit(single as ShapeNode, null, { ...inherited, target: collector }, path)\n if (parts.length !== before) throw new Error(`game-shapes: an instanced node cannot hold parts (${path})`)\n const matrices = new Float32Array(instances.length * 16)\n const hasColors = instances.some((instance) => instance.color !== undefined)\n const colors = hasColors ? new Float32Array(instances.length * 3) : undefined\n instances.forEach((instance, index) => {\n const local = matrixOf(instance)\n const m = local ? (parentWorld ? multiply(parentWorld, local) : local) : parentWorld ?? identity4()\n matrices.set(m, index * 16)\n if (colors) colors.set(colorChannels(colorHex(instance.color), space ?? 'srgb'), index * 3)\n })\n for (const acc of collector.accs.values()) {\n const batch = finish(acc, settings.indexed, false)\n instanced.push({\n material: acc.material,\n part: collector.part,\n ...(node.name !== undefined ? { name: node.name } : {}),\n geometry: { type: 'BufferGeometry', parameters: {}, positions: batch.positions, normals: batch.normals, uvs: batch.uvs, ...(batch.indices ? { indices: batch.indices } : {}), groups: [] },\n matrices: matrices.slice(),\n ...(colors ? { colors: colors.slice() } : {}),\n count: instances.length,\n })\n }\n }\n\n visit(schema.root, null, { material: rootMaterial, color: 0xffffff, uv: IDENTITY_UV, target: root }, 'root')\n\n for (const group of autoGroups.values()) {\n const template = buildPrimitive(group.node, level)\n const n = group.matrices.length / 16\n const rgb = colorChannels(group.color, space ?? 'srgb')\n const colors = new Float32Array(n * 3)\n for (let i = 0; i < n; i++) colors.set(rgb, i * 3)\n instanced.push({\n material: group.material,\n part: null,\n geometry: { type: template.type, parameters: template.parameters, positions: template.positions.slice(), normals: template.normals.slice(), uvs: template.uvs.slice(), ...(template.indices ? { indices: template.indices.slice() } : {}), groups: template.groups.map((g) => ({ ...g })) },\n matrices: new Float32Array(group.matrices),\n ...(space ? { colors } : {}),\n count: n,\n })\n }\n\n const batches: ShapeBatch[] = []\n for (const target of targets) for (const acc of target.accs.values()) if (acc.vertices > 0 && (!settings.indexed || acc.indices.length > 0)) batches.push(finish(acc, settings.indexed, space !== null))\n return { level: { distance: level.distance, detail: level.detail, batches, instanced }, parts, points, primitives }\n}\n\n/**\n * Turns a shape into buffers: one batch per (part, material) per level, baked; instanced nodes as instance batches; parts with their pivots;\n * points. Pure: the same schema and options give the same bytes, here or in a worker.\n */\nexport function generate(schema: ShapeSchema, options: GenerateOptions = {}): ShapeBuffers {\n if (options.validate !== false) assertValidShape(schema)\n const settings: Settings = {\n colors: options.colors ?? 'linear',\n indexed: options.indexed ?? true,\n ...(options.materials ? { materials: new Set(options.materials) } : {}),\n ...(options.fallbackMaterial !== undefined ? { fallback: options.fallbackMaterial } : {}),\n ...(options.materialAlias ? { alias: options.materialAlias } : {}),\n seed: options.seed ?? 0,\n ...(options.generators ? { generators: options.generators } : {}),\n ...(options.isolatedGenerators ? { isolatedGenerators: true } : {}),\n ...(options.instancing ? { autoMin: Math.max(2, options.instancing.minCount) } : {}),\n }\n const specs: LevelSpec[] = [{ distance: 0, detail: 1, index: 0 }, ...(schema.lod ?? []).map((rule, index) => ({ ...rule, index: index + 1 }))]\n const wanted = options.levels ? specs.filter((spec) => options.levels!.includes(spec.index)) : specs\n let parts: ShapePart[] = []\n let points: ShapePoint[] = []\n let primitives = 0\n const levels: ShapeLevel[] = []\n for (const spec of wanted) {\n const result = runLevel(schema, settings, spec)\n if (spec.index === 0 || levels.length === 0) {\n parts = result.parts\n points = result.points\n primitives = result.primitives\n }\n levels.push(result.level)\n }\n const first = levels[0]\n const vertices = first ? first.batches.reduce((sum, batch) => sum + batch.positions.length / 3, 0) : 0\n const triangles = first\n ? first.batches.reduce((sum, batch) => sum + (batch.indices ? batch.indices.length : batch.positions.length / 3) / 3, 0) +\n first.instanced.reduce((sum, batch) => sum + ((batch.geometry.indices ? batch.geometry.indices.length : batch.geometry.positions.length / 3) / 3) * batch.count, 0)\n : 0\n return {\n key: options.key === false ? '' : options.key ?? shapeKey(schema, options),\n ...(schema.name !== undefined ? { name: schema.name } : {}),\n levels,\n parts,\n points,\n ...(schema.materials ? { materials: schema.materials } : {}),\n ...(schema.cullDistance !== undefined ? { cullDistance: schema.cullDistance } : {}),\n stats: { vertices, triangles, batches: first ? first.batches.length : 0, instances: first ? first.instanced.reduce((sum, batch) => sum + batch.count, 0) : 0, primitives },\n }\n}\n","import { normalMatrix, transformPoint } from './math'\nimport type { InstancedBatch, PrimitiveBuffers, ShapeBatch, ShapeBuffers } from './types'\n\n/** One piece for `mergeTransformed`: arrays (any may be missing) and the matrix that takes them into the merged space. */\nexport interface TransformedPiece {\n readonly positions: ArrayLike<number>\n readonly normals?: ArrayLike<number>\n readonly uvs?: ArrayLike<number>\n readonly indices?: ArrayLike<number> | null\n /** Column-major 4 x 4. */\n readonly matrix: readonly number[]\n}\n\nexport interface MergedArrays {\n readonly positions: Float32Array\n readonly normals: Float32Array\n readonly uvs: Float32Array\n}\n\n/**\n * Joins pieces into one non-indexed set of arrays, each piece de-indexed, its missing normals and uvs zero-filled, its positions by\n * `Vector3.applyMatrix4` and its normals by the normal matrix and normalised: what three's `toNonIndexed` + `applyMatrix4` + a join do,\n * bit for bit (the port of `mergeByMaterial`).\n */\nexport function mergeTransformed(pieces: readonly TransformedPiece[]): MergedArrays {\n let total = 0\n for (const piece of pieces) total += piece.indices ? piece.indices.length : piece.positions.length / 3\n const positions = new Float32Array(total * 3)\n const normals = new Float32Array(total * 3)\n const uvs = new Float32Array(total * 2)\n let at = 0\n for (const piece of pieces) {\n const e = piece.matrix\n const nm = normalMatrix(e)\n const count = piece.indices ? piece.indices.length : piece.positions.length / 3\n // de-index into Float32 first, as toNonIndexed does, then transform the stored values\n const p = new Float32Array(count * 3)\n const n = new Float32Array(count * 3)\n for (let k = 0; k < count; k++) {\n const i = piece.indices ? (piece.indices[k] as number) : k\n for (let c = 0; c < 3; c++) {\n p[k * 3 + c] = piece.positions[i * 3 + c] as number\n if (piece.normals) n[k * 3 + c] = piece.normals[i * 3 + c] as number\n }\n if (piece.uvs) {\n uvs[(at + k) * 2] = piece.uvs[i * 2] as number\n uvs[(at + k) * 2 + 1] = piece.uvs[i * 2 + 1] as number\n }\n }\n for (let k = 0; k < count; k++) {\n const q = transformPoint(e, p[k * 3] as number, p[k * 3 + 1] as number, p[k * 3 + 2] as number)\n positions[(at + k) * 3] = q[0]\n positions[(at + k) * 3 + 1] = q[1]\n positions[(at + k) * 3 + 2] = q[2]\n const x = n[k * 3] as number\n const y = n[k * 3 + 1] as number\n const z = n[k * 3 + 2] as number\n const vx = nm[0]! * x + nm[3]! * y + nm[6]! * z\n const vy = nm[1]! * x + nm[4]! * y + nm[7]! * z\n const vz = nm[2]! * x + nm[5]! * y + nm[8]! * z\n const s = 1 / (Math.sqrt(vx * vx + vy * vy + vz * vz) || 1)\n normals[(at + k) * 3] = vx * s\n normals[(at + k) * 3 + 1] = vy * s\n normals[(at + k) * 3 + 2] = vz * s\n }\n at += count\n }\n return { positions, normals, uvs }\n}\n\nconst primitiveArrays = (p: PrimitiveBuffers): ArrayBuffer[] => [p.positions, p.normals, p.uvs, ...(p.indices ? [p.indices] : [])].map((a) => a.buffer as ArrayBuffer)\nconst batchArrays = (b: ShapeBatch): ArrayBuffer[] => [b.positions, b.normals, b.uvs, ...(b.colors ? [b.colors] : []), ...(b.indices ? [b.indices] : [])].map((a) => a.buffer as ArrayBuffer)\nconst instancedArrays = (b: InstancedBatch): ArrayBuffer[] => [...primitiveArrays(b.geometry), b.matrices.buffer as ArrayBuffer, ...(b.colors ? [b.colors.buffer as ArrayBuffer] : [])]\n\n/** Every `ArrayBuffer` of a result, for `postMessage(buffers, transferablesOf(buffers))`. */\nexport function transferablesOf(buffers: ShapeBuffers): ArrayBuffer[] {\n const out = new Set<ArrayBuffer>()\n for (const level of buffers.levels) {\n for (const batch of level.batches) for (const a of batchArrays(batch)) out.add(a)\n for (const batch of level.instanced) for (const a of instancedArrays(batch)) out.add(a)\n }\n return [...out]\n}\n\nconst copyPrimitive = (p: PrimitiveBuffers): PrimitiveBuffers => ({\n ...p,\n positions: p.positions.slice(),\n normals: p.normals.slice(),\n uvs: p.uvs.slice(),\n ...(p.indices ? { indices: p.indices.slice() } : {}),\n groups: p.groups.map((g) => ({ ...g })),\n})\n\n/** A deep copy whose arrays are its own (cached results are shared and read-only; copy before deforming). */\nexport function cloneBuffers(buffers: ShapeBuffers): ShapeBuffers {\n return {\n ...buffers,\n levels: buffers.levels.map((level) => ({\n ...level,\n batches: level.batches.map((b) => ({\n ...b,\n positions: b.positions.slice(),\n normals: b.normals.slice(),\n uvs: b.uvs.slice(),\n ...(b.colors ? { colors: b.colors.slice() } : {}),\n ...(b.indices ? { indices: b.indices.slice() } : {}),\n ranges: b.ranges.map((r) => ({ ...r })),\n bounds: { min: [...b.bounds.min], max: [...b.bounds.max] },\n })),\n instanced: level.instanced.map((b) => ({ ...b, geometry: copyPrimitive(b.geometry), matrices: b.matrices.slice(), ...(b.colors ? { colors: b.colors.slice() } : {}) })),\n })),\n }\n}\n\n/** Bytes held by the arrays of a result (what the cache counts). */\nexport function byteSizeOf(buffers: ShapeBuffers): number {\n return transferablesOf(buffers).reduce((sum, buffer) => sum + buffer.byteLength, 0)\n}\n","import { generate } from './generate'\nimport { transferablesOf } from './merge'\nimport type { GenerateOptions, ShapeBuffers, ShapeGenerator, ShapeSchema } from './types'\n\n/** A job the pool posts: `options` without `generators` (functions do not cross threads; register them in the worker script). */\nexport interface ShapeJob {\n readonly id: number\n readonly schema: ShapeSchema\n readonly options?: Omit<GenerateOptions, 'generators'>\n}\n\nexport type ShapeReply = { readonly id: number; readonly ok: true; readonly buffers: ShapeBuffers } | { readonly id: number; readonly ok: false; readonly error: string }\n\n/** What a worker scope looks like: `self` in a browser worker, a `worker_threads` `parentPort` wrapper in Node, or an in-thread fake. */\nexport interface WorkerScopeLike {\n onmessage: ((event: { data: unknown }) => void) | null\n postMessage(message: unknown, transfer?: ArrayBuffer[]): void\n}\n\nexport interface ShapeWorkerOptions {\n /** Generators of the `custom` nodes, the same ones the main thread registers. */\n readonly generators?: Readonly<Record<string, ShapeGenerator>>\n /** Transfer the arrays back (default true: zero copy, the worker keeps nothing). */\n readonly transfer?: boolean\n}\n\n/**\n * The worker side of `createShapePool`: answers each job with `generate(schema, options)`, transferring every array.\n *\n * ```ts\n * // shape-worker.ts\n * import { installShapeWorker } from '@adatechnology/game-shapes/worker'\n * installShapeWorker(self as never, { generators: myGenerators })\n * ```\n */\nexport function installShapeWorker(scope: WorkerScopeLike, options: ShapeWorkerOptions = {}): void {\n const transfer = options.transfer !== false\n scope.onmessage = (event) => {\n const job = event.data as ShapeJob\n try {\n const buffers = generate(job.schema, { ...(job.options ?? {}), ...(options.generators ? { generators: options.generators } : {}) })\n const reply: ShapeReply = { id: job.id, ok: true, buffers }\n scope.postMessage(reply, transfer ? transferablesOf(buffers) : [])\n } catch (error) {\n const reply: ShapeReply = { id: job.id, ok: false, error: error instanceof Error ? error.message : String(error) }\n scope.postMessage(reply)\n }\n }\n}\n","import type { PrimitiveBuffers, ShapeBatch } from '@adatechnology/game-shapes'\nimport type * as THREE_NS from 'three'\n\nexport type Three = typeof THREE_NS\n\nexport interface ToGeometryOptions {\n /** Copy the arrays into the attributes (default true). Without a copy the geometry shares the buffers (a cached result is read-only: do not deform it). */\n readonly copy?: boolean\n}\n\n/** The order in which three's generator of each type sets its attributes (toJSON and attribute iteration keep it). */\nconst UV_BEFORE_NORMAL = new Set(['LatheGeometry', 'ExtrudeGeometry'])\n/** Types whose constructor sets the index before the attributes. */\nconst INDEX_FIRST = new Set(['BoxGeometry', 'CylinderGeometry', 'ConeGeometry', 'SphereGeometry', 'PlaneGeometry', 'CircleGeometry', 'RingGeometry', 'LatheGeometry', 'TorusGeometry'])\n\nconst isPrimitive = (value: PrimitiveBuffers | ShapeBatch): value is PrimitiveBuffers => typeof (value as PrimitiveBuffers).type === 'string'\n\n/** Three's own parameters object for a type (lathe points as `Vector2`). */\nfunction threeParameters(THREE: Three, p: PrimitiveBuffers): Record<string, unknown> {\n if (p.type === 'LatheGeometry') {\n const points = (p.parameters.points as Array<{ x: number; y: number }>).map(({ x, y }) => new THREE.Vector2(x, y))\n return { ...p.parameters, points }\n }\n return { ...p.parameters }\n}\n\n/**\n * A `BufferGeometry` from game-shapes buffers. A primitive becomes an instance of the three class it reproduces (`type`, `parameters`, prototype:\n * `instanceof THREE.CylinderGeometry`, `clone()` and `toJSON()` behave as three's), with the same attributes, index and groups. A batch gets\n * `position`, `normal`, `uv`, `color` (when present) and its index.\n */\nexport function toGeometry(THREE: Three, source: PrimitiveBuffers | ShapeBatch, options: ToGeometryOptions = {}): THREE_NS.BufferGeometry {\n const copy = options.copy !== false\n const primitive = isPrimitive(source) ? source : null\n const ThreeClass = primitive && primitive.type !== 'BufferGeometry' ? ((THREE as unknown as Record<string, unknown>)[primitive.type] as (new () => THREE_NS.BufferGeometry) | undefined) : undefined\n // the BufferGeometry constructor only, with the prototype of the class it stands for\n const geometry = (ThreeClass ? Reflect.construct(THREE.BufferGeometry, [], ThreeClass) : new THREE.BufferGeometry()) as THREE_NS.BufferGeometry & { parameters?: Record<string, unknown> }\n if (primitive && ThreeClass) {\n Object.assign(geometry, { type: primitive.type, parameters: threeParameters(THREE, primitive) })\n }\n const attribute = (array: Float32Array, size: number): THREE_NS.BufferAttribute => (copy ? new THREE.Float32BufferAttribute(array, size) : new THREE.BufferAttribute(array, size))\n const index = source.indices\n const setIndex = (): void => {\n if (!index) return\n geometry.setIndex(copy ? (index instanceof Uint32Array ? new THREE.Uint32BufferAttribute(index, 1) : new THREE.Uint16BufferAttribute(index, 1)) : new THREE.BufferAttribute(index, 1))\n }\n const type = primitive?.type ?? 'BufferGeometry'\n if (INDEX_FIRST.has(type)) setIndex()\n geometry.setAttribute('position', attribute(source.positions, 3))\n if (UV_BEFORE_NORMAL.has(type)) {\n geometry.setAttribute('uv', attribute(source.uvs, 2))\n geometry.setAttribute('normal', attribute(source.normals, 3))\n } else {\n geometry.setAttribute('normal', attribute(source.normals, 3))\n geometry.setAttribute('uv', attribute(source.uvs, 2))\n }\n if (!primitive && (source as ShapeBatch).colors) geometry.setAttribute('color', attribute((source as ShapeBatch).colors as Float32Array, 3))\n if (!INDEX_FIRST.has(type)) setIndex()\n if (primitive) for (const group of primitive.groups) geometry.addGroup(group.start, group.count, group.materialIndex)\n return geometry\n}\n","import { box, circle, cone, cylinder, extrude, lathe, plane, ring, sphere, torus } from '@adatechnology/game-shapes'\nimport type { BevelSpec, PrimitiveBuffers, PrimitiveNode, ShapeOutline, Vec2 } from '@adatechnology/game-shapes'\nimport { buildPrimitive } from '@adatechnology/game-shapes'\nimport type * as THREE_NS from 'three'\n\nimport { toGeometry } from './geometry'\nimport type { Three } from './geometry'\n\n/** The geometry constructors `shapeGeometries` replaces. */\nexport const SHAPE_GEOMETRY_TYPES = ['BoxGeometry', 'CylinderGeometry', 'ConeGeometry', 'SphereGeometry', 'PlaneGeometry', 'CircleGeometry', 'RingGeometry', 'LatheGeometry', 'TorusGeometry', 'ExtrudeGeometry'] as const\n\ntype Curve = { isLineCurve?: boolean; v1?: { x: number; y: number }; v2?: { x: number; y: number } }\ntype PathLike = { curves: Curve[]; autoClose?: boolean; holes?: PathLike[] }\n\n/** The point list of a path made only of contiguous `LineCurve`s (what `moveTo` + `lineTo` builds), or null. */\nfunction linePoints(path: PathLike): Vec2[] | null {\n if (path.autoClose || path.curves.length === 0) return null\n const points: Vec2[] = []\n for (const [index, curve] of path.curves.entries()) {\n if (!curve.isLineCurve || !curve.v1 || !curve.v2) return null\n if (index === 0) points.push([curve.v1.x, curve.v1.y])\n else {\n const last = points[points.length - 1] as Vec2\n if (last[0] !== curve.v1.x || last[1] !== curve.v1.y) return null\n }\n points.push([curve.v2.x, curve.v2.y])\n }\n return points\n}\n\ntype ExtrudeOptions = { depth?: number; steps?: number; bevelEnabled?: boolean; bevelThickness?: number; bevelSize?: number; bevelOffset?: number; bevelSegments?: number; extrudePath?: unknown; UVGenerator?: unknown; curveSegments?: number }\n\n/** An extrusion game-shapes can make exactly: one straight-edged shape, no path, the world uvs. */\nfunction extrudeOf(shapes: unknown, options: ExtrudeOptions = {}): PrimitiveBuffers | null {\n if (Array.isArray(shapes) || !shapes || options.extrudePath !== undefined || options.UVGenerator !== undefined) return null\n const shape = shapes as PathLike\n const outer = linePoints(shape)\n if (!outer) return null\n const holes: Vec2[][] = []\n for (const hole of shape.holes ?? []) {\n const points = linePoints(hole)\n if (!points) return null\n holes.push(points)\n }\n const outline: ShapeOutline = holes.length ? { outer, holes } : { outer }\n const bevel: false | BevelSpec =\n options.bevelEnabled === false\n ? false\n : {\n ...(options.bevelThickness !== undefined ? { thickness: options.bevelThickness } : {}),\n ...(options.bevelSize !== undefined ? { size: options.bevelSize } : {}),\n ...(options.bevelOffset !== undefined ? { offset: options.bevelOffset } : {}),\n ...(options.bevelSegments !== undefined ? { segments: options.bevelSegments } : {}),\n }\n return extrude(outline, options.depth, options.steps, bevel)\n}\n\nexport interface ShapeGeometriesOptions {\n /** Go through the template cache of game-shapes (default true): the same primitive is generated once per process. */\n readonly cache?: boolean\n}\n\nconst namespaces = new WeakMap<object, Map<string, Three>>()\n/** The namespaces this module made: wrapping one again gives it back (a package handing its `T` to another). */\nconst shims = new WeakSet<object>()\n\n/**\n * A three namespace whose `BoxGeometry`, `CylinderGeometry`, `ConeGeometry`, `SphereGeometry`, `PlaneGeometry`, `CircleGeometry`, `RingGeometry`,\n * `LatheGeometry`, `TorusGeometry` and (straight-edged) `ExtrudeGeometry` are made by game-shapes, bit-identical to three's and instances of three's\n * classes; everything else is three's own. Hand it to code ported from the teams as `T`, so it keeps reading `new T.CylinderGeometry(...)`.\n * An extrusion with curves, several shapes, a path or a custom uv generator falls back to three.\n */\nexport function shapeGeometries(THREE: Three, options: ShapeGeometriesOptions = {}): Three {\n if (shims.has(THREE)) return THREE\n const cache = options.cache !== false\n const key = cache ? 'cache' : 'nocache'\n let byKey = namespaces.get(THREE)\n if (!byKey) {\n byKey = new Map()\n namespaces.set(THREE, byKey)\n }\n const existing = byKey.get(key)\n if (existing) return existing\n const made = (p: PrimitiveBuffers): THREE_NS.BufferGeometry => toGeometry(THREE, p)\n const viaCache = (node: PrimitiveNode, direct: () => PrimitiveBuffers): PrimitiveBuffers => (cache ? buildPrimitive(node) : direct())\n const defined = <T extends object>(o: T): T => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined)) as T\n const constructors = {\n BoxGeometry: (width?: number, height?: number, depth?: number, ws = 1, hs = 1, ds = 1) =>\n made(viaCache({ kind: 'box', size: [width ?? 1, height ?? 1, depth ?? 1], segments: [ws, hs, ds] }, () => box(width, height, depth, ws, hs, ds))),\n CylinderGeometry: (radiusTop?: number, radiusBottom?: number, height?: number, radialSegments?: number, heightSegments?: number, openEnded?: boolean, thetaStart?: number, thetaLength?: number) =>\n made(viaCache({ kind: 'cylinder', ...defined({ radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength }) }, () => cylinder(radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength))),\n ConeGeometry: (radius?: number, height?: number, radialSegments?: number, heightSegments?: number, openEnded?: boolean, thetaStart?: number, thetaLength?: number) =>\n made(viaCache({ kind: 'cone', ...defined({ radius, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength }) }, () => cone(radius, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength))),\n SphereGeometry: (radius?: number, widthSegments?: number, heightSegments?: number, phiStart?: number, phiLength?: number, thetaStart?: number, thetaLength?: number) =>\n made(viaCache({ kind: 'sphere', ...defined({ radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength }) }, () => sphere(radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength))),\n PlaneGeometry: (width?: number, height?: number, widthSegments?: number, heightSegments?: number) =>\n made(viaCache({ kind: 'plane', ...defined({ width, height, widthSegments, heightSegments }) }, () => plane(width, height, widthSegments, heightSegments))),\n CircleGeometry: (radius?: number, segments?: number, thetaStart?: number, thetaLength?: number) =>\n made(viaCache({ kind: 'circle', ...defined({ radius, segments, thetaStart, thetaLength }) }, () => circle(radius, segments, thetaStart, thetaLength))),\n RingGeometry: (innerRadius?: number, outerRadius?: number, thetaSegments?: number, phiSegments?: number, thetaStart?: number, thetaLength?: number) =>\n made(viaCache({ kind: 'ring', ...defined({ innerRadius, outerRadius, thetaSegments, phiSegments, thetaStart, thetaLength }) }, () => ring(innerRadius, outerRadius, thetaSegments, phiSegments, thetaStart, thetaLength))),\n LatheGeometry: (points?: Array<{ x: number; y: number }>, segments?: number, phiStart?: number, phiLength?: number) => {\n if (!points) return new THREE.LatheGeometry(undefined, segments, phiStart, phiLength)\n const profile = points.map((p) => [p.x, p.y] as const)\n return made(viaCache({ kind: 'lathe', points: profile, ...defined({ segments, phiStart, phiLength }) }, () => lathe(profile, segments, phiStart, phiLength)))\n },\n TorusGeometry: (radius?: number, tube?: number, radialSegments?: number, tubularSegments?: number, arc?: number) =>\n made(viaCache({ kind: 'torus', ...defined({ radius, tube, radialSegments, tubularSegments, arc }) }, () => torus(radius, tube, radialSegments, tubularSegments, arc))),\n ExtrudeGeometry: (shapes?: unknown, extrudeOptions?: ExtrudeOptions) => {\n const buffers = shapes === undefined ? null : extrudeOf(shapes, extrudeOptions)\n if (!buffers) return new THREE.ExtrudeGeometry(shapes as THREE_NS.Shape, extrudeOptions as THREE_NS.ExtrudeGeometryOptions)\n const geometry = toGeometry(THREE, buffers) as THREE_NS.BufferGeometry & { parameters: unknown }\n geometry.parameters = { shapes, options: extrudeOptions ?? {} }\n return geometry\n },\n } satisfies Record<(typeof SHAPE_GEOMETRY_TYPES)[number], (...args: never[]) => THREE_NS.BufferGeometry>\n const namespace: Record<string, unknown> = { ...(THREE as unknown as Record<string, unknown>) }\n for (const type of SHAPE_GEOMETRY_TYPES) {\n // three hands the constructor's own arguments through; each replacement takes the same ones\n const make = constructors[type] as (...args: unknown[]) => THREE_NS.BufferGeometry\n // callable with `new` (the returned object wins), and `instanceof` checks three's prototype\n const Constructor = function (this: unknown, ...args: unknown[]) {\n return make(...args)\n } as unknown as { prototype: unknown }\n Constructor.prototype = (THREE as unknown as Record<string, { prototype: unknown }>)[type]!.prototype\n Object.defineProperty(Constructor, 'name', { value: type })\n namespace[type] = Constructor\n }\n const result = namespace as unknown as Three\n byKey.set(key, result)\n shims.add(result)\n return result\n}\n","import { colorHex } from '@adatechnology/game-shapes'\nimport type { MaterialHint, ShapeBuffers, ShapeLevel, ShapePart, ShapePoint } from '@adatechnology/game-shapes'\nimport type * as THREE_NS from 'three'\n\nimport { toGeometry } from './geometry'\nimport type { Three } from './geometry'\n\n/** A `MeshStandardMaterial` per material id from the schema's hints (vertex colours on unless the hint says otherwise). */\nexport function createMaterials(THREE: Three, hints: Readonly<Record<string, MaterialHint>> = {}, ids: Iterable<string> = []): Record<string, THREE_NS.Material> {\n const out: Record<string, THREE_NS.Material> = {}\n const all = new Set([...Object.keys(hints), ...ids])\n for (const id of all) {\n const hint = hints[id] ?? {}\n const parameters: THREE_NS.MeshStandardMaterialParameters = { vertexColors: hint.vertexColors ?? true }\n if (hint.color !== undefined) parameters.color = colorHex(hint.color)\n if (hint.roughness !== undefined) parameters.roughness = hint.roughness\n if (hint.metalness !== undefined) parameters.metalness = hint.metalness\n if (hint.opacity !== undefined) parameters.opacity = hint.opacity\n if (hint.transparent !== undefined) parameters.transparent = hint.transparent\n if (hint.emissive !== undefined) parameters.emissive = colorHex(hint.emissive)\n if (hint.emissiveIntensity !== undefined) parameters.emissiveIntensity = hint.emissiveIntensity\n if (hint.flatShading !== undefined) parameters.flatShading = hint.flatShading\n if (hint.depthWrite !== undefined) parameters.depthWrite = hint.depthWrite\n if (hint.side !== undefined) parameters.side = hint.side === 'double' ? THREE.DoubleSide : hint.side === 'back' ? THREE.BackSide : THREE.FrontSide\n const material = new THREE.MeshStandardMaterial(parameters)\n material.name = id\n out[id] = material\n }\n return out\n}\n\nexport interface ToObjectOptions {\n /** Materials by id; missing ids get one from the schema hints (`createMaterials`). */\n readonly materials?: Readonly<Record<string, THREE_NS.Material>>\n /** The material of one draw (wins over `materials`): per material id and part (`null` for the static body). */\n readonly materialFor?: (material: string, part: string | null) => THREE_NS.Material | undefined\n /** Cast and receive shadows (default true). */\n readonly shadows?: boolean\n /** Copy the arrays into the attributes (default true; false shares them, read-only). */\n readonly copy?: boolean\n /** Name of the root. */\n readonly name?: string\n}\n\nexport interface ShapeObject {\n /** A `Group`, or a `LOD` when the shape has levels or a cull distance. */\n readonly object: THREE_NS.Object3D\n /** The level 0 group (the root itself without LOD). */\n readonly near: THREE_NS.Group\n /** Part groups of level 0 by id, at their pivots; animate these. */\n readonly parts: Record<string, THREE_NS.Group>\n readonly meshes: THREE_NS.Mesh[]\n readonly instanced: THREE_NS.InstancedMesh[]\n readonly points: readonly ShapePoint[]\n readonly materials: Record<string, THREE_NS.Material>\n}\n\nfunction placePart(THREE: Three, group: THREE_NS.Group, part: ShapePart): void {\n if (part.matrix && part.position === undefined && part.rotation === undefined && part.scale === undefined && part.heading === undefined) {\n new THREE.Matrix4().fromArray(part.matrix as number[]).decompose(group.position, group.quaternion, group.scale)\n return\n }\n if (part.position) group.position.set(part.position[0], part.position[1], part.position[2])\n if (part.heading !== undefined) group.rotation.y = part.heading\n else if (part.rotation) group.rotation.set(part.rotation[0], part.rotation[1], part.rotation[2], part.rotationOrder ?? 'XYZ')\n if (part.scale !== undefined) {\n if (typeof part.scale === 'number') group.scale.setScalar(part.scale)\n else group.scale.set(part.scale[0], part.scale[1], part.scale[2])\n }\n group.updateMatrix()\n}\n\n/**\n * The three objects of a shape: one mesh per batch (`static:<material>` or `part:<id>`, `userData.material`, `userData.part`), part groups at their\n * pivots, an `InstancedMesh` per instanced batch, and a `LOD` (levels at their distances, nothing from `cullDistance`) when there are levels.\n */\nexport function toObject3D(THREE: Three, buffers: ShapeBuffers, options: ToObjectOptions = {}): ShapeObject {\n const shadows = options.shadows !== false\n const ids = new Set<string>()\n for (const level of buffers.levels) {\n for (const b of level.batches) ids.add(b.material)\n for (const b of level.instanced) ids.add(b.material)\n }\n const made = createMaterials(THREE, buffers.materials, [...ids].filter((id) => !options.materials?.[id]))\n const materials: Record<string, THREE_NS.Material> = { ...made, ...(options.materials ?? {}) }\n const materialOf = (id: string, part: string | null): THREE_NS.Material => options.materialFor?.(id, part) ?? (materials[id] as THREE_NS.Material)\n const meshes: THREE_NS.Mesh[] = []\n const instanced: THREE_NS.InstancedMesh[] = []\n\n const build = (level: ShapeLevel, withParts: boolean): { group: THREE_NS.Group; parts: Record<string, THREE_NS.Group> } => {\n const group = new THREE.Group()\n group.name = options.name ?? buffers.name ?? 'shape'\n const parts: Record<string, THREE_NS.Group> = {}\n if (withParts) {\n for (const part of buffers.parts) {\n const g = new THREE.Group()\n g.name = `part:${part.id}`\n g.userData.part = part.id\n placePart(THREE, g, part)\n parts[part.id] = g\n }\n }\n const into = (part: string | null): THREE_NS.Object3D => (part !== null && parts[part]) || group\n for (const batch of level.batches) {\n const mesh = new THREE.Mesh(toGeometry(THREE, batch, { copy: options.copy !== false }), materialOf(batch.material, batch.part))\n mesh.geometry.computeBoundingSphere()\n mesh.geometry.computeBoundingBox()\n mesh.name = batch.part === null ? `static:${batch.material}` : `part:${batch.part}`\n if (batch.part !== null) mesh.userData.part = batch.part\n mesh.userData.material = batch.material\n if (batch.ranges.length) mesh.userData.ranges = batch.ranges\n mesh.castShadow = shadows\n mesh.receiveShadow = shadows\n into(batch.part).add(mesh)\n meshes.push(mesh)\n }\n for (const batch of level.instanced) {\n const mesh = new THREE.InstancedMesh(toGeometry(THREE, batch.geometry, { copy: options.copy !== false }), materialOf(batch.material, batch.part), batch.count)\n mesh.instanceMatrix.array.set(batch.matrices)\n mesh.instanceMatrix.needsUpdate = true\n if (batch.colors) mesh.instanceColor = new THREE.InstancedBufferAttribute(batch.colors.slice(), 3)\n mesh.name = `instanced:${batch.name ?? batch.material}`\n mesh.userData.material = batch.material\n if (batch.part !== null) mesh.userData.part = batch.part\n mesh.castShadow = shadows\n mesh.receiveShadow = shadows\n mesh.computeBoundingSphere()\n into(batch.part).add(mesh)\n instanced.push(mesh)\n }\n for (const part of Object.values(parts)) group.add(part)\n return { group, parts }\n }\n\n const levels = buffers.levels\n const first = build(levels[0] ?? { distance: 0, detail: 1, batches: [], instanced: [] }, true)\n let object: THREE_NS.Object3D = first.group\n if (levels.length > 1 || buffers.cullDistance !== undefined) {\n const lod = new THREE.LOD()\n lod.name = first.group.name\n lod.addLevel(first.group, 0)\n for (const level of levels.slice(1)) lod.addLevel(build(level, false).group, level.distance)\n if (buffers.cullDistance !== undefined) lod.addLevel(new THREE.Group(), buffers.cullDistance)\n object = lod\n }\n object.userData.points = buffers.points\n return { object, near: first.group, parts: first.parts, meshes, instanced, points: buffers.points, materials }\n}\n","import { mergeTransformed } from '@adatechnology/game-shapes'\nimport type { TransformedPiece } from '@adatechnology/game-shapes'\nimport type * as THREE_NS from 'three'\n\nimport type { Three } from './geometry'\n\nconst isVisible = (object: THREE_NS.Object3D): boolean => {\n for (let node: THREE_NS.Object3D | null = object; node; node = node.parent) if (!node.visible) return false\n return true\n}\n\n/** A plain, non-interleaved, not normalised Float32 attribute: what the core merge reads directly. */\nconst plain = (attribute: THREE_NS.BufferAttribute | THREE_NS.InterleavedBufferAttribute | undefined): attribute is THREE_NS.BufferAttribute =>\n attribute !== undefined && !(attribute as THREE_NS.InterleavedBufferAttribute).isInterleavedBufferAttribute && attribute.array instanceof Float32Array && !attribute.normalized\n\n/** Arrays of a geometry the core merge can take as they are; for anything else, the arrays three's own `toNonIndexed` makes (then not indexed). */\nfunction piece(geometry: THREE_NS.BufferGeometry, matrix: THREE_NS.Matrix4): TransformedPiece {\n const position = geometry.getAttribute('position')\n const normal = geometry.getAttribute('normal')\n const uv = geometry.getAttribute('uv')\n const ok = plain(position) && (normal === undefined || plain(normal)) && (uv === undefined || plain(uv)) && position.itemSize === 3 && (uv === undefined || uv.itemSize === 2)\n if (ok) {\n return { positions: position.array, ...(normal ? { normals: normal.array } : {}), ...(uv ? { uvs: uv.array } : {}), indices: geometry.index ? geometry.index.array : null, matrix: matrix.elements }\n }\n const flat = geometry.index ? geometry.toNonIndexed() : geometry\n const read = (name: string, size: number): Float32Array | undefined => {\n const attribute = flat.getAttribute(name)\n if (!attribute) return undefined\n const out = new Float32Array(attribute.count * size)\n for (let i = 0; i < attribute.count; i++) for (let c = 0; c < size; c++) out[i * size + c] = c === 0 ? attribute.getX(i) : c === 1 ? attribute.getY(i) : attribute.getZ(i)\n return out\n }\n const positions = read('position', 3) as Float32Array\n const normals = read('normal', 3)\n const uvs = read('uv', 2)\n if (flat !== geometry) flat.dispose()\n return { positions, ...(normals ? { normals } : {}), ...(uvs ? { uvs } : {}), indices: null, matrix: matrix.elements }\n}\n\n/**\n * Every visible mesh under `root` (whose world matrix must be up to date), baked into the space of `space` and joined by material: one non-indexed\n * mesh per material (position, normal, uv), no shadow cast. The bake is game-shapes' `mergeTransformed`, bit-identical to three's\n * `toNonIndexed` + `applyMatrix4` + join, so the result is the one `game-props3d`'s `mergeByMaterial` always gave.\n */\nexport function mergeByMaterial(THREE: Three, root: THREE_NS.Object3D, space: THREE_NS.Object3D): THREE_NS.Mesh[] {\n const toSpace = space.matrixWorld.clone().invert()\n const byMaterial = new Map<THREE_NS.Material, TransformedPiece[]>()\n root.traverse((object) => {\n const mesh = object as THREE_NS.Mesh\n if (!mesh.isMesh || Array.isArray(mesh.material) || !isVisible(mesh)) return\n const list = byMaterial.get(mesh.material) ?? []\n list.push(piece(mesh.geometry, new THREE.Matrix4().multiplyMatrices(toSpace, mesh.matrixWorld)))\n byMaterial.set(mesh.material, list)\n })\n return [...byMaterial].map(([material, pieces]) => {\n const merged = mergeTransformed(pieces)\n const geometry = new THREE.BufferGeometry()\n geometry.setAttribute('position', new THREE.BufferAttribute(merged.positions, 3))\n geometry.setAttribute('normal', new THREE.BufferAttribute(merged.normals, 3))\n geometry.setAttribute('uv', new THREE.BufferAttribute(merged.uvs, 2))\n const mesh = new THREE.Mesh(geometry, material)\n mesh.castShadow = false\n mesh.receiveShadow = true\n return mesh\n })\n}\n","import type * as ThreeNS from 'three'\n\nexport type Vec3 = [number, number, number]\ntype ThreeLib = typeof ThreeNS\n\n/**\n * Points of a hanging cord between `a` and `b` (n points including both ends, n >= 2). The deepest point sits `sag` meters below\n * the straight line between the ends, on a real catenary (y = a cosh(x/a)); when the ends differ in height the curve is\n * measured against the straight line between them.\n */\nexport function catenary(a: Vec3, b: Vec3, sag: number, n = 12): Vec3[] {\n const count = Math.max(2, Math.floor(n))\n const span = Math.hypot(b[0] - a[0], b[2] - a[2]) || Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2])\n const depth = Math.max(0, sag)\n const out: Vec3[] = []\n // Solve depth = c (cosh(span / 2c) - 1) for c by bisection (depth grows as c shrinks).\n let shape: (s: number) => number = () => 0\n if (depth > 1e-6 && span > 1e-6) {\n let lo = 1e-4\n let hi = span * 1000\n for (let i = 0; i < 80; i++) {\n const c = (lo + hi) / 2\n const d = c * (Math.cosh(span / (2 * c)) - 1)\n if (d > depth) lo = c\n else hi = c\n }\n const c = (lo + hi) / 2\n shape = (s) => c * (Math.cosh(((s - 0.5) * span) / c) - Math.cosh(span / (2 * c)))\n }\n for (let i = 0; i < count; i++) {\n const s = i / (count - 1)\n out.push([a[0] + (b[0] - a[0]) * s, a[1] + (b[1] - a[1]) * s + shape(s), a[2] + (b[2] - a[2]) * s])\n }\n return out\n}\n\n/** A thin tube through `points` (Catmull-Rom smoothed) for wires, cords and conduits. */\nexport function createCord(THREE: ThreeLib, points: readonly Vec3[], radius: number, material: ThreeNS.Material, radialSegments = 5): ThreeNS.Mesh {\n if (points.length < 2) throw new Error('createCord needs at least two points')\n const curve = new THREE.CatmullRomCurve3(points.map((p) => new THREE.Vector3(p[0], p[1], p[2])), false, 'centripetal')\n const segments = Math.max(4, (points.length - 1) * 4)\n const mesh = new THREE.Mesh(new THREE.TubeGeometry(curve, segments, radius, radialSegments, false), material)\n mesh.name = 'cord'\n mesh.castShadow = true\n return mesh\n}\n","import type * as THREE_NS from 'three'\n\n/** What `releaseObject3D` freed. */\nexport interface ReleaseReport {\n geometries: number\n materials: number\n textures: number\n}\n\nexport interface ReleaseOptions {\n /**\n * Trees that stay drawn (usually the scene): a geometry, material or texture any of them still uses is kept, so a resource shared by a clone that is\n * still on screen (a catalogue model, a material of the kit's palette) is not freed under it.\n */\n keep?: THREE_NS.Object3D | readonly THREE_NS.Object3D[]\n}\n\ntype Disposable = { dispose(): void; userData?: Record<string, unknown> }\ntype Owner = {\n geometry?: Disposable\n material?: THREE_NS.Material | THREE_NS.Material[]\n skeleton?: { dispose(): void }\n traverse(fn: (o: THREE_NS.Object3D) => void): void\n}\n\nconst isTexture = (v: unknown): v is THREE_NS.Texture => !!v && typeof v === 'object' && (v as { isTexture?: boolean }).isTexture === true\n\nfunction texturesOf(material: THREE_NS.Material, out: Set<THREE_NS.Texture>): void {\n for (const value of Object.values(material)) if (isTexture(value)) out.add(value)\n const uniforms = (material as { uniforms?: Record<string, { value?: unknown }> }).uniforms\n if (uniforms) for (const u of Object.values(uniforms)) if (isTexture(u?.value)) out.add(u.value)\n}\n\nfunction collect(roots: readonly THREE_NS.Object3D[]): { geometries: Set<Disposable>; materials: Set<THREE_NS.Material>; textures: Set<THREE_NS.Texture>; skeletons: Set<{ dispose(): void }> } {\n const geometries = new Set<Disposable>()\n const materials = new Set<THREE_NS.Material>()\n const textures = new Set<THREE_NS.Texture>()\n const skeletons = new Set<{ dispose(): void }>()\n for (const root of roots) {\n root.traverse((object) => {\n const o = object as unknown as Owner\n if (o.geometry && typeof o.geometry.dispose === 'function') geometries.add(o.geometry)\n if (o.skeleton && typeof o.skeleton.dispose === 'function') skeletons.add(o.skeleton)\n const list = Array.isArray(o.material) ? o.material : o.material ? [o.material] : []\n for (const m of list) {\n if (!m || typeof m.dispose !== 'function') continue\n materials.add(m)\n texturesOf(m, textures)\n }\n })\n }\n return { geometries, materials, textures, skeletons }\n}\n\nconst marked = new WeakSet<object>()\n/**\n * Marks geometries, materials or textures as shared on purpose (a palette, a model kit, a catalog): `releaseObject3D` and `discardObject3D` never free\n * them. Same as `userData.shared = true`, without touching the resource (its bytes and userData stay as they were, for digests and goldens).\n */\nexport function markShared<T extends object>(...resources: T[]): T[] {\n for (const resource of resources) marked.add(resource)\n return resources\n}\nconst shared = (r: { userData?: Record<string, unknown> }): boolean => r.userData?.shared === true || marked.has(r)\n\n/**\n * Free the GPU side of trees taken out of the scene: their geometries, materials, textures (maps and shader uniforms) and skeletons. three.js keeps a removed\n * mesh's buffers, programs and textures on the GPU (and the JS objects in its caches) until `dispose()`, so every object a game removes without this stays\n * in memory for good. A resource marked `userData.shared = true`, or still used by a tree in `keep`, is left alone. Disposing a resource that is used again\n * later is safe: three.js uploads it again on the next draw.\n */\nexport function releaseObject3D(roots: THREE_NS.Object3D | readonly THREE_NS.Object3D[], options: ReleaseOptions = {}): ReleaseReport {\n const list = (Array.isArray(roots) ? roots : [roots]) as THREE_NS.Object3D[]\n const report: ReleaseReport = { geometries: 0, materials: 0, textures: 0 }\n if (list.length === 0) return report\n const found = collect(list)\n const keepRoots = options.keep === undefined ? [] : ((Array.isArray(options.keep) ? options.keep : [options.keep]) as THREE_NS.Object3D[])\n // a released tree still hanging under a kept one is not \"kept\": skip it while walking the kept trees\n const releasing = new Set<THREE_NS.Object3D>(list)\n for (const keep of keepRoots) {\n const walk = (o: THREE_NS.Object3D): void => {\n if (releasing.has(o)) return\n const owner = o as unknown as Owner\n if (owner.geometry) found.geometries.delete(owner.geometry)\n if (owner.skeleton) found.skeletons.delete(owner.skeleton)\n const mats = Array.isArray(owner.material) ? owner.material : owner.material ? [owner.material] : []\n for (const m of mats) {\n if (!m) continue\n found.materials.delete(m)\n const t = new Set<THREE_NS.Texture>()\n texturesOf(m, t)\n for (const x of t) found.textures.delete(x)\n }\n for (const c of o.children) walk(c)\n }\n walk(keep)\n }\n for (const g of found.geometries) {\n if (shared(g)) continue\n g.dispose()\n report.geometries++\n }\n for (const m of found.materials) {\n if (shared(m as unknown as Disposable)) continue\n m.dispose()\n report.materials++\n }\n for (const t of found.textures) {\n if (shared(t as unknown as Disposable)) continue\n t.dispose()\n report.textures++\n }\n for (const s of found.skeletons) s.dispose()\n return report\n}\n\n/**\n * Take `objects` out of their trees and free what only they used (`releaseObject3D`, keeping whatever the rest of those trees, usually the scene,\n * still draws). The one call for pieces, fragments, helpers or bodies that leave the game for good. A list costs one walk of each scene, so pass\n * the whole list instead of calling it once per object.\n */\nexport function discardObject3D(objects: THREE_NS.Object3D | readonly THREE_NS.Object3D[], options: ReleaseOptions = {}): ReleaseReport {\n const list = (Array.isArray(objects) ? objects : [objects]) as THREE_NS.Object3D[]\n const keep = options.keep === undefined ? [] : Array.isArray(options.keep) ? [...(options.keep as THREE_NS.Object3D[])] : [options.keep as THREE_NS.Object3D]\n for (const object of list) {\n let top = object\n while (top.parent) top = top.parent\n object.removeFromParent()\n if (top !== object && !keep.includes(top)) keep.push(top)\n }\n return releaseObject3D(list, { keep })\n}\n","import { shapeGeometries } from '@adatechnology/game-shapes-three'\nimport type * as THREE_NS from 'three'\n\nimport type { Three } from './types'\n\n/** A body of revolution whose radius follows `profile` along the length, rear to front, closed by a point at each end. */\nexport const profileGeometry = (THREE: Three, length: number, profile: readonly number[], segments?: number): THREE_NS.LatheGeometry => {\n const points = profile.map((radius, index) => new THREE.Vector2(index / (profile.length - 1), radius))\n const curve = new THREE.SplineCurve(points)\n const outline: THREE_NS.Vector2[] = []\n outline.push(new THREE.Vector2(0.001, -length / 2 - 0.0))\n for (let step = 0; step <= 30; step++) {\n const point = curve.getPoint(step / 30)\n outline.push(new THREE.Vector2(Math.max(0.001, point.y), -length / 2 + point.x * length))\n }\n outline.push(new THREE.Vector2(0.001, length / 2))\n // the lathe comes from game-shapes (the same bytes as three's); a namespace already wrapped is given back as it is\n return new (shapeGeometries(THREE).LatheGeometry)(outline, segments || 22).rotateX(Math.PI / 2)\n}\n","import type { TextureMarker } from '@adatechnology/game-animals'\nimport type * as THREE_NS from 'three'\n\nimport { createStream } from './context'\nimport type { CanvasLike, DocumentLike, Three } from './types'\n\ntype Painter = {\n fillStyle: string\n strokeStyle: string\n lineWidth: number\n fillRect(x: number, y: number, width: number, height: number): void\n beginPath(): void\n arc(x: number, y: number, radius: number, from: number, to: number): void\n stroke(): void\n fill(): void\n}\n\nconst canvasTexture = (THREE: Three, document: DocumentLike, width: number, height: number, draw: (painter: Painter, width: number, height: number) => void): THREE_NS.CanvasTexture => {\n const canvas: CanvasLike = document.createElement('canvas')\n canvas.width = width\n canvas.height = height\n draw(canvas.getContext('2d') as Painter, width, height)\n const texture = new THREE.CanvasTexture(canvas as unknown as HTMLCanvasElement)\n texture.colorSpace = THREE.SRGBColorSpace\n return texture\n}\n\nconst spotsTexture = (THREE: Three, document: DocumentLike, base: string, spot: string): THREE_NS.CanvasTexture =>\n canvasTexture(THREE, document, 256, 256, (painter, width, height) => {\n painter.fillStyle = base\n painter.fillRect(0, 0, width, height)\n const next = createStream(11)\n for (let index = 0; index < 70; index++) {\n const x = next() * width\n const y = next() * height\n const radius = 5 + next() * 7\n painter.strokeStyle = spot\n painter.lineWidth = 3\n painter.beginPath()\n painter.arc(x, y, radius, 0, 7)\n painter.stroke()\n painter.fillStyle = 'rgba(120,70,20,.45)'\n painter.beginPath()\n painter.arc(x, y, radius * 0.6, 0, 7)\n painter.fill()\n }\n for (let index = 0; index < 60; index++) {\n painter.fillStyle = spot\n painter.beginPath()\n painter.arc(next() * width, next() * height, 1.5 + next() * 2.5, 0, 7)\n painter.fill()\n }\n })\n\nconst stripesTexture = (THREE: Three, document: DocumentLike, base: string, stripe: string): THREE_NS.CanvasTexture =>\n canvasTexture(THREE, document, 128, 128, (painter, width) => {\n painter.fillStyle = base\n painter.fillRect(0, 0, width, 128)\n painter.fillStyle = stripe\n for (let index = 0; index < 9; index++) painter.fillRect(0, index * 14 + (index % 2) * 3, width, 4)\n })\n\n/** The coat texture a species spec asks for (cat stripes, jaguar rosettes), painted on a canvas. */\nexport const createCoatTexture = (THREE: Three, document: DocumentLike, marker: TextureMarker): THREE_NS.CanvasTexture =>\n marker.kind === 'stripes' ? stripesTexture(THREE, document, marker.base, marker.stripe) : spotsTexture(THREE, document, marker.base, marker.spot)\n","import type { AnimalVisualState } from '@adatechnology/game-animals'\nimport type * as THREE_NS from 'three'\n\nimport type { AnimalRig, Three } from './types'\n\nconst INFECTED_TINT = 0x7d8a66\nconst POOL_COLOR = 0x3a0806\n\n/**\n * Applies a visual state (page: `setInfected`, with the dead look for every species): infected greys the coat, glows the eyes, shows the wounds and hides\n * the first ear; dead lets the animation lay the body down and shows a blood pool at `home`. The pool goes under `rig.root.parent`; with no parent yet,\n * it is created hidden from the scene and `rig.pool` is for the caller to add.\n */\nexport const applyVisualState = (THREE: Three, rig: AnimalRig, state: AnimalVisualState): void => {\n rig.infected = state === 'infected'\n rig.dead = state === 'dead'\n const tint = new THREE.Color(INFECTED_TINT)\n rig.materials.forEach((material) => {\n if (material === rig.eyeMaterial || material === rig.woundMaterial) return\n material.color.copy(material.userData.base as THREE_NS.Color)\n if (rig.infected) material.color.lerp(tint, 0.45).multiplyScalar(0.72)\n })\n const eye = rig.eyeMaterial\n if (eye) {\n if (rig.infected) {\n eye.color.set(0xdcd8b0)\n eye.emissive = new THREE.Color(0x8a8660)\n eye.emissiveIntensity = 0.8\n } else {\n eye.color.copy(eye.userData.base as THREE_NS.Color)\n eye.emissive = new THREE.Color(0)\n }\n }\n rig.infectionParts.forEach((part) => (part.visible = rig.infected))\n const firstEar = rig.ears[0]\n if (firstEar) firstEar.visible = !rig.infected\n if (!rig.dead) {\n rig.root.rotation.z = 0\n rig.root.position.y = 0\n }\n if (rig.pool) {\n rig.pool.visible = rig.dead\n return\n }\n if (!rig.dead) return\n const pool = new THREE.Mesh(new THREE.CircleGeometry(1, 22).rotateX(-Math.PI / 2), new THREE.MeshStandardMaterial({ color: POOL_COLOR, roughness: 0.25 }))\n pool.position.set(rig.home.x, 0.005, rig.home.z + 0.1)\n pool.receiveShadow = true\n pool.scale.setScalar(rig.size * 0.45)\n rig.root.parent?.add(pool)\n rig.pool = pool\n}\n","import type * as THREE_NS from 'three'\n\nimport type { AnimalRig } from './types'\n\n/** The \"joints\" view (page: `setJoint`): the coat turns see-through and every joint ball shows in `jointMaterial`. */\nexport const setJointView = (rig: AnimalRig, on: boolean, jointMaterial: THREE_NS.Material): void => {\n rig.materials.forEach((material) => {\n if (material === rig.eyeMaterial) return\n material.transparent = on\n material.opacity = on ? 0.28 : 1\n material.depthWrite = !on\n material.needsUpdate = true\n })\n rig.joints.forEach((joint) => {\n if (on) joint.userData.restMaterial ??= joint.material\n joint.material = on ? jointMaterial : (joint.userData.restMaterial as THREE_NS.Material)\n joint.renderOrder = on ? 2 : 0\n })\n}\n\n/** The \"split\" view (page: `setSplit`): sets where each pivot should go, the rest position or the rest plus its offset; `updateSplit` moves it there. */\nexport const setSplit = (rig: AnimalRig, on: boolean): void => {\n rig.pivots.forEach((pivot) => {\n pivot.group.userData.target = on ? pivot.rest.clone().add(pivot.offset) : pivot.rest.clone()\n })\n}\n\n/** Moves each pivot toward its split target (the page does it in the frame loop: a lerp of 5 per second). */\nexport const updateSplit = (rig: AnimalRig, delta: number): void => {\n rig.pivots.forEach((pivot) => {\n const target = pivot.group.userData.target as THREE_NS.Vector3 | undefined\n if (target) pivot.group.position.lerp(target, Math.min(1, delta * 5))\n })\n}\n","import type { Color, PropItem } from './types'\n\n/** Deterministic PRNG (mulberry32 variant) returning floats in [0, 1). */\nexport function rng(seed: number): () => number {\n let s = seed >>> 0\n return () => {\n s = (s + 0x6d2b79f5) >>> 0\n let t = s\n t = Math.imul(t ^ (t >>> 15), t | 1)\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n }\n}\n\n/** Seed of the details draw: `M.seed` (number or string) or a hash of the position. */\nexport const seedOf = (M: PropItem): number =>\n (M.seed != null\n ? typeof M.seed === 'number'\n ? M.seed\n : [...String(M.seed)].reduce((a, c) => (a * 31 + c.charCodeAt(0)) | 0, 7)\n : Math.round((M.x || 0) * 37 + (M.z || 0) * 91 + 1000)) >>> 0\n\n/** `'#rrggbb'` or number to number. */\nexport const hex = (v: Color): number => (typeof v === 'string' ? parseInt(v.replace('#', ''), 16) : v)\n\n/** Scales the brightness of a colour by factor `f` (clamped per channel). */\nexport const shade = (c: Color, f: number): number => {\n const n = hex(c)\n const r = Math.min(255, Math.round(((n >> 16) & 255) * f))\n const g = Math.min(255, Math.round(((n >> 8) & 255) * f))\n const b = Math.min(255, Math.round((n & 255) * f))\n return (r << 16) | (g << 8) | b\n}\n\n/** Linear mix of two colours. */\nexport const mix = (a: Color, b: Color, t: number): number => {\n const x = hex(a)\n const y = hex(b)\n const m = (s: number) => Math.round(((x >> s) & 255) * (1 - t) + ((y >> s) & 255) * t)\n return (m(16) << 16) | (m(8) << 8) | m(0)\n}\n","import { rng } from './colors'\nimport type { AtlasMap, AtlasName, DocumentLike, FloorAtlas, FloorKind, PictureAtlas, Uv, WaterAtlas } from './types'\n\ntype Ctx = CanvasRenderingContext2D\ntype Rand = () => number\n\nconst makeCanvas = (doc: DocumentLike, w: number, h: number): HTMLCanvasElement => {\n const c = doc.createElement('canvas')\n c.width = w\n c.height = h\n return c\n}\n\n// draws f() nine times (offsets of +-n) inside the clipped cell, so the pattern tiles without a seam\nfunction seamless(g: Ctx, x0: number, y0: number, n: number, f: () => void) {\n g.save()\n g.beginPath()\n g.rect(x0, y0, n, n)\n g.clip()\n for (let dy = -n; dy <= n; dy += n)\n for (let dx = -n; dx <= n; dx += n) {\n g.save()\n g.translate(x0 + dx, y0 + dy)\n f()\n g.restore()\n }\n g.restore()\n}\nconst noise = (g: Ctx, x0: number, y0: number, n: number, r: Rand, a: number) => {\n for (let i = 0; i < (n * n) / 6; i++) {\n const v = r() < 0.5 ? 0 : 255\n g.fillStyle = `rgba(${v},${v},${v},${r() * a})`\n g.fillRect(x0 + r() * n, y0 + r() * n, 1 + r() * 2, 1 + r() * 2)\n }\n}\n\n/** Floor kinds that live in the floor atlas: atlas cells and the world size (meters) one cell covers. */\nexport const FLOORS: Record<FloorKind, { cells: number[]; size: number }> = {\n lawn: { cells: [0, 1], size: 2.0 },\n tile_floor: { cells: [2], size: 1.0 },\n concrete: { cells: [3], size: 2.0 },\n deck_floor: { cells: [4], size: 1.2 },\n dirt: { cells: [5], size: 2.0 },\n pool_tile: { cells: [6], size: 1.0 },\n paving: { cells: [7], size: 1.0 },\n}\nconst PICTURE_CELLS = 8 // 0-3 abstract, 4-6 landscapes, 7 mirror\n\nfunction floorAtlas(doc: DocumentLike): FloorAtlas {\n const N = 256, cv = makeCanvas(doc, N * 4, N * 2), g = cv.getContext('2d')!, P = (c: number): [number, number] => [(c % 4) * N, Math.floor(c / 4) * N];\n const cell = (c: number, seed: number, f: (g: Ctx, x0: number, y0: number, r: Rand) => void) => { const [x0, y0] = P(c), r = rng(seed); f(g, x0, y0, r); };\n for (const [c, seed] of [[0, 11], [1, 12]] as Array<[number, number]>) cell(c, seed, (g, x0, y0, r) => { // lawn: base + blades + dry patches\n g.fillStyle = c ? '#648f3a' : '#5b8a37'; g.fillRect(x0, y0, N, N);\n seamless(g, x0, y0, N, () => { for (let i = 0; i < 40; i++) { g.fillStyle = `rgba(${r() < 0.5 ? '160,170,60' : '40,80,30'},${0.12 + r() * 0.12})`; g.beginPath(); g.arc(r() * N, r() * N, 8 + r() * 22, 0, 6.3); g.fill(); }\n for (let i = 0; i < 520; i++) { r(); /* the original drew a blade height here and never used it; the call keeps the random sequence identical */ g.strokeStyle = `hsl(${85 + r() * 30},${40 + r() * 25}%,${22 + r() * 24}%)`; g.lineWidth = 1; const x = r() * N, y = r() * N; g.beginPath(); g.moveTo(x, y); g.lineTo(x + (r() - 0.5) * 6, y - 4 - r() * 9); g.stroke(); } }); });\n cell(2, 21, (g, x0, y0, r) => { // beige 4x4 ceramic tiles with grout\n const q = N / 4; g.fillStyle = '#8c877b'; g.fillRect(x0, y0, N, N);\n for (let j = 0; j < 4; j++) for (let i = 0; i < 4; i++) { const v = 204 + (r() - 0.5) * 16; g.fillStyle = `rgb(${v},${v - 6},${v - 18})`; g.fillRect(x0 + i * q + 2, y0 + j * q + 2, q - 4, q - 4); g.fillStyle = 'rgba(255,255,255,.18)'; g.fillRect(x0 + i * q + 3, y0 + j * q + 3, q - 8, 3); }\n for (let k = 0; k < 14; k++) { const gr = g.createRadialGradient(0, 0, 0, 0, 0, 40); const x = x0 + r() * N, y = y0 + r() * N; g.save(); g.beginPath(); g.rect(x0, y0, N, N); g.clip(); g.translate(x, y); gr.addColorStop(0, 'rgba(90,80,60,.10)'); gr.addColorStop(1, 'rgba(90,80,60,0)'); g.fillStyle = gr; g.fillRect(-40, -40, 80, 80); g.restore(); } });\n cell(3, 31, (g, x0, y0, r) => { // concrete: slab, expansion joint, crack, stain\n g.fillStyle = '#a7a7a0'; g.fillRect(x0, y0, N, N);\n for (let k = 0; k < 18; k++) { const gr = g.createRadialGradient(0, 0, 0, 0, 0, 50); g.save(); g.beginPath(); g.rect(x0, y0, N, N); g.clip(); g.translate(x0 + r() * N, y0 + r() * N); gr.addColorStop(0, `rgba(${r() < 0.5 ? '70,70,60' : '200,200,190'},.12)`); gr.addColorStop(1, 'rgba(0,0,0,0)'); g.fillStyle = gr; g.fillRect(-50, -50, 100, 100); g.restore(); }\n noise(g, x0, y0, N, r, 0.25); g.fillStyle = 'rgba(40,40,36,.55)'; g.fillRect(x0, y0 + N / 2 - 1, N, 2); g.fillRect(x0 + N / 2 - 1, y0, 2, N); g.fillRect(x0, y0, N, 1); g.fillRect(x0, y0, 1, N);\n g.strokeStyle = 'rgba(50,50,46,.5)'; g.lineWidth = 1; let x = x0 + 30 + r() * 60, y = y0 + 8; g.beginPath(); g.moveTo(x, y); for (let s = 0; s < 8; s++) { x += (r() - 0.5) * 16; y += 12 + r() * 8; g.lineTo(x, y); } g.stroke(); });\n cell(4, 41, (g, x0, y0, r) => { // deck: 12 boards in staggered rows\n const th = N / 12; g.fillStyle = '#2a1c10'; g.fillRect(x0, y0, N, N);\n for (let j = 0; j < 12; j++) { const cut = r() * N; for (const [a, b] of [[0, cut], [cut, N]]) { const v = 150 + (r() - 0.5) * 36; g.fillStyle = `rgb(${v},${v * 0.68 | 0},${v * 0.42 | 0})`; g.fillRect(x0 + a + 1, y0 + j * th + 1, b - a - 2, th - 2);\n g.strokeStyle = 'rgba(50,28,12,.3)'; for (let s = 0; s < 3; s++) { const y = y0 + j * th + 3 + r() * (th - 6); g.beginPath(); g.moveTo(x0 + a, y); g.lineTo(x0 + b, y + (r() - 0.5) * 2); g.stroke(); }\n g.fillStyle = 'rgba(30,20,10,.7)'; g.fillRect(x0 + a + 4, y0 + j * th + th / 2, 2, 2); g.fillRect(x0 + b - 6, y0 + j * th + th / 2, 2, 2); } } });\n cell(5, 51, (g, x0, y0, r) => { // packed dirt\n g.fillStyle = '#7b5538'; g.fillRect(x0, y0, N, N);\n seamless(g, x0, y0, N, () => { for (let k = 0; k < 30; k++) { g.fillStyle = `rgba(${r() < 0.5 ? '60,38,24' : '150,112,80'},${0.12 + r() * 0.15})`; g.beginPath(); g.arc(r() * N, r() * N, 6 + r() * 20, 0, 6.3); g.fill(); }\n for (let k = 0; k < 90; k++) { const v = 90 + r() * 90; g.fillStyle = `rgb(${v},${v * 0.85 | 0},${v * 0.7 | 0})`; g.fillRect(r() * N, r() * N, 1 + r() * 3, 1 + r() * 3); } }); });\n cell(6, 61, (g, x0, y0, r) => { // 8x8 pool tiles\n const q = N / 8; g.fillStyle = '#d4e8ee'; g.fillRect(x0, y0, N, N);\n for (let j = 0; j < 8; j++) for (let i = 0; i < 8; i++) { const v = (r() - 0.5) * 24; g.fillStyle = `rgb(${50 + v},${150 + v},${190 + v})`; g.fillRect(x0 + i * q + 1, y0 + j * q + 1, q - 2, q - 2); g.fillStyle = 'rgba(255,255,255,.2)'; g.fillRect(x0 + i * q + 2, y0 + j * q + 2, q - 6, 2); } });\n cell(7, 71, (g, x0, y0, r) => { // reddish interlocking paving (small bricks)\n const bw = N / 4, bh = N / 8; g.fillStyle = '#7a7268'; g.fillRect(x0, y0, N, N);\n for (let j = 0; j < 8; j++) for (let i = -1; i < 4; i++) { const off = (j % 2) * bw / 2, v = 150 + (r() - 0.5) * 30; g.fillStyle = `rgb(${v + 20},${v * 0.55 | 0},${v * 0.42 | 0})`; g.fillRect(x0 + ((i * bw + off) % N + N) % N + 1, y0 + j * bh + 1, bw - 2, bh - 2); }\n noise(g, x0, y0, N, r, 0.2); });\n const cells = {} as Record<FloorKind, Uv[]>; for (const k of Object.keys(FLOORS) as FloorKind[]) cells[k] = FLOORS[k].cells.map(c => ({ u0: (c % 4) / 4, u1: (c % 4 + 1) / 4, v0: 1 - (Math.floor(c / 4) + 1) / 2, v1: 1 - Math.floor(c / 4) / 2 }));\n const e = 1.5 / (N * 4), ev = 1.5 / (N * 2); // inset of one and a half pixels: no bleeding from the neighbouring cell\n return { canvas: cv, cells, size: (k: FloorKind) => FLOORS[k].size, uv(k: FloorKind, v = 0) { const L = cells[k]; const c = L[v % L.length]; return { u0: c.u0 + e, u1: c.u1 - e, v0: c.v0 + ev, v1: c.v1 - ev }; }, variants: (k: FloorKind) => FLOORS[k].cells.length };\n}\n\nfunction pictureAtlas(doc: DocumentLike, names: string[]): PictureAtlas {\n const N = 256, cv = makeCanvas(doc, N * 4, N * 2), g = cv.getContext('2d')!;\n const cell = (c: number, seed: number, f: (r: Rand) => void) => { const x0 = (c % 4) * N, y0 = Math.floor(c / 4) * N, r = rng(seed); g.save(); g.beginPath(); g.rect(x0, y0, N, N); g.clip(); g.translate(x0, y0); f(r); g.restore(); };\n cell(0, 101, r => { g.fillStyle = '#e8dcc4'; g.fillRect(0, 0, N, N); g.fillStyle = '#c0613c'; g.beginPath(); g.arc(90 + r() * 30, 110, 60, 0, 6.3); g.fill(); g.fillStyle = '#2f7f84'; g.fillRect(120, 40 + r() * 30, 90, 150); g.fillStyle = '#d9a43a'; g.beginPath(); g.arc(150, 200, 70, Math.PI, 0); g.fill(); g.strokeStyle = '#2a2a2a'; g.lineWidth = 3; g.beginPath(); g.moveTo(20, 230); g.lineTo(236, 30); g.stroke(); });\n cell(1, 102, r => { g.fillStyle = '#1f3a5f'; g.fillRect(0, 0, N, N); for (let i = 0; i < 7; i++) { g.fillStyle = ['#f08a3c', '#f4e4c1', '#d94f4f', '#7fb5a5'][i % 4]; g.globalAlpha = 0.85; g.beginPath(); g.ellipse(r() * N, r() * N, 20 + r() * 50, 12 + r() * 30, r() * 3, 0, 6.3); g.fill(); } g.globalAlpha = 1; g.strokeStyle = '#f4e4c1'; g.lineWidth = 4; for (let i = 0; i < 3; i++) { g.beginPath(); g.moveTo(0, 50 + i * 70); g.bezierCurveTo(80, 10 + i * 70, 170, 120 + i * 40, N, 60 + i * 60); g.stroke(); } });\n cell(2, 103, r => { const cs = ['#e9e1cf', '#c8553d', '#2e5e7e', '#e4b23e', '#4b4b4b']; let x = 0; while (x < N) { const w = 30 + r() * 60; g.fillStyle = cs[(r() * cs.length) | 0]; g.fillRect(x, 0, w, N); x += w; } g.fillStyle = '#e9e1cf'; g.fillRect(40, 60, 176, 136); g.fillStyle = cs[1]; g.fillRect(70, 90, 60, 76); g.fillStyle = cs[2]; g.fillRect(140, 90, 50, 36); });\n cell(3, 104, r => { g.fillStyle = '#f2f0ea'; g.fillRect(0, 0, N, N); g.fillStyle = '#1a1a1a'; g.fillRect(40, 190, 176, 8); g.fillStyle = '#b8352e'; g.beginPath(); g.arc(128, 100, 52, 0, 6.3); g.fill(); g.strokeStyle = '#1a1a1a'; g.lineWidth = 2; for (let i = 0; i < 5; i++) { g.beginPath(); g.moveTo(40, 30 + i * 8 + r() * 4); g.lineTo(216, 30 + i * 8); g.stroke(); } });\n cell(4, 105, r => { const gr = g.createLinearGradient(0, 0, 0, N * 0.7); gr.addColorStop(0, '#5a4a8a'); gr.addColorStop(0.5, '#e8805a'); gr.addColorStop(1, '#f7d27a'); g.fillStyle = gr; g.fillRect(0, 0, N, N); g.fillStyle = '#fff3c0'; g.beginPath(); g.arc(150, 150, 36, 0, 6.3); g.fill();\n ([['#7a5a6a', 150], ['#4a4a5e', 175], ['#2a3a3a', 205]] as Array<[string, number]>).forEach(([c, y], k) => { g.fillStyle = c; g.beginPath(); g.moveTo(0, N); g.lineTo(0, y); for (let x = 0; x <= N; x += 32) g.lineTo(x, y - 10 + r() * 24 - k * 3); g.lineTo(N, N); g.fill(); }); });\n cell(5, 106, r => { const gr = g.createLinearGradient(0, 0, 0, N); gr.addColorStop(0, '#9fc8e8'); gr.addColorStop(0.55, '#e8f0f4'); gr.addColorStop(0.56, '#3d7a9a'); gr.addColorStop(1, '#22506a'); g.fillStyle = gr; g.fillRect(0, 0, N, N);\n g.fillStyle = '#5a6a78'; g.beginPath(); g.moveTo(0, 140); for (let x = 0; x <= N; x += 28) g.lineTo(x, 70 + r() * 50); g.lineTo(N, 140); g.fill(); g.fillStyle = '#3a5a4a'; g.beginPath(); g.moveTo(150, 140); g.quadraticCurveTo(200, 60, 230, 140); g.fill(); });\n cell(6, 107, () => { const gr = g.createLinearGradient(0, 0, 0, N); gr.addColorStop(0, '#f6e0a8'); gr.addColorStop(0.5, '#fbeed0'); gr.addColorStop(0.5, '#4bb0c4'); gr.addColorStop(0.72, '#2e8aa8'); gr.addColorStop(0.72, '#ecd8a2'); gr.addColorStop(1, '#d9bd7e'); g.fillStyle = gr; g.fillRect(0, 0, N, N);\n g.strokeStyle = '#4a3a22'; g.lineWidth = 5; g.beginPath(); g.moveTo(60, 255); g.quadraticCurveTo(70, 160, 100, 70); g.stroke(); g.fillStyle = '#2f6a3a'; for (let i = 0; i < 6; i++) { g.beginPath(); g.moveTo(100, 70); g.quadraticCurveTo(100 + Math.cos(i) * 40, 40 + Math.sin(i * 2) * 20, 100 + Math.cos(i * 1.1) * 60, 80 + i * 6); g.quadraticCurveTo(100 + Math.cos(i) * 30, 70, 100, 70); g.fill(); } });\n cell(7, 108, () => { const gr = g.createLinearGradient(0, 0, N, N); gr.addColorStop(0, '#e4e9eb'); gr.addColorStop(0.5, '#c4ccd0'); gr.addColorStop(1, '#a8b2b8'); g.fillStyle = gr; g.fillRect(0, 0, N, N);\n g.fillStyle = 'rgba(255,255,255,.35)'; g.beginPath(); g.moveTo(30, 0); g.lineTo(90, 0); g.lineTo(0, 120); g.lineTo(0, 60); g.fill(); g.beginPath(); g.moveTo(150, 0); g.lineTo(175, 0); g.lineTo(0, 256); g.lineTo(0, 232); g.fill();\n const v = g.createRadialGradient(128, 128, 90, 128, 128, 190); v.addColorStop(0, 'rgba(0,0,0,0)'); v.addColorStop(1, 'rgba(0,0,0,.18)'); g.fillStyle = v; g.fillRect(0, 0, N, N); });\n const uvc = (c: number): Uv => ({ u0: (c % 4) / 4 + 1.5 / (N * 4), u1: (c % 4 + 1) / 4 - 1.5 / (N * 4), v0: 1 - (Math.floor(c / 4) + 1) / 2 + 1.5 / (N * 2), v1: 1 - Math.floor(c / 4) / 2 - 1.5 / (N * 2) });\n return { canvas: cv, n: PICTURE_CELLS - 1, uv: c => uvc(c), mirror: () => uvc(7), names };\n}\n\n// light/dark ripple that tiles; the host scrolls texture.offset for the shimmer\nfunction waterAtlas(doc: DocumentLike): WaterAtlas {\n const N = 128, cv = makeCanvas(doc, N, N), g = cv.getContext('2d')!, r = rng(901);\n g.fillStyle = '#e6f2f8'; g.fillRect(0, 0, N, N);\n seamless(g, 0, 0, N, () => { for (let i = 0; i < 26; i++) { const x = r() * N, y = r() * N, w = 14 + r() * 26; g.strokeStyle = r() < 0.55 ? 'rgba(255,255,255,.9)' : 'rgba(110,170,200,.5)'; g.lineWidth = 1 + r() * 1.5; g.beginPath(); g.moveTo(x, y); g.quadraticCurveTo(x + w / 2, y - 5 - r() * 4, x + w, y); g.stroke(); } });\n return { canvas: cv };\n}\n\n/** Lazily builds and caches the procedural canvas atlases (one canvas per atlas; the host makes one texture per canvas). */\nexport function createAtlasProvider(doc: DocumentLike | null, pictureNames: string[]) {\n const cache: { [K in AtlasName]?: AtlasMap[AtlasName] } = {}\n return function atlas<K extends AtlasName>(name: K): AtlasMap[K] {\n if (!doc) throw new Error('game-props3d: a document is needed to build the atlases (pass options.document)')\n const made = cache[name]\n if (made) return made as AtlasMap[K]\n const build = { floor: () => floorAtlas(doc), picture: () => pictureAtlas(doc, pictureNames), water: () => waterAtlas(doc) }[name]\n return (cache[name] = build()) as AtlasMap[K]\n }\n}\n","import type { PropGroup, TypeDef } from './types'\n\nconst T = <I extends string>(id: I, interior: boolean, w: number, d: number, h: number, y: number, solid: boolean, group: PropGroup): TypeDef<I> => ({ id, interior, w, d, h, y, solid, group })\n\n/** id, interior, w, d, h, y (mounting height), solid (false = no collision), group. Names live in the locale table. */\nexport const TYPE_DEFS = [\n T('ceiling_light', true, 0.4, 0.4, 0.07, 0, false, 'ceiling'),\n T('pendant', true, 0.34, 0.34, 1.0, 0, false, 'ceiling'),\n T('track_spot', true, 1.2, 0.1, 0.2, 0, false, 'ceiling'),\n T('chandelier', true, 0.7, 0.7, 0.7, 0, false, 'ceiling'),\n T('wall_ac', true, 0.9, 0.2, 0.28, 2.15, false, 'wall'),\n T('picture_small', true, 0.35, 0.03, 0.45, 1.3, false, 'wall'),\n T('picture_large', true, 1.0, 0.04, 0.7, 1.2, false, 'wall'),\n T('mirror', true, 0.6, 0.03, 1.2, 0.9, false, 'wall'),\n T('curtain', true, 1.6, 0.12, 2.3, 0.1, false, 'wall'),\n T('blinds', true, 1.2, 0.08, 1.3, 1.0, false, 'wall'),\n T('flat_tv', true, 1.2, 0.06, 0.7, 1.0, false, 'wall'),\n T('floor_plant', true, 0.5, 0.5, 1.1, 0, true, 'plants'),\n T('table_plant', true, 0.2, 0.2, 0.3, 0, false, 'plants'),\n T('hanging_plant', true, 0.5, 0.5, 1.0, 0, false, 'plants'),\n T('tv_stand', true, 1.6, 0.42, 1.05, 0, true, 'furniture'),\n T('breakfast_bar', true, 2.0, 1.6, 0.95, 0, true, 'furniture'),\n T('sofa_l', true, 2.8, 1.7, 0.85, 0, true, 'furniture'),\n T('double_bed_modern', true, 1.6, 2.1, 1.2, 0, true, 'furniture'),\n T('home_office', true, 1.4, 0.75, 1.2, 0, true, 'furniture'),\n T('closet', true, 2.2, 0.6, 2.3, 0, true, 'furniture'),\n T('vanity', true, 0.6, 0.45, 1.5, 0, true, 'furniture'),\n T('ac_condenser', false, 0.8, 0.3, 0.55, 0.5, true, 'yard'),\n T('lawn', false, 4, 4, 0.02, 0, false, 'floor'),\n T('tile_floor', false, 4, 3, 0.03, 0, false, 'floor'),\n T('concrete', false, 4, 4, 0.03, 0, false, 'floor'),\n T('deck_floor', false, 4, 3, 0.04, 0, false, 'floor'),\n T('dirt', false, 4, 4, 0.02, 0, false, 'floor'),\n T('gourmet_counter', false, 4.2, 3.0, 2.9, 0, true, 'yard'),\n T('sauna', false, 2.0, 1.8, 2.1, 0, true, 'yard'),\n T('soaking_tub', false, 1.3, 1.3, 0.8, 0, true, 'yard'),\n T('pool', false, 4.5, 2.8, 0.1, 0, true, 'yard'),\n T('deck', false, 3.5, 3.0, 0.22, 0, true, 'yard'),\n T('sun_lounger', false, 0.62, 1.9, 0.5, 0, true, 'yard'),\n T('garden_bed', false, 2.0, 1.0, 0.9, 0, true, 'yard'),\n T('fruit_tree', false, 2.4, 2.4, 3.2, 0, true, 'yard'),\n T('palm_tree', false, 2.0, 2.0, 4.5, 0, true, 'yard'),\n T('pergola', false, 3.5, 3.0, 2.4, 0, true, 'yard'),\n T('slide', false, 0.8, 2.6, 1.5, 0, true, 'yard'),\n T('swing', false, 2.2, 1.7, 2.2, 0, true, 'yard'),\n T('bbq_simple', false, 0.9, 0.6, 1.0, 0, true, 'yard'),\n T('garage_car', false, 1.8, 4.2, 1.45, 0, true, 'yard'),\n T('window_boards', true, 1.2, 0.06, 1.15, 1.0, false, 'house_state'),\n T('barricade', true, 1.5, 0.7, 1.8, 0, true, 'house_state'),\n T('sandbags', false, 1.6, 0.5, 0.6, 0, true, 'house_state'),\n T('barbed_wire', false, 4.0, 0.2, 0.6, 0, false, 'house_state'),\n T('razor_wire', false, 4.0, 0.6, 0.6, 0, false, 'house_state'),\n T('water_bottle_crates', true, 0.9, 0.6, 0.65, 0, true, 'house_state'),\n T('canned_food', true, 0.4, 0.3, 0.3, 0, false, 'house_state'),\n T('water_jug', true, 0.3, 0.3, 0.5, 0, true, 'house_state'),\n T('floor_mattress', true, 0.9, 1.9, 0.3, 0, false, 'house_state'),\n T('lantern', true, 0.16, 0.16, 0.3, 0, false, 'house_state'),\n T('candles', true, 0.25, 0.25, 0.2, 0, false, 'house_state'),\n T('open_suitcases', true, 1.5, 0.7, 0.6, 0, false, 'house_state'),\n T('clothes_on_floor', true, 1.2, 1.0, 0.05, 0, false, 'house_state'),\n T('fallen_chair', true, 0.5, 0.5, 0.5, 0, false, 'house_state'),\n T('blood_stains', true, 1.2, 1.2, 0.01, 0, false, 'house_state'),\n T('open_door', true, 0.9, 0.15, 2.15, 0, false, 'house_state'),\n]\n\nexport type PropType = (typeof TYPE_DEFS)[number]['id']\n\nexport const DEFS = Object.fromEntries(TYPE_DEFS.map((t) => [t.id, t])) as Record<PropType, (typeof TYPE_DEFS)[number]>\n\nexport const MATERIALS = ['plain', 'metal', 'wood', 'glass', 'fabric', 'plant', 'water', 'emissive', 'atlas_picture', 'atlas_floor'] as const\n","import type { BuilderContext, BuilderMap } from '../types'\n\nexport function ceilingBuilders({ kit }: BuilderContext): Partial<BuilderMap> {\n const B: Partial<BuilderMap> = {}\n B.ceiling_light = (M, a) => { const K = kit(M, a), t = K.ceiling(), on = !!M.on;\n K.cyl('plain', 0, t - 0.05, 0, 0.2, 0.05, 0xece8e0); K.cyl('plain', 0, t - 0.07, 0, 0.15, 0.02, 0xd8d4c8); K.cyl(on ? 'emissive' : 'plain', 0, t - 0.078, 0, 0.12, 0.012, on ? 0xfff0c8 : 0xf4f0e6);\n if (on) K.light(0, t - 0.15, 0, 0xffe8b8, 6, 'ceiling_light'); };\n B.pendant = (M, a) => { const K = kit(M, a), t = K.ceiling(), { h } = K.dim('pendant'), ys = t - h, color = K.main(0x2a2a2a);\n K.box('plain', 0, ys + 0.1, 0, 0.012, h - 0.1, 0.012, 0x1a1a1a); K.cyl('plain', 0, t - 0.03, 0, 0.05, 0.03, 0xe8e4dc);\n K.cyl('plain', 0, ys, 0, 0.17, 0.14, color, 0, 0, 'sphere'); K.cyl('emissive', 0, ys - 0.035, 0, 0.045, 0.09, 0xffe0a0, 0, 0, 'sphere');\n if (M.light) K.light(0, ys - 0.1, 0, 0xffc880, 6, 'pendant'); };\n B.track_spot = (M, a) => { const K = kit(M, a), t = K.ceiling(), { w } = K.dim('track_spot'), on = !!M.on;\n K.box('metal', 0, t - 0.025, 0, w, 0.025, 0.035, 0x2a2a2a);\n for (let i = -1; i <= 1; i++) { const u = i * w / 3; K.box('metal', u, t - 0.1, 0, 0.02, 0.075, 0.02, 0x2a2a2a); K.cyl('metal', u, t - 0.16, 0.04, 0.035, 0.11, 0x222222, 2.6, 0); K.cyl(on ? 'emissive' : 'plain', u, t - 0.208, 0.069, 0.028, 0.006, on ? 0xfff0c8 : 0xe8e8e8, 2.6, 0); } };\n B.chandelier = (M, a) => { const K = kit(M, a), t = K.ceiling(), on = !!M.on, y = t - 0.5, gold = K.metal(0x8a7a5a);\n K.box('metal', 0, y, 0, 0.012, 0.5, 0.012, gold); K.cyl('metal', 0, y - 0.04, 0, 0.06, 0.08, gold); K.cyl('metal', 0, y - 0.06, 0, 0.14, 0.015, gold);\n for (let i = 0; i < 6; i++) { const an = i * Math.PI / 3, cx = Math.cos(an), cz = Math.sin(an);\n K.box('metal', cx * 0.14, y - 0.06, cz * 0.14, 0.28, 0.012, 0.012, gold, -an); K.cyl('plain', cx * 0.28, y - 0.06, cz * 0.28, 0.02, 0.09, 0xf0e6c8); K.cyl(on ? 'emissive' : 'plain', cx * 0.28, y + 0.03, cz * 0.28, 0.012, 0.03, on ? 0xffd890 : 0xe6d8b0, 0, 0, 'sphere');\n K.box('glass', cx * 0.3, y - 0.2, cz * 0.3, 0.025, 0.14, 0.025, 0xffffff, -an); K.box('glass', cx * 0.14, y - 0.18, cz * 0.14, 0.02, 0.12, 0.02, 0xffffff); }\n K.box('glass', 0, y - 0.3, 0, 0.05, 0.2, 0.05, 0xffffff, 0.7);\n if (M.light) K.light(0, y - 0.1, 0, 0xffd8a0, 6, 'chandelier'); };\n return B\n}\n","import type { BuilderContext, BuilderMap, FloorKind } from '../types'\n\nconst FLOOR_ITEMS: Array<FloorKind & keyof BuilderMap> = ['lawn', 'tile_floor', 'concrete', 'deck_floor', 'dirt']\n\n/** Ground cover items: one builder each, tiling the item's rectangle (or `M.poly`) with the floor atlas. */\nexport function floorBuilders({ cover }: BuilderContext): Partial<BuilderMap> {\n const B: Partial<BuilderMap> = {}\n for (const kind of FLOOR_ITEMS) B[kind] = (M, api) => cover(api, M, kind, { color: M.color })\n return B\n}\n","import { shade } from '../colors'\nimport type { Kit } from '../types'\n\nexport const GREENS = [0x3f7a3a, 0x2f6a30, 0x4f8a42, 0x5a9448]\n/** i-th shade of green (from the `plant` palette role or the defaults), alternating darker. */\nexport const green = (K: Kit, i: number): number => {\n const v = K.color('plant', GREENS[i % GREENS.length])\n return i % 2 ? shade(v, 0.85) : v\n}\nexport const pick = <T>(K: Kit, L: T[]): T => L[Math.floor(K.rnd() * L.length) % L.length]\n\n// flat TV: panel, screen, back box and either a stand (two feet) or a wall bracket; returns the panel height\nexport const drawTv = (K: Kit, x: number, y: number, z: number, w: number, stand: boolean): number => { const h = w * 0.5625, yb = stand ? y + 0.07 : y;\n K.box('plain', x, yb, z, w, h, 0.035, 0x101112); K.box('metal', x, yb + 0.015, z + 0.0175, w - 0.03, h - 0.03, 0.004, 0x0a0c10); K.box('plain', x, yb + h * 0.2, z - 0.035, w * 0.5, h * 0.55, 0.04, 0x1a1a1a);\n if (stand) { for (const s of [-1, 1]) K.box('metal', x + s * w * 0.33, y, z, 0.1, 0.07, 0.22, 0x1a1a1a); } else K.box('metal', x, yb + h * 0.35, z - 0.065, w * 0.3, 0.03, 0.03, 0x2a2a2a); return h; };\n","import type { BuilderContext, BuilderMap } from '../types'\nimport { shade } from '../colors'\nimport { drawTv, pick } from './shared'\n\nexport function furnitureBuilders({ kit, atlas }: BuilderContext): Partial<BuilderMap> {\n const B: Partial<BuilderMap> = {}\n B.breakfast_bar = (M, a) => { const K = kit(M, a), { w, d } = K.dim('breakfast_bar'), zb = -d / 2, cd = 0.65, body = K.color('wall_kitchen', 0xf0eee8), wood = K.wood(0xa07a50), paving = 0xf4f4f0;\n K.box('plain', 0, 0, zb + cd / 2 - 0.03, w - 0.06, 0.08, cd - 0.08, 0x2a2a2a); K.box('plain', 0, 0.08, zb + cd / 2, w, 0.8, cd, body);\n for (let i = 0; i < 4; i++) { K.box('wood', -w / 2 + (i + 0.5) * w / 4, 0.12, zb - 0.004, w / 4 - 0.025, 0.72, 0.012, wood); K.box('metal', -w / 2 + (i + 0.5) * w / 4 + (i % 2 ? -0.12 : 0.12), 0.75, zb - 0.012, 0.012, 0.14, 0.016, 0x9a9a9a); }\n K.box('plain', 0, 0.88, zb + (cd + 0.32) / 2, w + 0.04, 0.04, cd + 0.32, paving);\n for (const s of [-1, 1]) K.box('metal', s * (w / 2 - 0.05), 0, zb + cd + 0.24, 0.05, 0.88, 0.05, 0x2a2a2a);\n K.box('metal', -w * 0.22, 0.915, zb + cd / 2, 0.52, 0.006, 0.38, 0xb0b4b8); K.box('metal', -w * 0.22, 0.92, zb + cd / 2, 0.46, 0.004, 0.32, 0x70767a); K.cyl('metal', -w * 0.22, 0.92, zb + 0.1, 0.014, 0.26, 0xb8bcc0); K.box('metal', -w * 0.22, 1.16, zb + 0.15, 0.02, 0.02, 0.1, 0xb8bcc0);\n K.cyl('plain', w * 0.25, 0.92, zb + cd / 2 + 0.2, 0.13, 0.05, 0xf0f0ea); for (let i = 0; i < 3; i++) K.cyl('plain', w * 0.25 + (i - 1) * 0.07, 0.97, zb + cd / 2 + 0.2 + (i % 2) * 0.03, 0.045, 0.09, [0xe8801c, 0xc83030, 0xd8c040][i], 0, 0, 'sphere');\n const stool = K.accent(0x8a5a38); for (let i = 0; i < 3; i++) { const x = (i - 1) * w * 0.3, z = zb + cd + 0.5;\n K.cyl('plain', x, 0.62, z, 0.17, 0.05, stool); K.cyl('metal', x, 0.04, z, 0.02, 0.58, 0x2a2a2a); K.cyl('metal', x, 0, z, 0.17, 0.02, 0x2a2a2a); K.cyl('metal', x, 0.25, z, 0.14, 0.012, 0x2a2a2a); } };\n B.sofa_l = (M, a) => { const K = kit(M, a), { w, d } = K.dim('sofa_l'), c = K.fabric(pick(K, [0x6e7078, 0x8a7a68, 0x45586a])), cd = shade(c, 0.82), zb = -d / 2, pf = 0.95, ac = K.accent(0xc89a4a), cw = 0.95;\n for (const sx of [-1, 1]) for (const sz of [-1, 1]) K.box('wood', sx * (w / 2 - 0.1), 0, sz * (d / 2 - 0.1), 0.05, 0.08, 0.05, K.wood(0x2a1d14));\n K.box('fabric', -(cw / 2), 0.08, zb + pf / 2, w - cw, 0.26, pf, cd); K.box('fabric', w / 2 - cw / 2, 0.08, 0, cw, 0.26, d, cd);\n K.box('fabric', 0, 0.08, zb + 0.11, w, 0.72, 0.22, cd); K.box('fabric', -w / 2 + 0.09, 0.08, zb + pf / 2, 0.18, 0.55, pf, cd);\n const nm = 2, mw = (w - cw - 0.18) / nm; for (let i = 0; i < nm; i++) K.box('fabric', -w / 2 + 0.18 + (i + 0.5) * mw, 0.34, zb + 0.12 + 0.39, mw - 0.02, 0.16, 0.78, c);\n K.box('fabric', w / 2 - cw / 2, 0.34, 0.035, cw - 0.04, 0.16, d - 0.2, c);\n for (let i = 0; i < 4; i++) K.box('fabric', -w / 2 + 0.18 + (i + 0.5) * (w - 0.2) / 4, 0.42, zb + 0.28, (w - 0.2) / 4 - 0.03, 0.4, 0.14, c, 0, 0.15);\n K.box('fabric', -w * 0.3, 0.5, zb + 0.5, 0.4, 0.38, 0.12, ac, 0.3, 0.3); K.box('fabric', w * 0.32, 0.5, zb + 0.5, 0.38, 0.36, 0.12, shade(ac, 0.8), -0.3, 0.3); };\n B.double_bed_modern = (M, a) => { const K = kit(M, a), { w, d } = K.dim('double_bed_modern'), wood = K.wood(0x6a4c34), edr = K.fabric(0x8a8d92), accent = K.accent(0xc89a4a);\n K.box('wood', 0, 0.05, 0.02, w + 0.06, 0.2, d + 0.02, shade(wood, 0.85)); K.box('fabric', 0, 0.25, 0.05, w - 0.04, 0.24, d - 0.1, 0xf2f0ea);\n K.box('fabric', 0, 0.47, 0.35, w, 0.06, d * 0.64, edr); K.box('fabric', 0, 0.5, -0.05, w * 0.98, 0.05, 0.3, shade(edr, 1.12)); K.box('fabric', 0, 0.52, d / 2 - 0.4, w * 0.9, 0.03, 0.3, accent);\n for (const s of [-1, 1]) K.box('fabric', s * w * 0.24, 0.49, -d / 2 + 0.3, w * 0.4, 0.13, 0.36, 0xf4f2ec, s * 0.06);\n K.box('fabric', 0, 0.55, -d / 2 + 0.5, 0.4, 0.16, 0.12, accent, 0, 0.5);\n const hw = w + 0.5, n = Math.round(hw / 0.11); K.box('wood', 0, 0.2, -d / 2 - 0.05, hw, 1.0, 0.04, shade(wood, 0.7)); for (let i = 0; i < n; i++) K.box('wood', -hw / 2 + (i + 0.5) * hw / n, 0.25, -d / 2 - 0.02, hw / n - 0.025, 0.95, 0.04, i % 2 ? wood : shade(wood, 1.12));\n for (const s of [-1, 1]) { K.box('wood', s * (w / 2 + 0.3), 0.35, -d / 2 + 0.22, 0.4, 0.16, 0.36, shade(wood, 1.05)); K.cyl('plain', s * (w / 2 + 0.3), 0.51, -d / 2 + 0.2, 0.06, 0.03, 0xe8e4dc); K.cyl('plain', s * (w / 2 + 0.3), 0.54, -d / 2 + 0.2, 0.012, 0.18, 0xe8e4dc); K.cyl('plain', s * (w / 2 + 0.3), 0.72, -d / 2 + 0.2, 0.09, 0.12, 0xf0e6c8); } };\n B.home_office = (M, a) => { const K = kit(M, a), { w, d } = K.dim('home_office'), wood = K.wood(0xd2b48c), zb = -d / 2;\n K.box('wood', 0, 0.72, 0, w, 0.035, d, wood); for (const s of [-1, 1]) K.box('metal', s * (w / 2 - 0.04), 0, 0, 0.035, 0.72, d - 0.1, 0x222226); K.box('metal', 0, 0.3, zb + 0.05, w - 0.1, 0.03, 0.02, 0x222226);\n K.box('plain', w / 2 - 0.25, 0.28, 0, 0.4, 0.44, d - 0.08, 0xe8e4dc); for (let i = 0; i < 2; i++) { K.box('plain', w / 2 - 0.25, 0.3 + i * 0.2, d / 2 - 0.04, 0.37, 0.18, 0.01, 0xf2eee6); K.box('metal', w / 2 - 0.25, 0.39 + i * 0.2, d / 2 - 0.03, 0.12, 0.012, 0.012, 0x9a9a9a); }\n K.box('metal', 0, 0.755, zb + 0.2, 0.22, 0.012, 0.16, 0x1a1a1a); K.box('metal', 0, 0.76, zb + 0.14, 0.04, 0.2, 0.025, 0x1a1a1a); K.box('plain', 0, 0.88, zb + 0.15, 0.62, 0.36, 0.035, 0x101112); K.box('metal', 0, 0.895, zb + 0.1685, 0.6, 0.33, 0.004, 0x0a0c10);\n K.box('plain', 0, 0.755, zb + 0.43, 0.42, 0.014, 0.14, 0x1a1a1c); K.box('plain', 0.34, 0.755, zb + 0.43, 0.06, 0.025, 0.1, 0x1a1a1c, 0.2);\n K.cyl('metal', -w * 0.4, 0.755, zb + 0.2, 0.07, 0.015, 0x2a2a2a); K.box('metal', -w * 0.4, 0.77, zb + 0.2, 0.012, 0.3, 0.012, 0x2a2a2a, 0, 0.3); K.cyl('plain', -w * 0.4, 1.02, zb + 0.28, 0.06, 0.05, K.main(0xd8d4c8), 0.7, 0);\n K.box('fabric', -0.5, 0.755, 0.05, 0.2, 0.02, 0.28, K.accent(0x2e5e7e), 0.2); K.cyl('plain', -0.3, 0.755, 0.12, 0.04, 0.09, 0xd8d0c0);\n const zc = d / 2 + 0.3, cc = K.fabric(0x2a2a2e); K.box('plain', 0, 0.44, zc, 0.48, 0.07, 0.46, cc); K.box('plain', 0, 0.52, zc + 0.22, 0.44, 0.52, 0.07, cc, 0, 0.12); K.cyl('metal', 0, 0.06, zc, 0.025, 0.38, 0x2a2a2a);\n for (let i = 0; i < 5; i++) { const an = i * 1.2566, x = Math.cos(an) * 0.17, z = zc + Math.sin(an) * 0.17; K.box('metal', Math.cos(an) * 0.085, 0.06, zc + Math.sin(an) * 0.085, 0.18, 0.025, 0.03, 0x2a2a2a, -an); K.cyl('plain', x, 0, z, 0.03, 0.05, 0x1a1a1a); } };\n B.closet = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('closet'), wood = K.color('wall_bedroom', 0xe6e0d4), mw = K.wood(0xb89468), dv = -w * 0.1;\n K.box('plain', 0, 0, -d / 2 + 0.01, w, h, 0.02, shade(wood, 0.95)); for (const s of [-1, 1]) K.box('plain', s * (w / 2 - 0.015), 0, 0, 0.03, h, d, wood); K.box('plain', 0, h - 0.03, 0, w, 0.03, d, wood); K.box('plain', 0, 0.06, 0, w - 0.06, 0.02, d - 0.02, wood); K.box('plain', 0, 0, 0.0, w - 0.04, 0.06, d - 0.06, 0x2a2a2a); K.box('plain', dv, 0.06, 0, 0.025, h - 0.09, d - 0.02, wood);\n const rod = h * 0.76; K.cyl('metal', (-w / 2 + dv) / 2, rod, 0, 0.012, dv + w / 2 - 0.06, 0xb0b4b8, 0, Math.PI / 2); K.box('plain', (-w / 2 + dv) / 2, rod + 0.05, 0, dv + w / 2 - 0.05, 0.02, d - 0.04, wood);\n const nc = 9, L0 = -w / 2 + 0.06, L1 = dv - 0.05, cs = [0x2a2a30, 0xc8c0a8, 0x4a6a8a, 0xa8483a, 0xe0dcd0, 0x6a8a5a, 0x3a3a3a, 0x8a7a68, 0xd8b8a0];\n for (let i = 0; i < nc; i++) { const gh = 0.6 + K.rnd() * 0.5; K.box('fabric', L0 + (i + 0.5) * (L1 - L0) / nc, rod - gh - 0.02, 0, 0.045, gh, 0.4, i % 3 === 0 ? K.accent(cs[i]) : cs[i]); K.box('metal', L0 + (i + 0.5) * (L1 - L0) / nc, rod - 0.04, 0, 0.004, 0.04, 0.2, 0xb0b4b8); }\n const sx = (dv + w / 2) / 2 + dv / 2 + 0.0, rx0 = dv + 0.015, rw = w / 2 - 0.015 - rx0, rc = rx0 + rw / 2;\n for (let i = 0; i < 5; i++) { const y = 0.5 + i * 0.35; K.box('plain', rc, y, 0, rw, 0.022, d - 0.02, wood); if (i < 4) for (let j = 0; j < 3; j++) { const sh = 0.04 + K.rnd() * 0.12; K.box('fabric', rc - rw * 0.3 + j * rw * 0.3, y + 0.022, 0.0, 0.26, sh, 0.32, cs[(i * 3 + j) % cs.length]); } }\n for (let i = 0; i < 2; i++) { K.box('wood', rc, 0.08 + i * 0.2, d / 2 - 0.01, rw - 0.03, 0.18, 0.02, mw); K.box('metal', rc, 0.2 + i * 0.2, d / 2 + 0.005, 0.2, 0.012, 0.014, 0xb0b4b8); } void sx; };\n B.vanity = (M, a) => { const K = kit(M, a), { w, d } = K.dim('vanity'), zb = -d / 2, br = 0xf4f4f0;\n K.cyl('plain', 0, 0, zb + 0.12, 0.08, 0.7, br); K.cyl('plain', 0, 0, zb + 0.12, 0.1, 0.03, br); K.box('plain', 0, 0.74, 0, w, 0.12, d, br); K.box('plain', 0, 0.865, 0.02, w - 0.1, 0.012, d - 0.1, 0xd4d8da); K.cyl('metal', 0, 0.87, 0.05, 0.02, 0.01, 0x70767a);\n K.cyl('metal', 0, 0.87, zb + 0.05, 0.014, 0.1, 0xb8bcc0); K.box('metal', 0, 0.96, zb + 0.09, 0.02, 0.014, 0.09, 0xb8bcc0); K.box('metal', w * 0.3, 0.87, zb + 0.06, 0.04, 0.06, 0.04, 0x2a7a8a);\n K.box('plain', 0, 1.1, zb + 0.012, 0.52, 0.72, 0.024, K.color('metal', 0xe4e0d8)); K.plane('atlas_picture', 0, 1.46, zb + 0.026, 0.46, 0.66, 0xffffff, 0, 0, 0, atlas('picture').mirror());\n K.box('metal', -w / 2 - 0.02, 0.95, zb + 0.06, 0.03, 0.03, 0.03, 0xb0b4b8); K.box('metal', -w / 2 - 0.02, 0.95, zb + 0.06, 0.28, 0.014, 0.014, 0xb0b4b8); K.box('fabric', -w / 2 - 0.02, 0.62, zb + 0.065, 0.26, 0.34, 0.02, K.accent(0x2e6e7e)); };\n B.tv_stand = (M, a) => { const K = kit(M, a), { w, d } = K.dim('tv_stand'), wood = K.wood(0xc8a070), br = K.color('fabric_main', 0xf0ede6);\n for (const sx of [-1, 1]) for (const sz of [-1, 1]) K.box('metal', sx * (w / 2 - 0.08), 0, sz * (d / 2 - 0.06), 0.035, 0.16, 0.035, 0x1a1a1a);\n K.box('wood', 0, 0.16, 0, w, 0.32, d, wood); K.box('plain', 0, 0.48, 0, w + 0.02, 0.025, d + 0.02, br);\n for (let i = 0; i < 2; i++) K.box('plain', -w / 2 + (i + 0.5) * 0.375 * w, 0.18, d / 2 + 0.004, 0.375 * w - 0.02, 0.28, 0.01, shade(br, 0.97)); // two doors on the left\n K.box('plain', w / 4 + w / 8, 0.19, 0, w / 4 - 0.04, 0.2, d - 0.04, 0x1c1c20); K.box('plain', w / 4 + w / 8, 0.22, 0.05, 0.28, 0.06, 0.2, 0x2a2a30); K.box('fabric', w / 4 + w / 8 + 0.03, 0.29, 0.0, 0.2, 0.05, 0.14, K.accent(0x8a4a3a)); // open niche: console and books\n const th = drawTv(K, 0, 0.5, 0, Math.min(1.3, w * 0.78), true); K.box('plain', 0, 0.5, d / 2 - 0.06, Math.min(1.0, w * 0.55), 0.06, 0.06, 0x16161a); void th; };\n return B\n}\n","import { seedOf, shade } from '../colors'\nimport type { BuilderContext, BuilderMap, Material, PropItem, PropsApi } from '../types'\n\nexport function houseStateBuilders({ kit }: BuilderContext): Partial<BuilderMap> {\n const B: Partial<BuilderMap> = {}\n B.window_boards = (M, a) => { const K = kit(M, a), { w, h, d } = K.dim('window_boards'), y = M.y || 1.0, x = M.variant ? M.variant === 'x' : (seedOf(M) & 1) === 0, cs = [0x7a5634, 0x6b4a2a, 0x8a6a44, 0x5e4228], z = d / 2;\n if (x) { const L = Math.hypot(w, h) + 0.15, an = Math.atan2(h, w); for (const s of [-1, 1]) K.box('wood', 0, y + h / 2 - 0.075, z + (s > 0 ? 0 : 0.03), L, 0.15, 0.03, cs[s > 0 ? 0 : 1], 0, 0, s * an + (K.rnd() - 0.5) * 0.05); K.box('wood', 0, y + h * 0.3, z + 0.06, w + 0.3, 0.15, 0.03, cs[2], 0, 0, (K.rnd() - 0.5) * 0.06); for (const [nx, ny] of [[-w / 2, y], [w / 2, y], [-w / 2, y + h], [w / 2, y + h], [0, y + h / 2]]) K.cyl('metal', nx, ny, z + 0.09, 0.012, 0.02, 0x5a5a5e, Math.PI / 2, 0); }\n else { const n = Math.ceil(h / 0.17); for (let i = 0; i < n; i++) { const yy = y + i * (h - 0.15) / (n - 1 || 1), ww = w + 0.2 + K.rnd() * 0.15; K.box('wood', (K.rnd() - 0.5) * 0.1, yy, z + (i % 2) * 0.03, ww, 0.15, 0.03, cs[i % 4], 0, 0, (K.rnd() - 0.5) * 0.05); for (const s of [-1, 1]) K.cyl('metal', s * (w / 2 + 0.02), yy + 0.075, z + (i % 2) * 0.03 + 0.016, 0.01, 0.015, 0x5a5a5e, Math.PI / 2, 0); } } };\n B.barricade = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('barricade'), wood = K.wood(0x6a4a34), z0 = -d / 2;\n K.box('wood', -w * 0.22, 0, z0 + 0.3, w * 0.5, h * 0.8, 0.45, wood, 0.06, -0.12); K.box('plain', -w * 0.22, h * 0.3, z0 + 0.54, 0.3, 0.012, 0.01, 0x9a9a9a);\n K.box('fabric', w * 0.2, 0, z0 + 0.4, w * 0.5, h * 0.95, 0.18, K.fabric(0xc8c0b0), -0.08, -0.28); K.box('fabric', w * 0.2, 0, z0 + 0.4, w * 0.5 - 0.1, h * 0.9, 0.19, K.accent(0x5a7a9a), -0.08, -0.28);\n K.box('plain', w * 0.38, 0, z0 + 0.35, 0.5, 0.4, 0.4, 0xb89a6a, 0.2); K.box('plain', w * 0.4, 0.4, z0 + 0.33, 0.45, 0.35, 0.38, 0xa8885a, -0.3);\n for (let i = 0; i < 4; i++) { K.box('wood', 0, 0.45 + i * 0.3, z0 + 0.62, w + 0.3, 0.14, 0.04, [0x7a5634, 0x8a6a44, 0x6b4a2a, 0x7a5a38][i], 0, 0, (i % 2 ? 1 : -1) * (0.06 + K.rnd() * 0.08)); for (const s of [-1, 1]) K.cyl('metal', s * w * 0.4, 0.45 + i * 0.3, z0 + 0.65, 0.012, 0.02, 0x5a5a5e, Math.PI / 2, 0); }\n K.box('wood', -w * 0.4, 0.0, z0 + 0.9, 0.4, 0.04, 0.4, wood, 0.3, 0, 0.4); K.box('wood', -w * 0.45, 0.0, z0 + 0.7, 0.04, 0.5, 0.04, wood, 0, 0.9, 0.2); };\n B.sandbags = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('sandbags'), cs = [K.fabric(0xb8a678), 0xa89868, 0xc4b284], bw = 0.46, bh = 0.17, nl = Math.max(1, Math.min(4, Math.round(h / bh)));\n for (let l = 0; l < nl; l++) { const nb = Math.max(1, Math.floor((w - l * 0.18) / bw)); for (let i = 0; i < nb; i++) { const x = -nb * bw / 2 + (i + 0.5) * bw + (l % 2) * 0.12; K.box('fabric', x, l * bh * 0.95, (K.rnd() - 0.5) * 0.04, bw * 0.97, bh, Math.min(d, 0.3), cs[(i + l) % 3], (K.rnd() - 0.5) * 0.08, 0, (K.rnd() - 0.5) * 0.04); K.box('fabric', x + bw * 0.46, l * bh * 0.95 + 0.04, 0, 0.03, 0.09, 0.2, shade(cs[(i + l) % 3], 0.8)); } } };\n B.barbed_wire = (M, a) => { const K = kit(M, a), { w } = K.dim('barbed_wire'), np = Math.max(2, Math.round(w / 2) + 1), wire = 0x8a8c90;\n for (let i = 0; i < np; i++) { const x = -w / 2 + i * w / (np - 1); K.box('wood', x, 0, 0, 0.07, 0.6, 0.07, 0x5a4a3a); }\n for (const y of [0.25, 0.4, 0.55]) { K.box('metal', 0, y, 0.04, w, 0.008, 0.008, wire); const nb = Math.floor(w / 0.16); for (let i = 0; i < nb; i++) { const x = -w / 2 + (i + 0.5) * w / nb; K.box('metal', x, y - 0.012, 0.04, 0.05, 0.006, 0.006, wire, 0, 0, 0.8); K.box('metal', x, y - 0.012, 0.04, 0.05, 0.006, 0.006, wire, 0, 0, -0.8); } } };\n B.razor_wire = (M, a) => { const K = kit(M, a), { w } = K.dim('razor_wire'), r = 0.28, wire = 0x9a9ca0, nr = Math.max(3, Math.round(w / 0.22)), ns = 8, sl = 2 * Math.PI * r / ns * 1.05, y0 = M.y || 0;\n for (let i = 0; i < nr; i++) { const x = -w / 2 + (i + 0.5) * w / nr, lean = (i % 2 ? 0.1 : -0.1); for (let s = 0; s < ns; s++) { const th = (s + 0.5) / ns * Math.PI * 2; K.box('metal', x + Math.sin(th) * lean * 0.3, y0 + r + Math.sin(th) * r - 0.006, Math.cos(th) * r, 0.01, 0.012, sl, wire, 0, -(th + Math.PI / 2), lean); } }\n for (const s of [-1, 1]) K.box('metal', s * w / 2, y0, 0, 0.03, 0.12, 0.03, 0x5a5a5e); };\n B.water_bottle_crates = (M, a) => { const K = kit(M, a), { w, d } = K.dim('water_bottle_crates'), pw = 0.4, pd = 0.28, ph = 0.27, nx = Math.max(1, Math.floor(w / pw)), nz = Math.max(1, Math.floor(d / pd)), nl = 2;\n K.box('wood', 0, 0, 0, nx * pw, 0.05, nz * pd, 0x8a6a44);\n for (let l = 0; l < nl; l++) for (let i = 0; i < nx; i++) for (let j = 0; j < nz; j++) { const x = -nx * pw / 2 + (i + 0.5) * pw, z = -nz * pd / 2 + (j + 0.5) * pd, y = 0.05 + l * ph; K.box('plain', x, y, z, pw - 0.01, ph, pd - 0.01, 0xdceaf4); K.box('plain', x, y + 0.08, z, pw - 0.005, 0.08, pd - 0.005, K.accent(0x2a6aa8)); if (l === nl - 1) for (let u = 0; u < 3; u++) for (let v = 0; v < 2; v++) K.cyl('plain', x - 0.11 + u * 0.11, y + ph, z - 0.06 + v * 0.12, 0.028, 0.02, 0x2a6aa8); } };\n B.canned_food = (M, a) => { const K = kit(M, a), y = M.y || 0, cs = [0xc8a030, 0xb03a2a, 0x4a7a3a, 0xd8d0b0, 0x8a3a2a], rw = [4, 3, 2];\n rw.forEach((n, r) => { for (let i = 0; i < n; i++) { const x = (i - (n - 1) / 2) * 0.072, c = cs[(i + r * 2) % cs.length]; K.cyl('plain', x, y + r * 0.1, 0, 0.033, 0.1, c); K.cyl('plain', x, y + r * 0.1 + 0.02, 0, 0.0345, 0.055, 0xe8e4d8); K.cyl('metal', x, y + r * 0.1 + 0.1, 0, 0.031, 0.004, 0xb8bcc0); } });\n K.cyl('plain', 0.3, y + 0.033, 0.1, 0.033, 0.1, 0xc8a030, 0, Math.PI / 2); K.cyl('plain', -0.32, y + 0.033, 0.12, 0.033, 0.1, 0xb03a2a, 0, 1.2); };\n B.water_jug = (M, a) => { const K = kit(M, a), y = M.y || 0, az = K.accent(0x5aa0d8);\n K.cyl('plain', 0, y, 0, 0.13, 0.32, az); K.cyl('plain', 0, y + 0.32, 0, 0.1, 0.05, az); K.cyl('plain', 0, y + 0.37, 0, 0.05, 0.08, az); K.cyl('plain', 0, y + 0.45, 0, 0.055, 0.035, 0x2a5a9a); K.cyl('plain', 0, y + 0.12, 0, 0.1325, 0.1, 0xf4f4f0); K.box('plain', 0.14, y + 0.22, 0, 0.04, 0.12, 0.05, shade(az, 0.85)); };\n B.floor_mattress = (M, a) => { const K = kit(M, a), { w, d } = K.dim('floor_mattress'), h = 0.17, c = K.fabric(0xc8c0b0), ac = K.accent(0x6a4a3a);\n K.box('fabric', 0, 0, 0, w, h, d, c); for (let i = 0; i < 4; i++) K.box('fabric', -w / 2 + (i + 0.5) * w / 4, h, 0, w / 8, 0.003, d - 0.04, shade(c, 0.86)); K.box('fabric', 0, h, -d / 2 + 0.28, w * 0.7, 0.1, 0.38, 0xf0ece0, 0.05);\n K.box('fabric', 0.03, h, 0.3, w * 0.95, 0.05, d * 0.55, ac, 0.05); K.box('fabric', 0.15, h + 0.05, 0.0, w * 0.5, 0.05, 0.4, shade(ac, 1.2), 0.3); K.box('fabric', -0.2, h + 0.05, 0.5, w * 0.3, 0.06, 0.3, shade(ac, 0.8), -0.4); };\n B.lantern = (M, a) => { const K = kit(M, a), y = M.y || 0;\n K.cyl('metal', 0, y, 0, 0.07, 0.03, 0x2a4a2a); K.cyl('emissive', 0, y + 0.03, 0, 0.055, 0.18, 0xffd890); for (const sx of [-1, 1]) for (const sz of [-1, 1]) K.box('metal', sx * 0.05, y + 0.03, sz * 0.05, 0.01, 0.18, 0.01, 0x2a2a2a);\n K.cyl('metal', 0, y + 0.21, 0, 0.075, 0.025, 0x2a4a2a); K.box('metal', 0, y + 0.235, 0, 0.1, 0.012, 0.012, 0x2a2a2a); for (const s of [-1, 1]) K.box('metal', s * 0.05, y + 0.235, 0, 0.01, 0.045, 0.01, 0x2a2a2a);\n if (M.lit !== false) K.light(0, y + 0.15, 0, 0xffc880, 7, 'lantern'); };\n B.candles = (M, a) => { const K = kit(M, a), y = M.y || 0;\n K.cyl('plain', 0, y, 0, 0.09, 0.012, 0xf0f0ea); [[0, 0, 0.16], [0.06, 0.03, 0.1], [-0.05, 0.04, 0.12], [0.02, -0.06, 0.07]].forEach(([x, z, h], i) => { K.cyl('plain', x, y + 0.012, z, 0.017, h, 0xf0e6c8); K.cyl('emissive', x, y + 0.012 + h, z, 0.008, 0.03, i % 2 ? 0xffc060 : 0xffd890, 0, 0, 'sphere'); });\n K.cyl('plain', 0.02, y + 0.012, 0.0, 0.05, 0.008, 0xf0e6c8); if (M.lit !== false) K.light(0, y + 0.22, 0, 0xffb060, 5, 'candles'); };\n B.open_suitcases = (M, a) => { const K = kit(M, a), cs = [K.main(0xb03a34), 0x2a3a5a], rs = [0xc8c0a8, 0x4a6a8a, 0xe0dcd0, 0xa8483a];\n const suitcase = (x: number, z: number, ww: number, dd: number, c: number, ry: number, al: number) => { const lx = x, hz = z - dd / 2;\n K.box('plain', lx, 0, z, ww, 0.16, dd, c, ry); K.box('fabric', lx, 0.12, z, ww - 0.06, 0.05, dd - 0.06, 0xd8d0c0, ry);\n for (let i = 0; i < 5; i++) K.box('fabric', lx + (K.rnd() - 0.5) * (ww - 0.2), 0.16, z + (K.rnd() - 0.5) * (dd - 0.2), 0.14 + K.rnd() * 0.1, 0.04 + K.rnd() * 0.04, 0.12 + K.rnd() * 0.1, rs[i % 4], K.rnd() * 3);\n K.ramp('plain', lx, 0.16, hz, ww, 0.07, dd, c, al); K.box('metal', lx + ww * 0.3, 0.1, z + dd / 2, 0.04, 0.03, 0.01, 0xc0c0c0); };\n suitcase(-0.4, 0, 0.75, 0.5, cs[0], 0.15, 1.35); suitcase(0.45, 0.1, 0.6, 0.42, cs[1], -0.3, 1.2); };\n B.clothes_on_floor = (M, a) => { const K = kit(M, a), { w, d } = K.dim('clothes_on_floor'), cs = [0xc8c0a8, 0x4a6a8a, 0xa8483a, 0xe0dcd0, 0x6a8a5a, 0x2a2a30, K.accent(0xc89a4a)];\n for (let i = 0; i < 9; i++) { const x = (K.rnd() - 0.5) * (w - 0.3), z = (K.rnd() - 0.5) * (d - 0.3), r = K.rnd() * 6, c = cs[i % cs.length], y = 0.012 + i * 0.003;\n if (i % 3 === 0) { K.box('fabric', x, y, z, 0.3, 0.012, 0.36, c, r); for (const s of [-1, 1]) K.box('fabric', x + Math.cos(r) * s * 0.2, y, z - Math.sin(r) * s * 0.2, 0.2, 0.012, 0.09, shade(c, 0.9), r + s * 0.5); }\n else if (i % 3 === 1) { for (const s of [-1, 1]) K.box('fabric', x + s * 0.06, y, z, 0.13, 0.014, 0.5, 0x3a4a6a, r + s * 0.15); } else K.box('fabric', x, y, z, 0.14, 0.01, 0.07, c, r); }\n const sx = (K.rnd() - 0.5) * (w - 0.4), sz = (K.rnd() - 0.5) * (d - 0.4); K.box('plain', sx, 0.012, sz, 0.09, 0.07, 0.27, 0x2a2a2a, 0.4); K.box('plain', sx + 0.15, 0.012, sz + 0.05, 0.09, 0.07, 0.27, 0x2a2a2a, 0.9); };\n B.fallen_chair = (M, a) => { // wooden chair knocked over on its side: the parts are axis-aligned boxes, rotated 90 degrees around z\n const g = Math.PI / 2, dy = 0.23, api = { box: (Mm: PropItem, mat: Material, x: number, y: number, z: number, w: number, h: number, d: number, color: number, ry: number, rx: number, rz?: number) => { const cy = y + h / 2; a.box(Mm, mat, -cy + 0.0, dy + x - h / 2 + 0.0 - 0.0, z, w, h, d, color, ry, rx, g + (rz || 0)); } };\n const K = kit(M, api as unknown as PropsApi), c = K.main(0x6a4a30); // center (x, y, z) becomes (-cy, x): the api y is the base, so h/2 is subtracted\n K.box('wood', 0, 0.44, 0, 0.42, 0.04, 0.42, c); for (const sx of [-1, 1]) for (const sz of [-1, 1]) K.box('wood', sx * 0.18, 0, sz * 0.18, 0.035, 0.44, 0.035, shade(c, 0.85));\n K.box('wood', 0, 0.48, -0.19, 0.42, 0.45, 0.03, c); };\n B.blood_stains = (M, a) => { const K = kit(M, a), { w, d } = K.dim('blood_stains');\n for (let k = 0; k < 7; k++) K.cyl('plain', (K.rnd() - 0.5) * w * 0.3, 0.012 + k * 0.0004, (K.rnd() - 0.5) * d * 0.3, 0.12 + K.rnd() * 0.18, 0.003, k < 2 ? 0x3a0806 : 0x5a100c);\n for (let k = 0; k < 8; k++) { const an = K.rnd() * 6.28, r = 0.3 + K.rnd() * Math.min(w, d) * 0.4; K.cyl('plain', Math.cos(an) * r, 0.013 + k * 0.0002, Math.sin(an) * r, 0.02 + K.rnd() * 0.05, 0.003, 0x4a0a08); }\n if (M.drag !== false) for (let k = 1; k < 6; k++) K.box('plain', 0.3 + k * 0.1, 0.012, 0.15 + k * 0.17, 0.16 - k * 0.015, 0.003, 0.2, 0x4a0c0a, 0.5); };\n B.open_door = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('open_door'), wood = K.wood(0x6a4a2c), angle = M.angle != null ? M.angle : 1.2, L = w - 0.06, hx = -w / 2 + 0.03;\n K.box('wood', -w / 2 - 0.03, 0, 0, 0.06, h + 0.06, d, shade(wood, 0.8)); K.box('wood', w / 2 + 0.03, 0, 0, 0.06, h + 0.06, d, shade(wood, 0.8)); K.box('wood', 0, h, 0, w + 0.12, 0.06, d, shade(wood, 0.8));\n K.box('wood', hx + Math.cos(angle) * L / 2, 0.0, Math.sin(angle) * L / 2, L, h - 0.02, 0.04, wood, -angle); K.cyl('metal', hx + Math.cos(angle) * (L - 0.08), 1.0, Math.sin(angle) * (L - 0.08) + 0.04, 0.025, 0.06, 0xc8b070, Math.PI / 2, 0);\n for (const y of [0.2, 1.0, 1.8]) K.box('metal', hx, y, 0.0, 0.02, 0.1, 0.02, 0x8a8a8a); };\n return B\n}\n","import type { BuilderContext, BuilderMap } from '../types'\nimport { shade } from '../colors'\nimport type { Kit } from '../types'\nimport { green } from './shared'\n\nexport function plantBuilders({ kit }: BuilderContext): Partial<BuilderMap> {\n const B: Partial<BuilderMap> = {}\n const foliage = (K: Kit, x: number, y: number, z: number, n: number, w: number, h0: number, h1: number, tilt0: number, tilt1: number, twist = 2.4) => { for (let i = 0; i < n; i++) { const an = i * twist + K.rnd() * 0.5, t = tilt0 + (tilt1 - tilt0) * K.rnd(); K.leaf(x + Math.cos(an) * 0.03, y, z + Math.sin(an) * 0.03, w * (0.8 + K.rnd() * 0.4), h0 + (h1 - h0) * K.rnd(), green(K, i), an, t); } };\n B.floor_plant = (M, a) => { const K = kit(M, a), { w } = K.dim('floor_plant'), r = Math.min(0.24, w * 0.4), pot = K.main(0xb5603c);\n K.cyl('plain', 0, 0, 0, r, 0.38, pot); K.cyl('plain', 0, 0.34, 0, r * 1.07, 0.05, shade(pot, 0.92)); K.cyl('plain', 0, 0.385, 0, r * 0.92, 0.012, 0x3a2a1e);\n foliage(K, 0, 0.39, 0, 11, 0.2, 0.5, 0.85, 0.2, 0.85); };\n B.table_plant = (M, a) => { const K = kit(M, a), y = M.y || 0, pot = K.main(0xf0eee8);\n K.cyl('plain', 0, y, 0, 0.05, 0.08, pot); K.cyl('plain', 0, y + 0.08, 0, 0.045, 0.008, 0x3a2a1e); foliage(K, 0, y + 0.085, 0, 7, 0.07, 0.1, 0.2, 0.2, 0.9); };\n B.hanging_plant = (M, a) => { const K = kit(M, a), t = K.ceiling(), h = K.dim('hanging_plant').h, yp = t - h, pot = K.main(0xf0eee8), rope = K.accent(0xd8ccb0);\n K.box('metal', 0, t - 0.025, 0, 0.03, 0.025, 0.03, 0x8a8a8a); for (let i = 0; i < 3; i++) { const an = i * 2.09; K.box('fabric', Math.cos(an) * 0.08, yp + 0.12, Math.sin(an) * 0.08, 0.01, t - yp - 0.15, 0.01, rope); }\n K.cyl('plain', 0, yp, 0, 0.1, 0.14, pot); K.cyl('plain', 0, yp + 0.135, 0, 0.09, 0.01, 0x3a2a1e);\n for (let i = 0; i < 11; i++) { const an = i * 2.4, up = i < 3; K.leaf(Math.cos(an) * 0.06, yp + 0.14, Math.sin(an) * 0.06, 0.09, up ? 0.22 : 0.4 + K.rnd() * 0.3, green(K, i), an, up ? 0.5 : 2.3 + K.rnd() * 0.3); } };\n return B\n}\n","import type { BuilderContext, BuilderMap } from '../types'\nimport type { PropType } from '../catalog'\nimport { hex, shade } from '../colors'\nimport type { Kit } from '../types'\nimport { drawTv, pick } from './shared'\n\nexport function wallBuilders({ kit, atlas }: BuilderContext): Partial<BuilderMap> {\n const B: Partial<BuilderMap> = {}\n B.wall_ac = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('wall_ac'), y = M.y || 2.15;\n K.box('plain', 0, y, 0, w, h, d, 0xf0f0ee); K.box('plain', 0, y + h - 0.02, d * 0.1, w - 0.04, 0.02, d * 0.8, 0xe6e6e2); // curved cover\n K.box('plain', 0, y + 0.02, d / 2 - 0.02, w - 0.1, 0.035, 0.04, 0x30343a); K.box('plain', 0, y - 0.005, d / 2 - 0.03, w - 0.12, 0.012, 0.07, 0xe6e6e2, 0, 0.45); // slot and flap\n K.box('plain', w / 2 - 0.1, y + h - 0.1, d / 2 + 0.001, 0.05, 0.02, 0.004, 0x2a3a30); K.box('plain', -w / 2 + 0.01, y + 0.02, 0.0, 0.01, h - 0.04, d - 0.02, 0xdcdcd8); };\n const frame = (K: Kit, id: PropType, def: number, large: boolean) => { const { w, d, h } = K.dim(id), y = K.M.y || def, fr = Math.min(w, h) * 0.07 + 0.008, margin = large ? 0.05 : 0.035, A = atlas('picture'), idx = K.M.art != null ? K.M.art % 7 : Math.floor(K.rnd() * 7);\n const cm = K.color('picture_accent', pick(K, [0x4a3324, 0x1c1c1c, 0xe8e2d4, 0x8a6a44])); K.box('wood', 0, y, 0, w, h, d, K.M.color != null ? hex(K.M.color) : cm);\n K.box('plain', 0, y + fr, d / 2 + 0.0005, w - 2 * fr, h - 2 * fr, 0.004, 0xf4f1e8); K.plane('atlas_picture', 0, y + h / 2, d / 2 + 0.005, w - 2 * fr - 2 * margin, h - 2 * fr - 2 * margin, 0xffffff, 0, 0, 0, A.uv(idx)); };\n B.picture_small = (M, a) => frame(kit(M, a), 'picture_small', 1.3, false);\n B.picture_large = (M, a) => frame(kit(M, a), 'picture_large', 1.2, true);\n B.mirror = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('mirror'), y = M.y || 0.9, A = atlas('picture'), cm = K.color('metal', 0x3a3a3c);\n K.box('metal', 0, y, 0, w, h, d, M.color != null ? hex(M.color) : cm); K.plane('atlas_picture', 0, y + h / 2, d / 2 + 0.003, w - 0.05, h - 0.05, 0xffffff, 0, 0, 0, A.mirror()); };\n B.curtain = (M, a) => { const K = kit(M, a), { w, h } = K.dim('curtain'), y = M.y != null && M.y > 0 ? M.y : 0.1, color = K.fabric(pick(K, [0xd8cdb4, 0x9fb0be, 0xc5a88c, 0xe0dcd0])), isOpen = M.open !== false, top = y + h;\n K.box('metal', 0, top, 0, w + 0.1, 0.025, 0.025, K.metal(0x5a5048)); for (const s of [-1, 1]) K.cyl('metal', s * (w / 2 + 0.07), top - 0.02, 0, 0.03, 0.07, 0x5a5048, 0, 0, 'sphere');\n const pw = isOpen ? Math.min(0.5, w * 0.3) : w / 2 + 0.03, n = Math.max(4, Math.round(pw / 0.08));\n for (const s of [-1, 1]) for (let i = 0; i < n; i++) { const ux = isOpen ? s * (w / 2 + 0.03 - pw + (i + 0.5) * pw / n) : s * ((i + 0.5) * pw / n); K.box('fabric', ux, y, (i % 2 ? 0.025 : -0.01), pw / n * 1.15, h - 0.02, 0.02, i % 2 ? shade(color, 0.9) : color); }\n if (isOpen) for (const s of [-1, 1]) K.box('fabric', s * (w / 2 - pw * 0.25), y + h * 0.4, 0.04, pw * 0.5, 0.05, 0.02, shade(color, 0.7)); }; // tie-back\n B.blinds = (M, a) => { const K = kit(M, a), { w, h } = K.dim('blinds'), y = M.y || 1.0, retract = M.retracted != null ? M.retracted : 0.15, isWood = M.variant === 'wood', color = isWood ? K.wood(0x9a7a52) : K.color('wall_living', 0xe8e2d4), mat = isWood ? 'wood' : 'plain', yb = y + h * retract, n = Math.floor((y + h - yb) / 0.055);\n K.box('plain', 0, y + h - 0.05, 0, w, 0.05, 0.06, 0xdcd8d0); for (let i = 0; i < n; i++) K.box(mat, 0, y + h - 0.06 - (i + 1) * 0.055, 0.0, w - 0.05, 0.01, 0.035, color, 0, 0.3);\n for (const s of [-1, 1]) K.box('plain', s * (w / 2 - 0.05), yb, 0.02, 0.008, y + h - 0.05 - yb, 0.008, 0xb8b4ac); K.box('plain', 0, yb - 0.02, 0, w - 0.03, 0.025, 0.04, 0xdcd8d0); };\n B.flat_tv = (M, a) => { const K = kit(M, a), { w } = K.dim('flat_tv'); drawTv(K, 0, M.y != null && M.y > 0 ? M.y : 1.0, 0, w, false); };\n return B\n}\n","import type { BuilderContext, BuilderMap } from '../types'\nimport { hex, shade } from '../colors'\nimport { green, pick } from './shared'\n\nexport function yardBuilders({ kit, cover }: BuilderContext): Partial<BuilderMap> {\n const B: Partial<BuilderMap> = {}\n B.ac_condenser = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('ac_condenser'), y = M.y != null && M.y > 0 ? M.y : 0.5;\n for (const s of [-1, 1]) { K.box('metal', s * w * 0.32, y - 0.06, -d / 2 - 0.2, 0.04, 0.04, 0.3, 0x6a6a6a); K.box('metal', s * w * 0.32, y - 0.5 + 0.06, -d / 2 - 0.2 - 0.0, 0.035, 0.46, 0.035, 0x6a6a6a, 0, 0.0); }\n K.box('plain', 0, y, 0, w, h, d, 0xe8e8e4); K.box('plain', 0, y + h - 0.02, 0, w + 0.02, 0.025, d + 0.02, 0xd8d8d4);\n K.cyl('metal', -w * 0.12, y + h / 2, d / 2 + 0.004, 0.2, 0.012, 0x2a2a2e, Math.PI / 2, 0); K.cyl('metal', -w * 0.12, y + h / 2, d / 2 + 0.012, 0.085, 0.02, 0x4a4a4e, Math.PI / 2, 0);\n for (let i = 0; i < 4; i++) K.box('metal', -w * 0.12, y + h / 2 - 0.2, d / 2 + 0.018, 0.4, 0.008, 0.008, 0x8a8a8a, 0, 0, i * 0.785);\n for (let i = 0; i < 3; i++) K.cyl('metal', -w * 0.12, y + h / 2, d / 2 + 0.02, 0.07 + i * 0.043, 0.006, 0x7a7a7e, Math.PI / 2, 0);\n for (let i = 0; i < 6; i++) K.box('plain', w * 0.33, y + 0.06 + i * 0.075, d / 2 + 0.003, 0.14, 0.03, 0.006, 0xc4c4c0);\n K.cyl('metal', w / 2 - 0.05, y + 0.1, -d / 2 - 0.08, 0.012, 0.18, 0xb8733c, Math.PI / 2, 0); K.box('plain', w / 2 - 0.05, y + 0.06, -d / 2 - 0.02, 0.05, 0.12, 0.05, 0xe0e0dc); };\n B.pool = (M, a) => { const K = kit(M, a), { w, d } = K.dim('pool'), t = 0.32, bo = K.color('exterior_floor', 0xe8e4da);\n K.box('plain', 0, 0, -d / 2 - t / 2, w + 2 * t, 0.1, t, bo); K.box('plain', 0, 0, d / 2 + t / 2, w + 2 * t, 0.1, t, bo); for (const s of [-1, 1]) K.box('plain', s * (w / 2 + t / 2), 0, 0, t, 0.1, d, bo);\n cover(a, M, 'pool_tile', { w, d, y: 0.035, size: 1.0 });\n K.plane('water', 0, 0.075, 0, w, d, 0x8fd4ee, 0, -Math.PI / 2, 0);\n for (let i = 0; i < 3; i++) K.box('plain', w / 2 - 0.35 + i * 0.0, 0.035 + i * 0.01, d / 2 - 0.2 - i * 0.28, 0.7 - i * 0.05, 0.03, 0.26, 0xdfeef2);\n for (const s of [-1, 1]) { K.box('metal', -w / 4 + s * 0.2, 0.0, d / 2 + t * 0.6, 0.03, 0.85, 0.03, 0xc8ccd0); K.box('metal', -w / 4 + s * 0.2, 0.85, d / 2 + t * 0.6 - 0.1, 0.03, 0.03, 0.22, 0xc8ccd0); K.box('metal', -w / 4 + s * 0.2, 0.0, d / 2 - 0.25, 0.03, 0.88, 0.03, 0xc8ccd0, 0, 0.0); }\n for (let i = 0; i < 3; i++) K.box('metal', -w / 4, 0.02, d / 2 - 0.08 - i * 0.07, 0.4, 0.015, 0.05, 0xc8ccd0); };\n B.deck = (M, a) => { const K = kit(M, a), { w, d } = K.dim('deck'), h = 0.2, wood = K.wood(0x6a4a30);\n K.box('wood', 0, 0, 0, w - 0.05, h, d - 0.05, shade(wood, 0.7)); cover(a, M, 'deck_floor', { w, d, y: h + 0.012, size: 1.2, color: M.color });\n for (const [x, z, ww, dd] of [[0, -d / 2, w, 0.05], [0, d / 2, w, 0.05], [-w / 2, 0, 0.05, d], [w / 2, 0, 0.05, d]]) K.box('wood', x, 0, z, ww, h + 0.02, dd, wood);\n K.box('wood', 0, 0, d / 2 + 0.17, 1.4, 0.1, 0.34, wood); K.box('wood', 0, 0.1, d / 2 + 0.17, 1.4, 0.012, 0.34, shade(wood, 1.2)); };\n B.sun_lounger = (M, a) => { const K = kit(M, a), { w } = K.dim('sun_lounger'), fr = K.metal(0xe8e8e4), al = 0.95, cu = K.fabric(0x3a6a9a), stripe = K.accent(0xf0ece0);\n for (const s of [-1, 1]) { K.box('metal', s * (w / 2 - 0.03), 0.17, -0.3, 0.035, 0.035, 1.35, fr); K.box('metal', s * (w / 2 - 0.03), 0.0, 0.95, 0.035, 0.2, 0.035, fr); K.box('metal', s * (w / 2 - 0.03), 0.0, -0.3, 0.035, 0.2, 0.035, fr); K.cyl('plain', s * (w / 2 - 0.03), 0.07, -0.3, 0.07, 0.03, 0x2a2a2a, 0, Math.PI / 2); }\n K.box('fabric', 0, 0.21, 0.35, w - 0.1, 0.06, 1.3, cu); K.ramp('fabric', 0, 0.2, -0.3, w - 0.1, 0.06, 0.8, cu, al);\n for (let i = 0; i < 4; i++) { K.box('fabric', 0, 0.265, 0.0 + i * 0.3, w - 0.098, 0.004, 0.08, stripe); } K.ramp('metal', w / 2 - 0.03, 0.18, -0.3, 0.03, 0.03, 0.5, fr, al - 0.3); K.ramp('metal', -w / 2 + 0.03, 0.18, -0.3, 0.03, 0.03, 0.5, fr, al - 0.3); };\n B.garden_bed = (M, a) => { const K = kit(M, a), { w, d } = K.dim('garden_bed'), wood = K.wood(0x9a7048), hb = 0.3;\n for (const s of [-1, 1]) { K.box('wood', 0, 0, s * (d / 2 - 0.015), w, hb, 0.03, wood); K.box('wood', s * (w / 2 - 0.015), 0, 0, 0.03, hb, d - 0.06, shade(wood, 0.9)); }\n K.box('plain', 0, 0.02, 0, w - 0.06, hb - 0.05, d - 0.06, 0x3a2a1e);\n const lin = d > 0.8 ? 3 : 2, n = Math.max(3, Math.round(w / 0.4));\n for (let r = 0; r < lin; r++) for (let i = 0; i < n; i++) { const x = -w / 2 + (i + 0.5) * w / n, z = (r - (lin - 1) / 2) * 0.3;\n if (r === 0) { for (let j = 0; j < 6; j++) K.leaf(x, hb - 0.02, z, 0.11, 0.14, green(K, j) , j * 1.05, 0.9); }\n else if (r === 1) { K.box('wood', x, hb - 0.03, z, 0.015, 0.75, 0.015, 0x8a7048); K.cyl('plant', x, hb + 0.05, z, 0.1, 0.28, green(K, i), 0, 0, 'sphere'); K.cyl('plain', x + 0.05, hb + 0.18, z + 0.07, 0.03, 0.06, 0xc83030, 0, 0, 'sphere'); K.cyl('plain', x - 0.06, hb + 0.3, z + 0.05, 0.03, 0.06, 0xd04030, 0, 0, 'sphere'); }\n else { K.cyl('plant', x, hb - 0.02, z, 0.1, 0.17, 0x8ab05a, 0, 0, 'sphere'); for (let j = 0; j < 5; j++) K.leaf(x, hb, z, 0.14, 0.2, green(K, j + 1), j * 1.3, 1.0); } } };\n B.fruit_tree = (M, a) => { const K = kit(M, a), { w, h } = K.dim('fruit_tree'), fruit = M.fruit || pick(K, [0xe8801c, 0xe8a030, 0xb8c050, 0xc83030, 0xd8d040]), tr = K.wood(0x6a4a30);\n K.cyl('wood', 0, 0, 0, 0.13, h * 0.5, tr); K.box('wood', 0.2, h * 0.35, 0, 0.08, 0.6, 0.08, tr, 0, 0, -0.7); K.box('wood', -0.2, h * 0.35, 0.05, 0.08, 0.55, 0.08, tr, 0, 0, 0.7);\n const bl = [[0, h * 0.72, 0, 1.0], [0.55, h * 0.62, 0.2, 0.7], [-0.55, h * 0.64, -0.1, 0.72], [0.1, h * 0.6, -0.55, 0.65], [0.1, h * 0.9, 0.1, 0.6]];\n bl.forEach(([x, y, z, r], i) => K.cyl('plant', x * w / 2.4, y - r * 0.4, z * w / 2.4, r * w / 2.4, r * 1.5 * w / 2.4, green(K, i), 0, 0, 'sphere'));\n for (let i = 0; i < 16; i++) { const b = bl[i % 4], an = K.rnd() * 6.28, el = K.rnd() * 0.9, r = b[3] * w / 2.4 * 0.95; K.cyl('plain', b[0] * w / 2.4 + Math.cos(an) * r * Math.cos(el), b[1] * 1.0 - b[3] * 0.4 + Math.sin(el) * r * 0.5, b[2] * w / 2.4 + Math.sin(an) * r * Math.cos(el), 0.055, 0.11, fruit, 0, 0, 'sphere'); } };\n B.palm_tree = (M, a) => { const K = kit(M, a), { h } = K.dim('palm_tree'), tr = K.wood(0x8a7a5a), lean = 0.35, n = 9, sh = h * 0.78 / n; let x = 0, y = 0;\n for (let i = 0; i < n; i++) { const r = 0.14 - i * 0.006; K.cyl('wood', x, y, 0, r, sh, i % 2 ? tr : shade(tr, 0.82)); y += sh; x += lean * (2 * i + 1) / (n * n) * 1.2; }\n const top = y;\n for (let i = 0; i < 10; i++) { const an = i * 0.628 + K.rnd() * 0.2, t1 = 0.7 + K.rnd() * 0.4, L = 1.5, w = 0.5; K.leaf(x, top, 0, w, L, green(K, i), an, t1);\n const ex = x + Math.sin(t1) * Math.sin(an) * L, ey = top + Math.cos(t1) * L, ez = Math.sin(t1) * Math.cos(an) * L; K.leaf(ex, ey, ez, w * 0.7, 0.9, green(K, i + 1), an, t1 + 1.0); }\n for (let i = 0; i < 3; i++) K.cyl('wood', x + Math.cos(i * 2.1) * 0.14, top - 0.2, Math.sin(i * 2.1) * 0.14, 0.09, 0.2, 0x6a5a3a, 0, 0, 'sphere'); };\n B.pergola = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('pergola'), wood = M.variant === 'dark' ? shade(K.wood(0x6a4a30), 0.8) : K.color('exterior_floor', 0xe8e2d4), pc = M.color != null ? hex(M.color) : wood;\n for (const sx of [-1, 1]) for (const sz of [-1, 1]) K.box('wood', sx * (w / 2 - 0.1), 0, sz * (d / 2 - 0.1), 0.14, h, 0.14, pc);\n for (const sz of [-1, 1]) K.box('wood', 0, h - 0.04, sz * (d / 2 - 0.1), w + 0.2, 0.16, 0.08, pc); const n = Math.round(w / 0.4);\n for (let i = 0; i <= n; i++) K.box('wood', -w / 2 - 0.1 + i * (w + 0.2) / n, h + 0.12, 0, 0.06, 0.08, d + 0.3, shade(pc, 0.92));\n for (let i = 0; i < 9; i++) K.leaf(-w / 2 + 0.1 + (i % 2) * 0.06, 0.2 + i * 0.25, -d / 2 + 0.1, 0.2, 0.28, green(K, i), i * 1.7, 0.7); for (let i = 0; i < 7; i++) K.leaf(-w / 2 + 0.2 + i * 0.35, h + 0.2, -d / 4 + K.rnd() * 0.3, 0.22, 0.3, green(K, i), i * 2.0, 1.3); };\n B.slide = (M, a) => { const K = kit(M, a), { d, h } = K.dim('slide'), py = h * 0.8, pz = -d / 2 + 0.55, az = K.accent(0xe8c030), vm = K.main(0xc8403a), az2 = 0x3a6ab0;\n for (const sx of [-1, 1]) for (const sz of [-1, 1]) K.box('metal', sx * 0.3, 0, pz + sz * 0.3, 0.05, py, 0.05, az2);\n K.box('plain', 0, py, pz, 0.74, 0.05, 0.74, vm); for (const s of [-1, 1]) { K.box('plain', s * 0.35, py + 0.05, pz, 0.04, 0.42, 0.74, az); }\n K.box('plain', 0, py + 0.05, pz - 0.35, 0.74, 0.42, 0.04, az); const run = d / 2 - pz - 0.4, al = Math.atan2(py - 0.1, run);\n K.ramp('plain', 0, py, pz + 0.37, 0.5, 0.04, Math.hypot(run, py - 0.1) + 0.1, 0xd8d0c0, -al + 0.0 + 0); for (const s of [-1, 1]) K.ramp('plain', s * 0.27, py + 0.03, pz + 0.37, 0.04, 0.12, Math.hypot(run, py - 0.1) + 0.1, az, -al);\n for (const s of [-1, 1]) K.box('metal', s * 0.28, 0, pz - 0.45, 0.04, py + 0.1, 0.04, az2, 0, -0.1 + 0); for (let i = 0; i < 5; i++) K.box('metal', 0, 0.2 + i * (py - 0.2) / 5, pz - 0.46, 0.56, 0.03, 0.03, 0xc8ccd0); K.box('plain', 0, py + 0.4, pz, 0.8, 0.04, 0.8, vm); };\n B.swing = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('swing'), mt = K.color('metal', 0x3a6ab0), L = Math.hypot(h, d / 2), an = Math.atan2(d / 2, h);\n for (const sx of [-1, 1]) for (const sz of [-1, 1]) K.box('metal', sx * w / 2, h / 2 - L / 2 + 0.0, sz * d / 4, 0.07, L, 0.07, mt, 0, -sz * an, 0);\n K.box('metal', 0, h - 0.04, 0, w + 0.1, 0.1, 0.1, shade(mt, 0.85)); const cs = [K.accent(0xc8403a), 0x2a2a2a];\n for (let i = 0; i < 2; i++) { const x = (i - 0.5) * w * 0.45, sy = 0.5, sw = 0.46; for (const s of [-1, 1]) K.box('metal', x + s * (sw / 2 - 0.02), sy, 0, 0.012, h - 0.05 - sy, 0.012, 0xa0a4a8); K.box('plain', x, sy - 0.04, 0, sw, 0.045, 0.17, cs[i]); } };\n B.bbq_simple = (M, a) => { const K = kit(M, a), { w, d } = K.dim('bbq_simple'), br = K.color('exterior_floor', 0xb25a40), hb = 0.78;\n K.box('plain', 0, 0, 0, w, 0.45, d, br); for (const [x, z, ww, dd] of [[0, -d / 2 + 0.06, w, 0.12], [0, d / 2 - 0.06, w, 0.12], [-w / 2 + 0.06, 0, 0.12, d - 0.2], [w / 2 - 0.06, 0, 0.12, d - 0.2]]) K.box('plain', x, 0.45, z, ww, hb - 0.45, dd, br);\n K.box('plain', 0, 0.45, 0, w - 0.22, 0.04, d - 0.22, 0x2a2624); for (let i = 0; i < 10; i++) K.box('plain', (K.rnd() - 0.5) * (w - 0.3), 0.49, (K.rnd() - 0.5) * (d - 0.3), 0.08, 0.02, 0.07, i % 3 ? 0xc84a1a : 0x3a2a24, K.rnd() * 3);\n for (let i = 0; i < 7; i++) K.box('metal', -w / 2 + 0.16 + i * (w - 0.32) / 6, hb + 0.01, 0, 0.012, 0.012, d - 0.1, 0x5a5a5e); K.box('metal', 0, hb + 0.01, -d / 2 + 0.2, w - 0.1, 0.012, 0.02, 0x5a5a5e);\n for (let i = 0; i < 3; i++) { const z = (i - 1) * 0.15; K.cyl('metal', 0, hb + 0.05, z, 0.006, w + 0.12, 0xb0b4b8, 0, Math.PI / 2); for (let j = 0; j < 3; j++) K.cyl('plain', -w * 0.25 + j * w * 0.22, hb + 0.05, z, 0.035, 0.07, [0x8a3a2a, 0xa8483a, 0xd8b090][j], 0, Math.PI / 2); }\n for (let i = 0; i < 4; i++) K.cyl('wood', w / 2 + 0.2, 0.05 + (i > 2 ? 0.1 : 0), -0.1 + (i % 3) * 0.1, 0.04, 0.3, 0x8a6a44, 0, Math.PI / 2); };\n B.garage_car = (M, a) => { const K = kit(M, a), { w, d } = K.dim('garage_car'), c = K.main(pick(K, [0xa8aaac, 0xe8e8e6, 0x2a3a5a, 0xa83a34, 0x222426])), zf = d / 2;\n K.box('plain', 0, 0.26, 0, w, 0.5, d, c); K.box('plain', 0, 0.5, zf - 0.7, w - 0.04, 0.1, 1.4, shade(c, 1.04), 0, 0.04); K.box('plain', 0, 0.76, -zf + 0.3, w - 0.04, 0.1, 0.6, c);\n const cz = -0.35; for (const sx of [-1, 1]) for (const sz of [-1, 1]) K.box('plain', sx * (w / 2 - 0.1), 0.76, cz + sz * 0.8, 0.07, 0.5, 0.07, shade(c, 0.9)); K.box('plain', 0, 1.26, cz, w - 0.1, 0.05, 1.75, shade(c, 1.04));\n K.box('glass', 0, 0.76, cz, w - 0.06, 0.5, 1.6, 0xffffff); K.box('glass', 0, 0.76, cz + 0.8, w - 0.12, 0.5, 0.04, 0xffffff, 0, 0.5); for (const s of [-1, 1]) { K.box('plain', s * 0.4, 0.55, cz + 0.4, 0.4, 0.3, 0.4, 0x2a2a2e); K.box('plain', s * 0.4, 0.55, cz - 0.45, 0.4, 0.3, 0.4, 0x2a2a2e); }\n for (const sx of [-1, 1]) for (const sz of [-1, 1]) { K.cyl('plain', sx * (w / 2 - 0.1), 0.3, sz * 1.3, 0.3, 0.2, 0x1a1a1a, 0, Math.PI / 2); K.cyl('metal', sx * (w / 2 - 0.0), 0.3, sz * 1.3, 0.17, 0.02, 0xb8bcc0, 0, Math.PI / 2); }\n for (const s of [-1, 1]) { K.box('plain', s * 0.62, 0.5, zf - 0.02, 0.32, 0.12, 0.05, 0xf4f0d8); K.box('plain', s * 0.62, 0.52, -zf + 0.02, 0.3, 0.1, 0.05, 0xb02a24); K.box('plain', s * (w / 2 + 0.03), 0.9, cz + 0.55, 0.1, 0.1, 0.14, shade(c, 0.9)); }\n K.box('plain', 0, 0.2, zf - 0.01, w, 0.16, 0.06, 0x2a2a2a); K.box('plain', 0, 0.2, -zf + 0.01, w, 0.16, 0.06, 0x2a2a2a); K.box('plain', 0, 0.32, zf + 0.02, 0.5, 0.16, 0.02, 0x1c1c1c); K.box('plain', 0, 0.28, -zf - 0.015, 0.4, 0.12, 0.01, 0xf0e8c0); };\n B.gourmet_counter = (M, a) => { const K = kit(M, a), { w, d } = K.dim('gourmet_counter'), br = K.color('exterior_floor', 0xa8553c), zc = -d / 2 + 0.35, xa = -w / 2 + 0.8, xb = xa + 0.8 + 0.72, xc = xb + 0.72 + 0.65, wood = K.wood(0x8a5a38), gr = 0x4a4844, cab = K.color('wall_kitchen', 0xe8e4dc);\n K.box('plain', xa, 0, zc, 1.6, 0.85, 0.7, br); for (let i = 1; i < 4; i++) K.box('plain', xa, i * 0.2, zc + 0.351, 1.6, 0.012, 0.004, shade(br, 0.7)); K.box('plain', xa, 0.12, zc + 0.352, 0.7, 0.4, 0.004, 0x1a1612);\n for (const [x, z, ww, dd] of [[xa, zc - 0.25, 1.6, 0.2], [xa, zc + 0.3, 1.6, 0.1], [xa - 0.75, zc, 0.1, 0.7], [xa + 0.75, zc, 0.1, 0.7]]) K.box('plain', x, 0.85, z, ww, 0.12, dd, br);\n K.box('plain', xa, 0.85, zc + 0.02, 1.4, 0.025, 0.4, 0x2a2624); for (let i = 0; i < 8; i++) K.box('metal', xa - 0.62 + i * 0.18, 0.97, zc + 0.02, 0.012, 0.012, 0.4, 0x5a5a5e); for (let i = 0; i < 6; i++) K.box('plain', xa + (K.rnd() - 0.5) * 1.2, 0.88, zc + (K.rnd() - 0.5) * 0.3, 0.1, 0.02, 0.08, i % 2 ? 0xc84a1a : 0x3a2a24, K.rnd() * 3);\n K.box('plain', xa, 1.5, zc - 0.07, 1.5, 0.4, 0.56, br); K.box('plain', xa, 1.5, zc + 0.215, 1.5, 0.4, 0.01, 0x1a1612); K.box('plain', xa, 1.9, zc - 0.12, 0.62, 1.0, 0.46, br); K.box('plain', xa, 2.9, zc - 0.12, 0.76, 0.08, 0.6, shade(br, 0.8)); K.box('plain', xa, 2.98, zc - 0.12, 0.5, 0.06, 0.4, shade(br, 0.75));\n K.box('plain', xb, 0, zc, 1.44, 0.85, 0.7, cab); for (let i = 0; i < 3; i++) K.box('wood', xb - 0.48 + i * 0.48, 0.08, zc + 0.355, 0.44, 0.7, 0.012, wood); K.box('plain', (xb + xc) / 2, 0.85, zc, 0.72 * 2 + 0.65 + 0.04, 0.04, 0.74, gr);\n K.box('metal', xb, 0.89, zc, 0.5, 0.01, 0.38, 0xb0b4b8); K.box('metal', xb, 0.895, zc, 0.44, 0.006, 0.32, 0x70767a); K.cyl('metal', xb, 0.89, zc - 0.28, 0.014, 0.3, 0xb8bcc0); K.box('metal', xb, 1.17, zc - 0.2, 0.02, 0.02, 0.11, 0xb8bcc0);\n K.box('plain', xc, 0, zc, 1.3, 0.85, 0.7, cab); for (let i = 0; i < 2; i++) K.box('wood', xc - 0.32 + i * 0.64, 0.08, zc + 0.355, 0.6, 0.7, 0.012, wood);\n K.cyl('plain', xc, 0.89, zc, 0.1, 0.05, 0xf0f0ea); K.cyl('plain', xc + 0.4, 0.89, zc, 0.04, 0.26, 0x2a6a3a);\n const tz = 0.55, tl = Math.min(2.6, w * 0.65); K.box('wood', 0, 0.74, tz, tl, 0.05, 0.85, wood); for (const s of [-1, 1]) { K.box('wood', s * (tl / 2 - 0.2), 0, tz, 0.08, 0.74, 0.7, shade(wood, 0.8)); K.box('wood', s * (tl / 2 - 0.2), 0.3, tz - 0.001, 0.0 + 0.6, 0.04, 0.05, shade(wood, 0.75)); }\n for (const s of [-1, 1]) { K.box('wood', 0, 0.42, tz + s * 0.72, tl - 0.2, 0.04, 0.3, shade(wood, 1.1)); for (const sx of [-1, 1]) K.box('wood', sx * (tl / 2 - 0.4), 0, tz + s * 0.72, 0.06, 0.42, 0.26, shade(wood, 0.8)); }\n K.cyl('plain', 0.3, 0.79, tz, 0.15, 0.08, 0xd8d0c0); K.cyl('plain', -0.5, 0.79, tz + 0.1, 0.035, 0.26, 0x2a6a3a); for (let i = 0; i < 3; i++) K.cyl('plain', -0.9 + i * 0.2, 0.79, tz - 0.2, 0.035, 0.1, 0xe8e4d8); };\n B.sauna = (M, a) => { const K = kit(M, a), { w, d, h } = K.dim('sauna'), t = 0.08, wood = K.wood(0xc08a54), shade1 = shade(wood, 0.88), dw = 0.7, dx = w / 2 - 0.55;\n K.box('wood', 0, 0, 0, w, 0.05, d, shade(wood, 0.75)); K.box('wood', 0, 0.05, -d / 2 + t / 2, w, h - 0.11, t, wood); for (const s of [-1, 1]) K.box('wood', s * (w / 2 - t / 2), 0.05, 0, t, h - 0.11, d - 2 * t * 0 , wood);\n K.box('wood', 0, h - 0.06, 0, w + 0.12, 0.06, d + 0.12, shade(wood, 0.75));\n const lx0 = -w / 2, lx1 = dx - dw / 2; K.box('wood', (lx0 + lx1) / 2, 0.05, d / 2 - t / 2, lx1 - lx0, h - 0.11, t, wood); K.box('wood', (dx + dw / 2 + w / 2) / 2, 0.05, d / 2 - t / 2, w / 2 - (dx + dw / 2), h - 0.11, t, wood); K.box('wood', dx, 1.85, d / 2 - t / 2, dw, h - 0.11 - 1.8, t, wood);\n for (let i = 0; i < 12; i++) { const x = lx0 + 0.1 + i * (lx1 - lx0) / 12; K.box('wood', x, 0.05, d / 2 + 0.003, 0.012, h - 0.11, 0.005, shade(wood, 0.6)); }\n K.box('wood', dx - dw / 2 - 0.025, 0.05, d / 2 - 0.02, 0.05, 1.82, 0.06, shade1); K.box('wood', dx + dw / 2 + 0.025, 0.05, d / 2 - 0.02, 0.05, 1.82, 0.06, shade1); K.box('glass', dx, 0.06, d / 2 - 0.02, dw, 1.75, 0.02, 0xffffff); K.box('metal', dx - dw / 2 + 0.08, 0.7, d / 2 + 0.03, 0.02, 0.5, 0.03, 0xc8ccd0);\n K.box('wood', 0, 0.85, -d / 2 + t + 0.25, w - 2 * t, 0.05, 0.5, shade(wood, 1.1)); K.box('wood', 0, 0.45, -d / 2 + t + 0.65, w - 2 * t, 0.05, 0.4, shade(wood, 1.1)); K.box('wood', 0, 0.05, -d / 2 + t + 0.1, w - 2 * t, 0.8, 0.04, shade(wood, 0.8));\n K.cyl('metal', -w / 2 + 0.3, 0.05, d / 2 - 0.4, 0.14, 0.55, 0x2a2a2e); for (let i = 0; i < 5; i++) K.cyl('plain', -w / 2 + 0.3 + Math.cos(i * 1.3) * 0.06, 0.6, d / 2 - 0.4 + Math.sin(i * 1.3) * 0.06, 0.04, 0.07, 0x6a6a6a, 0, 0, 'sphere');\n K.box('plain', 0, h - 0.2, -d / 2 + t + 0.02, 0.4, 0.06, 0.04, 0xfff0c8); K.box('emissive', 0, h - 0.19, -d / 2 + t + 0.045, 0.34, 0.01, 0.006, 0xffe0a0); };\n B.soaking_tub = (M, a) => { const K = kit(M, a), { w } = K.dim('soaking_tub'), r = Math.min(0.62, w * 0.48), wood = K.wood(0xa8784a);\n K.cyl('wood', 0, 0, 0, r, 0.78, wood, 0, 0, 'cyl'); K.cyl('wood', 0, 0.76, 0, r + 0.025, 0.04, shade(wood, 1.15)); for (const y of [0.15, 0.5]) K.cyl('metal', 0, y, 0, r + 0.012, 0.03, 0x2a2a2e);\n K.cyl('water', 0, 0.74, 0, r - 0.04, 0.01, 0x9ad4e8); for (let i = 0; i < 8; i++) K.box('wood', Math.cos(i * 0.785) * (r + 0.001), 0.02, Math.sin(i * 0.785) * (r + 0.001), 0.012, 0.74, 0.12, shade(wood, 0.7), -i * 0.785 + 0);\n K.box('wood', 0, 0, r + 0.4, 0.5, 0.3, 0.34, shade(wood, 0.9)); K.cyl('wood', 0.2, 0.3, r + 0.4, 0.1, 0.14, shade(wood, 1.1)); K.box('wood', -0.1, 0.34, r + 0.4, 0.3, 0.02, 0.025, shade(wood, 1.1), 0.5); K.box('fabric', -0.1, 0.3, r + 0.42, 0.26, 0.03, 0.2, K.accent(0xf0ece0)); };\n return B\n}\n","import { hex, mix, rng, seedOf } from './colors'\nimport type { AtlasMap, CoverOptions, FloorKind, PropItem, PropsApi, Uv } from './types'\n\n/** Default height of each floor kind above the ground. */\nconst HEIGHTS: Record<FloorKind, number> = { lawn: 0.02, dirt: 0.025, concrete: 0.03, tile_floor: 0.035, deck_floor: 0.04, pool_tile: 0.04, paving: 0.035 }\n\nconst inside = (poly: Array<[number, number]>, x: number, z: number): boolean => {\n let in_ = false\n for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {\n const [xi, zi] = poly[i]\n const [xj, zj] = poly[j]\n if (zi > z !== zj > z && x < ((xj - xi) * (z - zi)) / (zj - zi) + xi) in_ = !in_\n }\n return in_\n}\n\n/**\n * Covers a rectangle centered on M (w x d) or a polygon `[[x, z], ...]` in local coordinates with floor tiles:\n * one atlas square per cell (repeats and tiles); the polygon uses cells of a quarter of the size.\n */\nexport function createCover(atlas: <K extends 'floor'>(name: K) => AtlasMap[K]) {\n return function cover(api: PropsApi, M: PropItem, kind: FloorKind, o: CoverOptions = {}): void {\n const A = atlas('floor')\n const size = o.size || A.size(kind)\n const nv = A.variants(kind)\n const y = o.y != null ? o.y : HEIGHTS[kind] || 0.03\n const color = o.color != null ? hex(o.color) : 0xffffff\n const poly = o.poly || M.poly\n const sd = seedOf(M)\n const rnd = rng(sd)\n const tint = () => {\n const v = 0.93 + rnd() * 0.07\n return mix(0x000000, color, v)\n }\n const quad = (cx: number, cz: number, qw: number, qd: number, uv: Uv) => api.plane(M, 'atlas_floor', cx, y, cz, qw, qd, tint(), 0, -Math.PI / 2, 0, uv)\n if (!poly) {\n const w = o.w || M.w || 4\n const d = o.d || M.d || 4\n for (let j = 0, v = 0; v < d - 1e-6; j++, v += size)\n for (let i = 0, u = 0; u < w - 1e-6; i++, u += size) {\n const qw = Math.min(size, w - u)\n const qd = Math.min(size, d - v)\n const b = A.uv(kind, (i * 7 + j * 13 + sd) % nv)\n const uv = { u0: b.u0, u1: b.u0 + ((b.u1 - b.u0) * qw) / size, v1: b.v1, v0: b.v1 - ((b.v1 - b.v0) * qd) / size }\n quad(-w / 2 + u + qw / 2, -d / 2 + v + qd / 2, qw, qd, uv)\n }\n return\n }\n let x0 = 1e9\n let z0 = 1e9\n let x1 = -1e9\n let z1 = -1e9\n for (const [x, z] of poly) {\n x0 = Math.min(x0, x)\n x1 = Math.max(x1, x)\n z0 = Math.min(z0, z)\n z1 = Math.max(z1, z)\n }\n const st = size / 4\n for (let j = 0; z0 + j * st < z1; j++)\n for (let i = 0; x0 + i * st < x1; i++) {\n const cx = x0 + (i + 0.5) * st\n const cz = z0 + (j + 0.5) * st\n if (!inside(poly, cx, cz)) continue\n const b = A.uv(kind, (((i >> 2) * 7 + (j >> 2) * 13 + sd) % nv))\n const du = (b.u1 - b.u0) / 4\n const dv = (b.v1 - b.v0) / 4\n const ui = i & 3\n const vj = j & 3\n quad(cx, cz, st, st, { u0: b.u0 + ui * du, u1: b.u0 + (ui + 1) * du, v1: b.v1 - vj * dv, v0: b.v1 - (vj + 1) * dv })\n }\n }\n}\n","import { DEFS } from './catalog'\nimport { hex, rng, seedOf } from './colors'\nimport type { EmitFn, Kit, PropItem, PropsApi } from './types'\n\n/** Shortcuts that fix `M` and the api, so builders read as a list of primitives. */\nexport function makeKit(M: PropItem, api: PropsApi, emit: EmitFn): Kit {\n const role = (name: string): number | null => {\n const v = M.palette && M.palette[name]\n return v != null ? hex(v) : null\n }\n const K: Kit = {\n M,\n api,\n rnd: rng(seedOf(M)),\n box: (material, x, y, z, w, h, d, color, ry, rx, rz) => api.box(M, material, x, y, z, w, h, d, hex(color), ry || 0, rx || 0, rz || 0),\n cyl: (material, x, y, z, r, h, color, rx, rz, shape) => api.cylinder(M, material, x, y, z, r, h, hex(color), rx || 0, rz || 0, shape),\n leaf: (x, y, z, w, h, color, ry, rx) => api.leaf(M, x, y, z, w, h, hex(color), ry || 0, rx || 0),\n plane: (material, x, y, z, w, h, color, ry, rx, rz, uv) => api.plane(M, material, x, y, z, w, h, hex(color), ry || 0, rx || 0, rz || 0, uv),\n light: (x, y, z, color, range, kind) => {\n if (api.light) api.light(M, x, y, z, hex(color), range, kind)\n else emit('light', { M, x, y, z, color: hex(color), range, kind })\n },\n // colours: role in the house palette (M.palette) or the default; main = M.color\n color: (name, fallback) => role(name) ?? hex(fallback),\n main: (fallback) => (M.color != null ? hex(M.color) : hex(fallback)),\n fabric: (fallback) => (M.color != null ? hex(M.color) : (role('fabric_main') ?? hex(fallback))),\n accent: (fallback) => role('fabric_accent') ?? hex(fallback),\n wood: (fallback) => role('wood') ?? hex(fallback),\n metal: (fallback) => role('metal') ?? hex(fallback),\n dim: (id) => {\n const t = DEFS[id]\n return { w: M.w || t.w, d: M.d || t.d, h: M.h || t.h }\n },\n ceiling: () => M.ceiling || 2.9,\n // long tilted box: starts at (x, y, z) and follows u = (0, sin a, -cos a) for L meters\n ramp(material, x, y, z, w, t, L, color, alpha) {\n const cy = y + (Math.sin(alpha) * L) / 2\n const cz = z - (Math.cos(alpha) * L) / 2\n K.box(material, x, cy - t / 2, cz, w, t, L, color, 0, alpha + Math.PI, 0)\n },\n }\n return K\n}\n","import type { PropType } from './catalog'\n\n/** Locales shipped with the package. Unknown locales fall back to `en`. */\nexport type PropsLocale = 'en' | 'pt-BR'\n\nexport interface PropsStrings {\n /** Display name of each prop type. */\n typeNames: Record<PropType, string>\n /** Names of the picture atlas cells (4 abstract, 3 landscapes, the mirror). */\n pictureNames: string[]\n}\n\nexport const locales: Record<PropsLocale, PropsStrings> = {\n en: {\n typeNames: {\n ceiling_light: \"Ceiling light\",\n pendant: \"Pendant lamp\",\n track_spot: \"Track spot\",\n chandelier: \"Chandelier\",\n wall_ac: \"Indoor AC unit (split)\",\n picture_small: \"Small picture\",\n picture_large: \"Large picture\",\n mirror: \"Mirror\",\n curtain: \"Curtain\",\n blinds: \"Blinds\",\n flat_tv: \"Flat TV\",\n floor_plant: \"Floor plant\",\n table_plant: \"Table plant\",\n hanging_plant: \"Hanging plant\",\n tv_stand: \"TV stand with flat TV\",\n breakfast_bar: \"Breakfast bar\",\n sofa_l: \"L-shaped sofa\",\n double_bed_modern: \"Modern double bed\",\n home_office: \"Home office\",\n closet: \"Closet\",\n vanity: \"Bathroom vanity\",\n ac_condenser: \"AC condenser\",\n lawn: \"Lawn\",\n tile_floor: \"Ceramic tile floor\",\n concrete: \"Concrete slab\",\n deck_floor: \"Deck (flooring)\",\n dirt: \"Packed dirt\",\n gourmet_counter: \"Gourmet counter\",\n sauna: \"Sauna\",\n soaking_tub: \"Soaking tub\",\n pool: \"Pool\",\n deck: \"Raised deck\",\n sun_lounger: \"Sun lounger\",\n garden_bed: \"Vegetable garden bed\",\n fruit_tree: \"Fruit tree\",\n palm_tree: \"Palm tree\",\n pergola: \"Pergola\",\n slide: \"Slide\",\n swing: \"Swing\",\n bbq_simple: \"Simple barbecue\",\n garage_car: \"Car\",\n window_boards: \"Boards on the window\",\n barricade: \"Barricade\",\n sandbags: \"Sandbags\",\n barbed_wire: \"Barbed wire\",\n razor_wire: \"Razor wire\",\n water_bottle_crates: \"Mineral water crates\",\n canned_food: \"Canned food\",\n water_jug: \"Water jug\",\n floor_mattress: \"Mattress on the floor\",\n lantern: \"Lantern\",\n candles: \"Candles\",\n open_suitcases: \"Open suitcases\",\n clothes_on_floor: \"Clothes on the floor\",\n fallen_chair: \"Fallen chair\",\n blood_stains: \"Blood stains\",\n open_door: \"Open door\",\n },\n pictureNames: ['warm abstract', 'blue abstract', 'color blocks', 'minimal', 'sunset', 'mountains and lake', 'beach with palm tree', 'mirror'],\n },\n 'pt-BR': {\n typeNames: {\n ceiling_light: \"Plafon\",\n pendant: \"Pendente\",\n track_spot: \"Spot de trilho\",\n chandelier: \"Lustre\",\n wall_ac: \"Split interno\",\n picture_small: \"Quadro pequeno\",\n picture_large: \"Quadro grande\",\n mirror: \"Espelho\",\n curtain: \"Cortina\",\n blinds: \"Persiana\",\n flat_tv: \"TV plana\",\n floor_plant: \"Vaso de chão\",\n table_plant: \"Vaso de mesa\",\n hanging_plant: \"Planta pendente\",\n tv_stand: \"Rack com TV plana\",\n breakfast_bar: \"Bancada americana\",\n sofa_l: \"Sofá em L\",\n double_bed_modern: \"Cama de casal moderna\",\n home_office: \"Home office\",\n closet: \"Closet\",\n vanity: \"Lavabo\",\n ac_condenser: \"Condensadora\",\n lawn: \"Gramado\",\n tile_floor: \"Piso cerâmico\",\n concrete: \"Cimentado\",\n deck_floor: \"Deck (piso)\",\n dirt: \"Terra batida\",\n gourmet_counter: \"Bancada gourmet\",\n sauna: \"Sauna\",\n soaking_tub: \"Ofurô\",\n pool: \"Piscina\",\n deck: \"Deck elevado\",\n sun_lounger: \"Espreguiçadeira\",\n garden_bed: \"Canteiro de horta\",\n fruit_tree: \"Árvore frutífera\",\n palm_tree: \"Palmeira\",\n pergola: \"Pergolado\",\n slide: \"Escorregador\",\n swing: \"Balanço\",\n bbq_simple: \"Churrasqueira simples\",\n garage_car: \"Carro\",\n window_boards: \"Tábuas na janela\",\n barricade: \"Barricada\",\n sandbags: \"Sacos de areia\",\n barbed_wire: \"Arame farpado\",\n razor_wire: \"Concertina\",\n water_bottle_crates: \"Caixas de água mineral\",\n canned_food: \"Enlatados\",\n water_jug: \"Galão de água\",\n floor_mattress: \"Colchão no chão\",\n lantern: \"Lanterna\",\n candles: \"Velas\",\n open_suitcases: \"Malas abertas\",\n clothes_on_floor: \"Roupas no chão\",\n fallen_chair: \"Cadeira caída\",\n blood_stains: \"Manchas de sangue\",\n open_door: \"Porta aberta\",\n },\n pictureNames: ['abstrato quente', 'abstrato azul', 'blocos de cor', 'minimal', 'pôr do sol', 'montanhas e lago', 'praia com coqueiro', 'espelho'],\n },\n}\n\nexport const resolveStrings = (locale?: string): PropsStrings => locales[locale as PropsLocale] ?? locales.en\n","import { createAtlasProvider } from './atlas'\nimport { DEFS, MATERIALS, TYPE_DEFS, type PropType } from './catalog'\nimport { ceilingBuilders } from './builders/ceiling'\nimport { floorBuilders } from './builders/floors'\nimport { furnitureBuilders } from './builders/furniture'\nimport { houseStateBuilders } from './builders/house-state'\nimport { plantBuilders } from './builders/plants'\nimport { wallBuilders } from './builders/wall'\nimport { yardBuilders } from './builders/yard'\nimport { createCover } from './cover'\nimport { makeKit } from './kit'\nimport { resolveStrings } from './locale'\nimport type { BuilderContext, BuilderMap, DocumentLike, EmitFn, Props3d, Props3dOptions, TypeInfo } from './types'\n\n/** Type metadata with display names for a locale (`en` by default). */\nexport function listTypes(locale?: string): TypeInfo[] {\n const names = resolveStrings(locale).typeNames\n return TYPE_DEFS.map((t) => ({ ...t, name: names[t.id] }))\n}\n\n/**\n * Creates the props kit. Builders only describe primitives through the `api` you pass to them; nothing here touches a renderer.\n *\n * `three` is accepted so the call looks like the other game-kit packages, but the props never use it.\n *\n * ```ts\n * const props = create(THREE, { document })\n * props.builders.sofa_l({ w: 2.8, d: 1.7, h: 0.85, seed: 'living' }, api)\n * ```\n */\nexport function create(_three?: unknown, options: Props3dOptions = {}): Props3d {\n const emit: EmitFn = options.emit || (() => {})\n const doc: DocumentLike | null = options.document || (typeof document !== 'undefined' ? document : null)\n const strings = resolveStrings(options.locale)\n const atlas = createAtlasProvider(doc, strings.pictureNames)\n const cover = createCover(atlas)\n const ctx: BuilderContext = { kit: (M, api) => makeKit(M, api, emit), atlas, cover }\n const builders = Object.assign(\n {},\n ceilingBuilders(ctx),\n wallBuilders(ctx),\n plantBuilders(ctx),\n furnitureBuilders(ctx),\n floorBuilders(ctx),\n yardBuilders(ctx),\n houseStateBuilders(ctx),\n ) as BuilderMap\n return {\n builders,\n types: listTypes(options.locale),\n names: Object.keys(builders) as PropType[],\n atlas,\n cover,\n materials: MATERIALS,\n }\n}\n\nexport { DEFS, MATERIALS, TYPE_DEFS }\n","import type * as ThreeNamespace from 'three'\n\ntype Three = typeof ThreeNamespace\n\n/** What `createNodeSkin` makes: one mesh that stands for a whole tree of meshes, each vertex tied to the node of the tree that moves it. */\nexport type NodeSkin = {\n readonly mesh: ThreeNamespace.Mesh\n /** The nodes the animation moves, in the order of the skin's `aNodeIndex`. */\n readonly nodes: readonly ThreeNamespace.Object3D[]\n /** The matrix of each node, uploaded as the `uNodes` uniform. */\n readonly matrices: { readonly value: ThreeNamespace.Matrix4[] }\n /** Copies the world matrix of every node into `matrices`: call it once per frame, after the tree was animated. */\n update(): void\n}\n\nexport type NodeSkinOptions = {\n /** Meshes the animation moves and that therefore stay nodes of their own (a head ball, an eye). Groups and meshes with children are always nodes. */\n readonly nodes?: Iterable<ThreeNamespace.Object3D>\n}\n\nconst MAX_WALK_DEPTH = 3\n\n/**\n * Every Object3D reachable from the fields of `owner` (up to 3 levels, skipping the keys in `skip`): what the animation of a tree keeps in its own\n * state (head, jaw, wings, legs). Pass the result as `nodes` so those meshes are not merged into their parents.\n */\nexport const collectReferencedNodes = (owner: object, skip: readonly string[] = []): Set<ThreeNamespace.Object3D> => {\n const found = new Set<ThreeNamespace.Object3D>()\n const seen = new Set<unknown>()\n const walk = (value: unknown, depth: number): void => {\n if (!value || typeof value !== 'object' || depth > MAX_WALK_DEPTH || seen.has(value)) return\n seen.add(value)\n const candidate = value as { isObject3D?: boolean; isMaterial?: boolean; isBufferGeometry?: boolean }\n if (candidate.isObject3D) {\n found.add(value as ThreeNamespace.Object3D)\n return\n }\n if (candidate.isMaterial || candidate.isBufferGeometry) return\n for (const key in value) walk((value as Record<string, unknown>)[key], depth + 1)\n }\n for (const key in owner) if (!skip.includes(key)) walk((owner as Record<string, unknown>)[key], 0)\n return found\n}\n\ntype TextureSample = { readonly data: Uint8ClampedArray; readonly width: number; readonly height: number }\n\nconst isVisibleInTree = (object: ThreeNamespace.Object3D): boolean => {\n for (let node: ThreeNamespace.Object3D | null = object; node; node = node.parent) if (!node.visible) return false\n return true\n}\n\n/**\n * A GPU \"node skin\": a tree of about fifty meshes (an animal, a character prop) merged into one mesh, every vertex tied to the node that moves it.\n * The tree stays out of the scene and keeps animating; `update()` copies its node matrices each frame. Colour (with the texture sampled at the uv),\n * roughness, metalness and emissive are baked per vertex, so the 40 animals of a scene cost 40 draw calls instead of 2,000.\n * Generalised from the game's `bichoPele` (G 66536); the colours are read the way r160 does (linear converted to sRGB), as the game does.\n */\nexport const createNodeSkin = (THREE: Three, root: ThreeNamespace.Object3D, options: NodeSkinOptions = {}): NodeSkin => {\n const referenced = new Set(options.nodes ?? [])\n const nodes: ThreeNamespace.Object3D[] = []\n const indexOf = new Map<ThreeNamespace.Object3D, number>()\n root.traverse((object) => {\n const mesh = object as ThreeNamespace.Mesh\n if (!mesh.isMesh || object.children.length > 0 || referenced.has(object)) {\n indexOf.set(object, nodes.length)\n nodes.push(object)\n }\n })\n\n const positions: number[] = []\n const normals: number[] = []\n const tints: number[] = []\n const surfaces: number[] = []\n const nodeIndices: number[] = []\n const samples = new Map<ThreeNamespace.Texture, TextureSample>()\n const color = new THREE.Color()\n const vertex = new THREE.Vector3()\n const normal = new THREE.Vector3()\n\n const sampleOf = (texture: ThreeNamespace.Texture): TextureSample | undefined => {\n const image = texture.image as { getContext?: (kind: string) => CanvasRenderingContext2D | null; width: number; height: number } | undefined\n if (!image?.getContext) return undefined\n const cached = samples.get(texture)\n if (cached) return cached\n const context = image.getContext('2d')\n if (!context) return undefined\n const sample = { data: context.getImageData(0, 0, image.width, image.height).data, width: image.width, height: image.height }\n samples.set(texture, sample)\n return sample\n }\n\n root.traverse((object) => {\n const mesh = object as ThreeNamespace.Mesh\n if (!mesh.isMesh || !isVisibleInTree(object)) return\n const material = (Array.isArray(mesh.material) ? mesh.material[0] : mesh.material) as ThreeNamespace.MeshStandardMaterial\n const geometry = mesh.geometry\n const position = geometry.attributes.position as ThreeNamespace.BufferAttribute\n const normalAttribute = geometry.attributes.normal as ThreeNamespace.BufferAttribute | undefined\n const uv = geometry.attributes.uv as ThreeNamespace.BufferAttribute | undefined\n const matrix = new THREE.Matrix4()\n let nodeIndex = indexOf.get(object)\n if (nodeIndex === undefined) {\n nodeIndex = indexOf.get(object.parent as ThreeNamespace.Object3D) ?? 0\n object.updateMatrix()\n matrix.copy(object.matrix)\n }\n const normalMatrix = new THREE.Matrix3().getNormalMatrix(matrix)\n const sample = material.map ? sampleOf(material.map) : undefined\n const emissive = material.emissive ? (material.emissive.r + material.emissive.g + material.emissive.b) * (material.emissiveIntensity ?? 1) : 0\n const index = geometry.index ? geometry.index.array : null\n const count = index ? index.length : position.count\n for (let item = 0; item < count; item++) {\n const at = index ? (index[item] as number) : item\n vertex.fromBufferAttribute(position, at).applyMatrix4(matrix)\n positions.push(vertex.x, vertex.y, vertex.z)\n if (normalAttribute) normal.fromBufferAttribute(normalAttribute, at).applyMatrix3(normalMatrix).normalize()\n else normal.set(0, 1, 0)\n normals.push(normal.x, normal.y, normal.z)\n color.copy(material.color || color.set(1, 1, 1)).convertLinearToSRGB()\n if (sample && uv && material.map) {\n const repeat = material.map.repeat\n const u = (((uv.getX(at) * repeat.x) % 1) + 1) % 1\n const v = (((uv.getY(at) * repeat.y) % 1) + 1) % 1\n const pixelX = Math.min(sample.width - 1, (u * sample.width) | 0)\n const pixelY = Math.min(sample.height - 1, ((1 - v) * sample.height) | 0)\n const offset = (pixelY * sample.width + pixelX) * 4\n color.multiply(new THREE.Color().setRGB((sample.data[offset] as number) / 255, (sample.data[offset + 1] as number) / 255, (sample.data[offset + 2] as number) / 255, THREE.SRGBColorSpace))\n }\n if (emissive > 0.3) color.lerp(material.emissive, 0.7)\n tints.push(color.r, color.g, color.b)\n surfaces.push(material.roughness ?? 0.8, material.metalness ?? 0, emissive > 0.3 ? 1 : 0)\n nodeIndices.push(nodeIndex)\n }\n })\n\n const geometry = new THREE.BufferGeometry()\n geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))\n geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3))\n geometry.setAttribute('aTint', new THREE.Float32BufferAttribute(tints, 3))\n geometry.setAttribute('aSurface', new THREE.Float32BufferAttribute(surfaces, 3))\n geometry.setAttribute('aNodeIndex', new THREE.Float32BufferAttribute(nodeIndices, 1))\n geometry.computeBoundingSphere()\n\n const matrices = { value: nodes.map(() => new THREE.Matrix4()) }\n const skinVertex = (shader: ThreeNamespace.WebGLProgramParametersWithUniforms): void => {\n shader.uniforms.uNodes = matrices\n shader.vertexShader = shader.vertexShader\n .replace(\n '#include <common>',\n `#include <common>\\nuniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3 aTint; attribute vec3 aSurface; varying vec3 vTint; varying vec3 vSurface;`,\n )\n .replace('#include <beginnormal_vertex>', 'vec3 objectNormal = mat3(uNodes[int(aNodeIndex + .5)]) * normal;')\n .replace('#include <begin_vertex>', 'vec3 transformed = (uNodes[int(aNodeIndex + .5)] * vec4(position, 1.)).xyz; vTint = aTint; vSurface = aSurface;')\n }\n\n const material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0 })\n material.onBeforeCompile = (shader) => {\n skinVertex(shader)\n shader.fragmentShader = shader.fragmentShader\n .replace('#include <common>', '#include <common>\\nvarying vec3 vTint; varying vec3 vSurface;')\n .replace('vec4 diffuseColor = vec4( diffuse, opacity );', 'vec4 diffuseColor = vec4( vTint, opacity );')\n .replace('#include <roughnessmap_fragment>', 'float roughnessFactor = vSurface.x;')\n .replace('#include <metalnessmap_fragment>', 'float metalnessFactor = vSurface.y;')\n .replace('#include <emissivemap_fragment>', 'totalEmissiveRadiance = vTint * vSurface.z * 1.5;')\n }\n material.customProgramCacheKey = () => `nodeSkin${nodes.length}`\n const depthMaterial = new THREE.MeshDepthMaterial({ depthPacking: THREE.RGBADepthPacking })\n depthMaterial.onBeforeCompile = skinVertex\n depthMaterial.customProgramCacheKey = () => `nodeSkinDepth${nodes.length}`\n\n const mesh = new THREE.Mesh(geometry, material)\n mesh.customDepthMaterial = depthMaterial\n mesh.frustumCulled = false\n root.updateMatrixWorld(true)\n return {\n mesh,\n nodes,\n matrices,\n update() {\n root.updateMatrixWorld(true)\n for (let index = 0; index < nodes.length; index++) (matrices.value[index] as ThreeNamespace.Matrix4).copy((nodes[index] as ThreeNamespace.Object3D).matrixWorld)\n },\n }\n}\n","import type { Primitive, PropsApi } from './types'\n\n/**\n * An api that records every primitive instead of drawing it. Handy for tests, previews and for measuring a prop.\n *\n * ```ts\n * const { api, primitives } = createRecorder()\n * props.builders.sofa_l(item, api)\n * console.log(primitives.length)\n * ```\n * Pass `{ light: false }` to leave `api.light` undefined, so builders fall back to `emit('light', ...)`.\n */\nexport function createRecorder(options: { light?: boolean } = {}): { api: PropsApi; primitives: Primitive[] } {\n const primitives: Primitive[] = []\n const api: PropsApi = {\n box: (M, material, x, y, z, w, h, d, color, ry, rx, rz) => void primitives.push({ kind: 'box', M, material, x, y, z, w, h, d, color, ry, rx, rz }),\n cylinder: (M, material, x, y, z, r, h, color, rx, rz, shape) => void primitives.push({ kind: 'cylinder', M, material, x, y, z, r, h, color, rx, rz, shape }),\n leaf: (M, x, y, z, w, h, color, ry, rx) => void primitives.push({ kind: 'leaf', M, x, y, z, w, h, color, ry, rx }),\n plane: (M, material, x, y, z, w, h, color, ry, rx, rz, uv) => void primitives.push({ kind: 'plane', M, material, x, y, z, w, h, color, ry, rx, rz, uv }),\n }\n if (options.light !== false) {\n api.light = (M, x, y, z, color, range, lightKind) => void primitives.push({ kind: 'light', M, x, y, z, color, range, lightKind })\n }\n return { api, primitives }\n}\n","/**\n * Every visible mesh under `root`, baked into `space` and joined per material. It lives in `@adatechnology/game-shapes-three` now (on the\n * game-shapes merge, the same bytes as the version this package had); this re-export keeps the old import working.\n */\nexport { mergeByMaterial } from '@adatechnology/game-shapes-three'\n","import type { GroupNode, ShapeNode, ShapeSchema, Vec3 } from '@adatechnology/game-shapes'\n\nimport type { CylinderShape, PropItem, PropsApi, Uv } from './types'\n\nexport interface ShapeRecorderOptions {\n /** Radial segments of `cyl` and `cone` (default 12). */\n readonly cylinderSegments?: number\n /** Radial segments of `cyl6` (default 6). */\n readonly hexSegments?: number\n /** Width and height segments of `sphere` (default 12 x 8). */\n readonly sphereSegments?: readonly [number, number]\n /** Name of the schema. */\n readonly name?: string\n}\n\n/** A part as the api received it: the first call's pivot and turn (later calls with the same id draw into it). */\nexport interface RecordedPart {\n readonly id: string\n readonly pivot: Vec3\n readonly ry: number\n}\n\n/** The props api plus parts (the `HouseholdApi` contract): a part draws in its own frame, at `pivot`, turned by `ry`. */\nexport interface ShapeRecorderApi extends PropsApi {\n light(M: PropItem, x: number, y: number, z: number, color: number, range: number, kind: string): void\n part(id: string, pivot: Vec3, ry: number, draw: () => void): void\n}\n\nexport interface ShapeRecorder {\n readonly api: ShapeRecorderApi\n /** The game-shapes schema of everything drawn so far. */\n schema(): ShapeSchema\n /** The parts in the order of their first call. */\n readonly parts: readonly RecordedPart[]\n}\n\n/** The double-sided diamond of `leaf`: base at the origin, tip at +y, in the xy plane (front face counter-clockwise seen from +z). */\nconst LEAF = (() => {\n const f = [0, 0, 0, -0.5, 0.4, 0, 0.5, 0.4, 0, 0, 1, 0]\n const positions: number[] = []\n const normals: number[] = []\n const uvs: number[] = []\n const indices: number[] = []\n for (const side of [1, -1]) {\n const base = positions.length / 3\n for (let i = 0; i < 4; i++) {\n positions.push(f[i * 3] as number, f[i * 3 + 1] as number, f[i * 3 + 2] as number)\n normals.push(0, 0, side)\n uvs.push((f[i * 3] as number) + 0.5, f[i * 3 + 1] as number)\n }\n if (side === 1) indices.push(base, base + 2, base + 1, base + 1, base + 2, base + 3)\n else indices.push(base, base + 1, base + 2, base + 1, base + 3, base + 2)\n }\n return { positions, normals, uvs, indices }\n})()\n\n/**\n * An api that turns what a builder draws into a `game-shapes` schema, with the arithmetic of the interactables mesh api: each primitive\n * under its item frame (`heading` = `M.ry`, at `M.x, 0, M.z`), placed by `compose(position, euler YXZ, scale)` over a unit template (a box\n * with its base at `y`, a cylinder centred as the api says, a sphere of radius `h / 2`, the double-sided leaf, a unit plane with the atlas\n * cell as its uv), parts as part nodes, lights as points (`data: { light: true, color, range, kind }`).\n *\n * ```ts\n * const recorder = createShapeRecorder()\n * props.builders.sofa_l(item, recorder.api)\n * const buffers = generate(recorder.schema(), { validate: false, key: false })\n * ```\n */\nexport function createShapeRecorder(options: ShapeRecorderOptions = {}): ShapeRecorder {\n const cyl = options.cylinderSegments ?? 12\n const hex = options.hexSegments ?? 6\n const [sphereW, sphereH] = options.sphereSegments ?? [12, 8]\n const root: ShapeNode[] = []\n const parts: RecordedPart[] = []\n const partChildren = new Map<string, ShapeNode[]>()\n let current = root\n // consecutive primitives of the same item frame share one frame group (one matrix, the same products)\n let lastFrame: { list: ShapeNode[]; group: GroupNode & { children: ShapeNode[] }; x: number; z: number; ry: number } | null = null\n\n const into = (M: PropItem | undefined): ShapeNode[] => {\n const x = M?.x ?? 0\n const z = M?.z ?? 0\n const ry = M?.ry ?? 0\n const last = lastFrame\n if (last && last.list === current && current[current.length - 1] === last.group && Object.is(last.x, x) && Object.is(last.z, z) && Object.is(last.ry, ry)) return last.group.children\n const group = { kind: 'group' as const, position: [x, 0, z] as Vec3, heading: ry, children: [] as ShapeNode[] }\n current.push(group)\n lastFrame = { list: current, group, x, z, ry }\n return group.children\n }\n\n const api: ShapeRecorderApi = {\n box(M, material, x, y, z, w, h, d, color, ry, rx, rz) {\n into(M).push({ kind: 'box', material, color, position: [x, y + h / 2, z], rotation: [rx, ry, rz], rotationOrder: 'YXZ', scale: [w, h, d] })\n },\n cylinder(M, material, x, y, z, r, h, color, rx, rz, shape?: CylinderShape) {\n const tilted = rx !== 0 || rz !== 0\n const cy = shape === 'sphere' ? y + h / 2 : tilted ? y : y + h / 2\n const sy = shape === 'sphere' ? h / 2 : h\n const place = { material, color, position: [x, cy, z] as Vec3, rotation: [rx, 0, rz] as Vec3, rotationOrder: 'YXZ' as const, scale: [r, sy, r] as Vec3 }\n const list = into(M)\n switch (shape) {\n case 'cyl6':\n list.push({ kind: 'cylinder', radiusTop: 1, radiusBottom: 1, height: 1, radialSegments: hex, ...place })\n break\n case 'cone':\n list.push({ kind: 'cylinder', radiusTop: 0, radiusBottom: 1, height: 1, radialSegments: cyl, ...place })\n break\n case 'sphere':\n list.push({ kind: 'sphere', radius: 1, widthSegments: sphereW, heightSegments: sphereH, ...place })\n break\n default:\n list.push({ kind: 'cylinder', radiusTop: 1, radiusBottom: 1, height: 1, radialSegments: cyl, ...place })\n }\n },\n leaf(M, x, y, z, w, h, color, ry, rx) {\n into(M).push({ kind: 'mesh', ...LEAF, material: 'plant', color, position: [x, y, z], rotation: [rx, ry, 0], rotationOrder: 'YXZ', scale: [w, h, 1] })\n },\n plane(M, material, x, y, z, w, h, color, ry, rx, rz, uv?: Uv) {\n into(M).push({\n kind: 'plane',\n width: 1,\n height: 1,\n material,\n color,\n position: [x, y, z],\n rotation: [rx, ry, rz],\n rotationOrder: 'YXZ',\n scale: [w, h, 1],\n ...(uv ? { uv: { u0: uv.u0, v0: uv.v0, u1: uv.u1, v1: uv.v1 } } : {}),\n })\n },\n light(M, x, y, z, color, range, kind) {\n into(M).push({ kind: 'point', position: [x, y, z], data: { light: true, color, range, kind } })\n },\n part(id, pivot, ry, draw) {\n let children = partChildren.get(id)\n if (!children) {\n children = []\n partChildren.set(id, children)\n parts.push({ id, pivot: [pivot[0], pivot[1], pivot[2]], ry })\n current.push({ kind: 'group', part: id, position: [pivot[0], pivot[1], pivot[2]], rotation: [0, ry, 0], children })\n }\n const outer = current\n current = children\n try {\n draw()\n } finally {\n current = outer\n }\n },\n }\n\n return {\n api,\n parts,\n schema: () => ({ version: 1, ...(options.name !== undefined ? { name: options.name } : {}), root: { kind: 'group', children: root } }),\n }\n}\n","import type { BirdSpec } from '@adatechnology/game-animals'\n\nimport type { AnimateParams } from './animate-types'\nimport type { AnimalRig } from './types'\n\nconst { PI } = Math\n\n/** Advances a bird's cycle (page: `animBird`): steps, flight for the species that fly, wings, peck, alarm, tail. */\nexport const animateBird = (rig: AnimalRig, params: AnimateParams): void => {\n const { time: t, delta: dt, clip } = params\n const species = rig.species\n const spec = species.body as BirdSpec\n const isVulture = species.id === 'vulture'\n const isChicken = species.id === 'chicken'\n const isLapwing = species.id === 'southern_lapwing'\n const flying = clip === 'run' && !isChicken\n const hz = clip === 'run' && !flying ? species.runHz : clip === 'walk' ? species.walkHz : 0\n rig.phase += dt * (flying ? (isVulture ? 1.1 : 4.5) : hz) * 2 * PI\n const targetHeight = flying ? rig.size * 1.2 : 0\n rig.trunk.position.y += (targetHeight - rig.trunk.position.y) * Math.min(1, dt * 3)\n rig.legs.forEach((leg, index) => {\n if (flying) {\n leg.hip.rotation.x = 1.2\n leg.knee.rotation.x = -1.6\n } else if (clip === 'idle') {\n leg.hip.rotation.x = 0\n leg.knee.rotation.x = 0\n } else {\n const phase = rig.phase + index * PI\n leg.hip.rotation.x = -Math.sin(phase) * 0.45\n leg.knee.rotation.x = -Math.max(0, Math.cos(phase)) * 0.7\n }\n leg.foot.rotation.x = -(leg.hip.rotation.x + leg.knee.rotation.x)\n })\n rig.wings.forEach((wing) => {\n const { side, shoulder, elbow } = wing\n if (flying) {\n const flap = isVulture ? 0.25 + Math.sin(rig.phase) * 0.18 : Math.sin(rig.phase) * 0.9\n shoulder.rotation.y += (0 - shoulder.rotation.y) * Math.min(1, dt * 6)\n shoulder.rotation.z = side * flap\n elbow.rotation.z = side * (isVulture ? 0.08 : Math.sin(rig.phase - 0.6) * 0.4)\n elbow.rotation.y = 0\n shoulder.scale.x += (1 - shoulder.scale.x) * Math.min(1, dt * 6)\n } else {\n const alert = isLapwing && clip === 'idle' ? Math.max(0, Math.sin(t * 1.3)) * 0.9 : 0\n const flutter = isChicken && clip === 'run' ? Math.abs(Math.sin(rig.phase * 1.5)) * 0.8 : 0\n shoulder.rotation.y += ((side * PI) / 2 * 0.97 - alert * side * 0.9 - shoulder.rotation.y) * Math.min(1, dt * 6)\n shoulder.rotation.z = side * (-0.06 + flutter + alert * 0.6)\n elbow.rotation.z = side * 0.04\n elbow.rotation.y = 0\n shoulder.scale.x += ((alert > 0.1 ? 1 : 0.58) - shoulder.scale.x) * Math.min(1, dt * 6)\n }\n })\n const peck = isChicken && clip === 'idle' ? Math.max(0, Math.sin(t * 1.6)) ** 8 * 1.2 : 0\n if (rig.neck) {\n rig.neck.rotation.x = rig.neckAngle + (clip === 'walk' && !isVulture ? Math.max(0, Math.sin(rig.phase * 2)) * 0.35 : 0) + peck + (flying ? 0.6 : 0)\n rig.neck.rotation.y = clip === 'idle' ? Math.sin(t * 0.9) * 0.5 : 0\n }\n if (rig.jaw) rig.jaw.rotation.x = isLapwing && clip === 'idle' ? Math.max(0, Math.sin(t * 1.3)) * Math.abs(Math.sin(t * 14)) * 0.5 : 0\n if (rig.tailRoot) rig.tailRoot.rotation.x = (spec.tailA || 0) + (flying ? 0.6 : 0)\n}\n","import type { GaitPhases, QuadrupedSpec } from '@adatechnology/game-animals'\n\nimport type { AnimateParams } from './animate-types'\nimport type { AnimalRig } from './types'\n\nconst { PI } = Math\n\n/** Advances a quadruped's step cycle (page: `animQuad`): legs by gait phase, trunk bob, neck, head, jaw and tail; a dead one lies down. */\nexport const animateQuadruped = (rig: AnimalRig, params: AnimateParams, gaits: { readonly walk: GaitPhases; readonly trot: GaitPhases; readonly gallop: GaitPhases }): void => {\n const { time: t, delta: dt, clip } = params\n const species = rig.species\n const spec = species.body as QuadrupedSpec\n if (rig.dead) {\n rig.root.rotation.z += (PI / 2 * 0.97 - rig.root.rotation.z) * Math.min(1, dt * 3)\n rig.root.position.y += (spec.R * (spec.sx || 1) * 0.85 - rig.root.position.y) * Math.min(1, dt * 3)\n rig.legs.forEach((leg) => {\n leg.hip.rotation.x = leg.angle1 + (leg.isFront ? -0.5 : 0.4)\n leg.knee.rotation.x = 0.05\n leg.foot.rotation.x = 0\n })\n if (rig.neck) rig.neck.rotation.x = rig.neckAngle + 0.35\n if (rig.jaw) rig.jaw.rotation.x = 0.25\n return\n }\n const hz = (clip === 'run' ? species.runHz : clip === 'walk' ? species.walkHz : 0) * (rig.infected ? 1.2 : 1)\n rig.phase += dt * hz * 2 * PI\n const gait = clip === 'run' ? (species.gallop ? gaits.gallop : gaits.trot) : gaits.walk\n const amplitude = clip === 'run' ? 0.62 : 0.34\n const flex = clip === 'run' ? 1.15 : 0.75\n rig.legs.forEach((leg, index) => {\n if (clip === 'idle') {\n leg.hip.rotation.x = leg.angle1\n leg.knee.rotation.x = leg.angle2\n } else {\n const phase = rig.phase + (gait[index] as number)\n const limit = rig.infected && index === 1 ? 0.35 : 1\n leg.hip.rotation.x = leg.angle1 - Math.sin(phase) * amplitude * limit\n leg.knee.rotation.x = leg.angle2 + Math.max(0, Math.cos(phase)) * flex * limit\n }\n leg.foot.rotation.x = -(leg.hip.rotation.x + leg.knee.rotation.x) * 0.85\n })\n const bob = clip === 'idle' ? 0 : Math.abs(Math.sin(rig.phase)) * (clip === 'run' ? 0.05 : 0.018) * rig.size\n rig.trunk.position.y = bob * 0.6\n const bodyGroup = rig.bodyGroup\n if (bodyGroup) {\n bodyGroup.rotation.x = clip === 'run' && species.gallop ? Math.sin(rig.phase + PI * 0.4) * 0.07 : 0\n bodyGroup.scale.y = 1 + (clip === 'idle' ? Math.sin(t * 2.2) * 0.012 : 0)\n }\n const low = rig.infected ? 0.35 : 0\n if (rig.neck) {\n rig.neck.rotation.x = rig.neckAngle + low + (clip === 'idle' ? Math.sin(t * 0.7) * 0.08 : Math.sin(rig.phase * 2) * 0.04)\n rig.neck.rotation.y = clip === 'idle' ? Math.sin(t * 0.45) * 0.35 : 0\n }\n if (rig.head) rig.head.rotation.z = rig.infected ? Math.sin(t * 11) * Math.sin(t * 1.7) * 0.12 : 0\n if (rig.jaw) rig.jaw.rotation.x = rig.infected ? 0.25 + Math.max(0, Math.sin(t * 6)) * 0.25 : clip === 'run' ? 0.12 : 0\n const wagging = species.id === 'dog' && !rig.infected\n rig.tail.forEach((segment, index) => {\n segment.rotation.z = Math.sin(t * (wagging ? 9 : 2.2) - index * 0.6) * (wagging ? 0.35 : 0.12)\n })\n}\n","import type * as THREE_NS from 'three'\n\nimport type { AnimateParams } from './animate-types'\nimport type { AnimalRig, Three } from './types'\n\nconst { PI } = Math\n\n/** The path of the body for a clip: crawling in a wave, or coiled with the head up in an S (and striking when `strike` rises). */\nconst bodyPath = (THREE: Three, rig: AnimalRig, params: AnimateParams, spacing: number): THREE_NS.Vector3[] => {\n const { time: t, clip } = params\n const points: THREE_NS.Vector3[] = []\n for (let index = 0; index < rig.segmentCount; index++) {\n let x: number\n let z: number\n let y = 0\n if (clip === 'walk') {\n z = 0.6 - index * spacing\n x = Math.sin(rig.phase * 2.2 - index * 0.42) * 0.08 * Math.min(1, index / 6 + 0.3)\n } else {\n const strike = clip === 'run' ? Math.max(0, Math.sin(t * 2.2)) ** 6 : 0\n if (index < 9) {\n const k = index / 9\n z = 0.08 + (1 - k) * 0.12 + strike * (1 - k) * 0.35\n y = (1 - k) * 0.13 * (1 - strike * 0.6)\n x = Math.sin(k * PI * 1.6) * 0.05 * (1 - strike)\n } else {\n const angle = (index - 9) * 0.36\n const radius = 0.16 - (index - 9) * 0.0045\n x = Math.cos(angle) * radius\n z = Math.sin(angle) * radius - 0.05\n y = (index - 9) * 0.0012\n }\n }\n points.push(new THREE.Vector3(x, y, z))\n }\n return points\n}\n\n/** Advances a snake (page: `animSnake`): lays the 34 segments along the path of the clip, then the head, jaw and tongue. */\nexport const animateSnake = (THREE: Three, rig: AnimalRig, params: AnimateParams): void => {\n const { time: t, delta: dt, clip } = params\n const spacing = rig.segmentLength * (params.split ? 1.5 : 1)\n if (clip === 'walk') rig.phase += dt * 2.4\n const points = bodyPath(THREE, rig, params, spacing)\n const at = (index: number): THREE_NS.Vector3 => points[index] as THREE_NS.Vector3\n for (let index = 0; index < rig.segmentCount; index++) {\n const group = (rig.segments[index] as AnimalRig['segments'][number]).group\n group.position.copy(at(index))\n const direction = index === 0 ? at(0).clone().sub(at(1)) : at(index - 1).clone().sub(at(index))\n group.rotation.set(0, Math.atan2(direction.x, direction.z), 0)\n group.rotateX(-Math.atan2(direction.y, Math.hypot(direction.x, direction.z)))\n }\n const head = rig.head as THREE_NS.Group\n const headDirection = at(0).clone().sub(at(1)).normalize()\n head.position.copy(at(0)).addScaledVector(headDirection, 0.02)\n head.rotation.set(0, Math.atan2(headDirection.x, headDirection.z), 0)\n head.rotateX(-Math.atan2(headDirection.y, Math.hypot(headDirection.x, headDirection.z)) * 0.5)\n const jaw = rig.jaw as THREE_NS.Group\n jaw.rotation.x = clip === 'run' ? Math.max(0, Math.sin(t * 2.2)) ** 6 * 0.9 : 0\n const tongue = rig.tongue as THREE_NS.Mesh\n tongue.visible = Math.sin(t * 5) > 0.3\n tongue.scale.z = 1 + Math.sin(t * 20) * 0.3\n}\n","import { birdSize } from '@adatechnology/game-animals'\nimport type { BirdSpec } from '@adatechnology/game-animals'\nimport type * as THREE_NS from 'three'\n\nimport { addMesh, createRigTools } from './context'\nimport type { BirdExtra } from './extras'\nimport type { AnimalRig, Three } from './types'\n\nconst buildWings = (THREE: Three, rig: AnimalRig, spec: BirdSpec, parts: { bodyGroup: THREE_NS.Group; main: THREE_NS.MeshStandardMaterial; dark: THREE_NS.MeshStandardMaterial }): void => {\n const tools = createRigTools(THREE, rig)\n const { bodyGroup, main, dark } = parts\n for (const side of [-1, 1]) {\n const shoulder = tools.pivot(bodyGroup, side * spec.r * spec.s[0] * 1.02, spec.r * 0.22, spec.r * 0.3, new THREE.Vector3(side * 0.12, 0.03, 0).multiplyScalar(rig.size))\n tools.joint(shoulder, spec.r * 0.14, main, 0, 0, 0)\n tools.ball(shoulder, 0.5, spec.wingM ? tools.material(spec.wingM) : dark, side * spec.wi * 0.5, 0, -spec.wc * 0.3, spec.wi * 1.05, spec.r * 0.16, spec.wc)\n const elbow = tools.pivot(shoulder, side * spec.wi, 0, 0, new THREE.Vector3(side * 0.06, 0, 0).multiplyScalar(rig.size))\n tools.joint(elbow, spec.r * 0.1, main, 0, 0, 0)\n tools.ball(elbow, 0.5, dark, side * spec.wo * 0.45, 0, -spec.wc * 0.32, spec.wo * 1.05, spec.r * 0.12, spec.wc * 0.75)\n if (spec.fingers) {\n for (let finger = 0; finger < 4; finger++) {\n addMesh(THREE, elbow, new THREE.BoxGeometry(spec.wo * 0.35, spec.r * 0.03, spec.wc * 0.12), dark, side * (spec.wo + spec.wo * 0.12), 0, -spec.wc * 0.05 - finger * spec.wc * 0.18, 0, side * (0.05 + finger * 0.12), 0)\n }\n }\n if (spec.spur) addMesh(THREE, elbow, new THREE.ConeGeometry(spec.r * 0.06, spec.r * 0.25, 5), tools.material(0xc0302a), 0, 0, spec.wc * 0.2, Math.PI / 2, 0, 0)\n rig.wings.push({ shoulder, elbow, side })\n }\n}\n\nconst buildLegs = (THREE: Three, rig: AnimalRig, spec: BirdSpec, parts: { bodyGroup: THREE_NS.Group; main: THREE_NS.MeshStandardMaterial; leg: THREE_NS.MeshStandardMaterial }): void => {\n const tools = createRigTools(THREE, rig)\n const { bodyGroup, main, leg } = parts\n for (const side of [-1, 1]) {\n const hip = tools.pivot(bodyGroup, side * spec.r * 0.38, -spec.r * 0.55, spec.r * 0.05, new THREE.Vector3(side * 0.06, -0.05, 0).multiplyScalar(rig.size))\n tools.joint(hip, spec.r * 0.18, main, 0, 0, 0)\n tools.limb(hip, spec.r * 0.16, spec.r * 0.09, spec.shank, spec.featherLeg ? main : leg)\n const ankle = tools.pivot(hip, 0, -spec.shank, 0, new THREE.Vector3(0, -0.04, 0).multiplyScalar(rig.size))\n tools.joint(ankle, spec.r * 0.06, leg, 0, 0, 0)\n tools.limb(ankle, spec.r * 0.055, spec.r * 0.05, spec.tars, leg)\n const foot = tools.pivot(ankle, 0, -spec.tars, 0)\n for (const angle of [-0.45, 0, 0.45]) addMesh(THREE, foot, new THREE.BoxGeometry(spec.r * 0.05, spec.r * 0.04, spec.r * 0.55).translate(0, 0, spec.r * 0.25), leg, 0, -spec.r * 0.02, 0, 0, angle, 0)\n addMesh(THREE, foot, new THREE.BoxGeometry(spec.r * 0.05, spec.r * 0.04, spec.r * 0.25).translate(0, 0, -spec.r * 0.12), leg, 0, -spec.r * 0.02, 0)\n rig.legs.push({ hip, knee: ankle, foot, isFront: false, angle1: 0.35, angle2: -0.5, side })\n }\n}\n\n/** Builds a bird: body, tail, two wings with shoulder and elbow pivots, neck, head with beak and jaw, eyes, the species' extra parts, and two legs. */\nexport const buildBird = (THREE: Three, rig: AnimalRig, options: { extra?: BirdExtra }): AnimalRig => {\n const spec = rig.species.body as BirdSpec\n const tools = createRigTools(THREE, rig)\n const colors = spec.c\n const main = tools.material(colors.main)\n const dark = tools.material(colors.dark)\n const leg = tools.material(colors.leg)\n const beak = tools.material(colors.beak, { roughness: 0.5 })\n const belly = tools.material(colors.belly || colors.main)\n rig.main = main\n const hipY = spec.tars + spec.shank * 0.9\n rig.size = birdSize(spec)\n const bodyGroup = tools.pivot(rig.trunk, 0, hipY + spec.r * 0.55, 0)\n rig.bodyGroup = bodyGroup\n bodyGroup.rotation.x = spec.pitch || 0\n tools.ball(bodyGroup, spec.r, main, 0, 0, 0, spec.s[0], spec.s[1], spec.s[2])\n tools.ball(bodyGroup, spec.r * 0.9, belly, 0, -spec.r * 0.22, spec.r * 0.15, spec.s[0] * 0.9, spec.s[1] * 0.8, spec.s[2] * 0.85)\n const tail = tools.pivot(bodyGroup, 0, spec.r * 0.15, -spec.r * spec.s[2] * 0.85, new THREE.Vector3(0, 0, -0.08).multiplyScalar(rig.size))\n tail.rotation.x = spec.tailA || -0.3\n rig.tailRoot = tail\n addMesh(THREE, tail, new THREE.BoxGeometry(spec.tailW, spec.r * 0.08, spec.tailL).translate(0, 0, -spec.tailL / 2), spec.tailM ? tools.material(spec.tailM) : dark)\n buildWings(THREE, rig, spec, { bodyGroup, main, dark })\n const neck = tools.pivot(bodyGroup, 0, spec.r * 0.45, spec.r * spec.s[2] * 0.75, new THREE.Vector3(0, 0.05, 0.08).multiplyScalar(rig.size))\n neck.rotation.x = spec.neckA\n rig.neck = neck\n rig.neckAngle = spec.neckA\n tools.joint(neck, spec.nr * 1.1, spec.neckM ? tools.material(spec.neckM) : main, 0, 0, 0)\n addMesh(THREE, neck, new THREE.CylinderGeometry(spec.nr * 0.85, spec.nr, spec.nl, 12).translate(0, spec.nl / 2, 0), spec.neckM ? tools.material(spec.neckM) : main)\n const head = tools.pivot(neck, 0, spec.nl, 0, new THREE.Vector3(0, 0.05, 0.05).multiplyScalar(rig.size))\n head.rotation.x = -spec.neckA + (spec.headA || 0)\n rig.head = head\n rig.headAngle = head.rotation.x\n const headMaterial = spec.headM ? tools.material(spec.headM) : main\n tools.ball(head, spec.hr, headMaterial, 0, 0, 0, spec.hs ? spec.hs[0] : 1, spec.hs ? spec.hs[1] : 1, spec.hs ? spec.hs[2] : 1.1)\n addMesh(THREE, head, new THREE.ConeGeometry(spec.br, spec.bl, 8).rotateX(Math.PI / 2).translate(0, 0, spec.bl / 2), beak, 0, -spec.hr * 0.1, spec.hr * 0.85, spec.hook || 0, 0, 0)\n const jaw = tools.pivot(head, 0, -spec.hr * 0.25, spec.hr * 0.8)\n rig.jaw = jaw\n addMesh(THREE, jaw, new THREE.ConeGeometry(spec.br * 0.7, spec.bl * 0.85, 8).rotateX(Math.PI / 2).translate(0, 0, spec.bl * 0.42), beak).scale.set(1, 0.5, 1)\n tools.eyes(head, spec.hr * 0.17, spec.hr * 0.68, spec.hr * 0.2, spec.hr * 0.35)\n if (spec.eyeC && rig.eyeMaterial) {\n rig.eyeMaterial.color.set(spec.eyeC)\n rig.eyeMaterial.userData.base = rig.eyeMaterial.color.clone()\n }\n options.extra?.({ THREE, rig, tools }, { bodyGroup, head, neck, main, dark })\n buildLegs(THREE, rig, spec, { bodyGroup, main, leg })\n return rig\n}\n","import { quadrupedLayout } from '@adatechnology/game-animals'\nimport type { QuadrupedSpec } from '@adatechnology/game-animals'\nimport type * as THREE_NS from 'three'\n\nimport { addMesh, createRigTools } from './context'\nimport { createCoatTexture } from './textures'\nimport { profileGeometry } from './profile'\nimport type { AnimalRig, DocumentLike, Three } from './types'\nimport type { QuadrupedExtra } from './extras'\n\nconst RIB_COUNT = 4\n\ntype LegDefinition = readonly [number, number, 'F' | 'H']\n\nconst LEG_DEFINITIONS: readonly LegDefinition[] = [\n [1, 1, 'F'],\n [-1, 1, 'F'],\n [1, -1, 'H'],\n [-1, -1, 'H'],\n]\n\n/** The body and the four legs of a quadruped (page: the first half of `quad`). */\nconst buildBodyAndLegs = (THREE: Three, rig: AnimalRig, spec: QuadrupedSpec, main: THREE_NS.MeshStandardMaterial, belly: THREE_NS.MeshStandardMaterial, dark: THREE_NS.MeshStandardMaterial) => {\n const tools = createRigTools(THREE, rig)\n const { L, R: radius } = spec\n const leg = spec.leg\n const layout = quadrupedLayout(spec)\n const { frontHipHeight, hindHipHeight, footHeight, bodyHeight, span } = layout\n const frontAngle1 = leg.fa1 ?? 0.06\n const frontAngle2 = leg.fa2 ?? -0.08\n const hindAngle1 = leg.ha1 ?? -0.32\n const hindAngle2 = leg.ha2 ?? 0.58\n rig.size = layout.size\n const bodyGroup = tools.pivot(rig.trunk, 0, bodyHeight, 0)\n rig.bodyGroup = bodyGroup\n const body = addMesh(THREE, bodyGroup, profileGeometry(THREE, L, spec.prof.map((value) => value * radius)), main)\n body.scale.set(spec.sx || 1, spec.sy || 1, 1)\n body.rotation.x = Math.atan2(hindHipHeight - frontHipHeight, span) * 0.6\n rig.body = body\n const belt = addMesh(THREE, bodyGroup, profileGeometry(THREE, L * 0.8, spec.prof.map((value) => value * 0.8 * radius)), belly)\n belt.scale.set((spec.sx || 1) * 0.92, (spec.sy || 1) * 0.85, 1)\n belt.position.y = -radius * 0.2\n LEG_DEFINITIONS.forEach(([sx, sz, kind]) => {\n const front = kind === 'F'\n const upper = front ? leg.fu : leg.hu\n const lower = front ? leg.fl : leg.hl\n const height = front ? frontHipHeight : hindHipHeight\n const hip = tools.pivot(rig.trunk, sx * radius * (spec.sx || 1) * 0.55, height, (sz * span) / 2, new THREE.Vector3(sx * 0.18, -0.06, sz * 0.12).multiplyScalar(rig.size))\n tools.ball(hip, leg.r * (front ? 1.9 : 2.3), main, 0, -upper * 0.18, front ? 0 : -leg.r * 0.3, 0.75, 1.25, 1)\n tools.joint(hip, leg.r * 1.15, main, 0, 0, 0)\n tools.limb(hip, leg.r * 1.25, leg.r * 0.95, upper, main)\n const knee = tools.pivot(hip, 0, -upper, 0, new THREE.Vector3(0, -0.07 * rig.size, 0))\n tools.joint(knee, leg.r * 0.95, main, 0, 0, 0)\n tools.limb(knee, leg.r * 0.92, leg.r * 0.68, lower, main)\n const foot = tools.pivot(knee, 0, -lower, 0, new THREE.Vector3(0, -0.05 * rig.size, 0))\n if (leg.hoof) {\n tools.joint(foot, leg.r * 0.72, dark, 0, 0, 0)\n addMesh(THREE, foot, new THREE.CylinderGeometry(leg.r * 0.8, leg.r * 1.05, footHeight, 12).translate(0, -footHeight / 2, 0), dark)\n } else {\n tools.joint(foot, leg.r * 0.66, main, 0, 0, 0)\n tools.ball(foot, leg.r * 0.95, leg.dark ? dark : main, 0, -footHeight * 0.55, leg.r * 0.5, 1, 0.62, 1.45)\n }\n rig.legs.push({ hip, knee, foot, isFront: front, angle1: front ? frontAngle1 : hindAngle1, angle2: front ? frontAngle2 : hindAngle2, side: sx })\n })\n return { bodyGroup, bodyHeight }\n}\n\nconst buildHead = (THREE: Three, rig: AnimalRig, spec: QuadrupedSpec, parts: { bodyGroup: THREE_NS.Group; main: THREE_NS.MeshStandardMaterial; belly: THREE_NS.MeshStandardMaterial; nose: THREE_NS.MeshStandardMaterial }) => {\n const tools = createRigTools(THREE, rig)\n const { bodyGroup, main, belly, nose } = parts\n const { L, R: radius } = spec\n const neckSpec = spec.neck\n const headSpec = spec.head\n const neck = tools.pivot(bodyGroup, 0, radius * (spec.sy || 1) * (neckSpec.y ?? 0.35), (L / 2) * (neckSpec.z ?? 0.78), new THREE.Vector3(0, 0.05, 0.12).multiplyScalar(rig.size))\n neck.rotation.x = neckSpec.ang\n rig.neck = neck\n rig.neckAngle = neckSpec.ang\n tools.joint(neck, neckSpec.r * 1.05, main, 0, 0, 0)\n addMesh(THREE, neck, new THREE.CylinderGeometry(neckSpec.r2 || neckSpec.r * 0.85, neckSpec.r, neckSpec.len, 14).translate(0, neckSpec.len / 2, 0), main)\n const head = tools.pivot(neck, 0, neckSpec.len, 0, new THREE.Vector3(0, 0.06, 0.08).multiplyScalar(rig.size))\n head.rotation.x = -neckSpec.ang + (headSpec.down || 0)\n rig.head = head\n rig.headAngle = head.rotation.x\n tools.joint(head, neckSpec.r2 || neckSpec.r * 0.85, main, 0, 0, 0)\n tools.ball(head, headSpec.r, main, 0, headSpec.r * 0.25, headSpec.r * 0.25, headSpec.s[0], headSpec.s[1], headSpec.s[2])\n const snoutZ = headSpec.r * 0.25 + headSpec.r * headSpec.s[2] * 0.55\n const snout = addMesh(THREE, head, new THREE.CylinderGeometry(headSpec.sr2, headSpec.sr1, headSpec.sl, 14).rotateX(Math.PI / 2), headSpec.muzzle ? tools.material(headSpec.muzzle) : main, 0, headSpec.r * (headSpec.sy ?? 0), snoutZ + headSpec.sl / 2)\n snout.scale.set(1, headSpec.sq || 1, 1)\n tools.ball(head, headSpec.sr2 * 1.02, nose, 0, headSpec.r * (headSpec.sy ?? 0) + headSpec.sr2 * 0.1, snoutZ + headSpec.sl, 1, 0.85 * (headSpec.sq || 1), 0.45)\n const jaw = tools.pivot(head, 0, headSpec.r * (headSpec.sy ?? 0) - headSpec.sr1 * 0.55 * (headSpec.sq || 1), snoutZ - headSpec.sr1 * 0.2, new THREE.Vector3(0, -0.04, 0.04).multiplyScalar(rig.size))\n rig.jaw = jaw\n addMesh(THREE, jaw, new THREE.CylinderGeometry(headSpec.sr2 * 0.7, headSpec.sr1 * 0.75, headSpec.sl * 0.92, 12).rotateX(Math.PI / 2).translate(0, 0, headSpec.sl * 0.46), belly).scale.set(1, 0.45, 1)\n const blood = tools.ball(jaw, headSpec.sr2 * 0.9, rig.woundMaterial || (rig.woundMaterial = new THREE.MeshStandardMaterial({ color: 0x6a100c, roughness: 0.3 })), 0, 0.0, headSpec.sl * 0.9, 1.1, 0.6, 0.6)\n tools.eyes(head, headSpec.r * 0.13, headSpec.r * headSpec.s[0] * 0.52, headSpec.r * 0.5, headSpec.r * 0.25 + headSpec.r * headSpec.s[2] * 0.55)\n const ears = spec.ears\n for (const side of [-1, 1]) {\n const earGroup = tools.pivot(head, side * headSpec.r * headSpec.s[0] * 0.55, headSpec.r * (0.25 + headSpec.s[1] * 0.75), headSpec.r * 0.1, new THREE.Vector3(side * 0.04, 0.04, 0).multiplyScalar(rig.size))\n rig.ears.push(earGroup)\n if (ears.t === 'cone') addMesh(THREE, earGroup, new THREE.ConeGeometry(ears.s * 0.45, ears.s, 8).translate(0, ears.s / 2, 0), main, 0, 0, 0, ears.fwd || 0, 0, side * -(ears.tilt ?? 0.3))\n else if (ears.t === 'round') tools.ball(earGroup, ears.s, main, 0, ears.s * 0.4, 0, 1, 1, 0.35)\n else if (ears.t === 'flop') {\n const ear = tools.ball(earGroup, ears.s, main, side * ears.s * 0.9, -ears.s * 0.1, 0, 1.5, 0.45, 0.7)\n ear.rotation.z = side * -0.25\n }\n }\n return { neck, head, blood, neckSpec, headSpec }\n}\n\nconst buildTail = (THREE: Three, rig: AnimalRig, spec: QuadrupedSpec, parts: { bodyGroup: THREE_NS.Group; main: THREE_NS.MeshStandardMaterial; dark: THREE_NS.MeshStandardMaterial }): void => {\n const tools = createRigTools(THREE, rig)\n const tailSpec = spec.tail\n if (!tailSpec || !tailSpec.n) return\n const { bodyGroup, main, dark } = parts\n const { L, R: radius } = spec\n let parent = tools.pivot(bodyGroup, 0, radius * (spec.sy || 1) * (tailSpec.y ?? 0.3), (-L / 2) * 0.95, new THREE.Vector3(0, 0.02, -0.12).multiplyScalar(rig.size))\n parent.rotation.x = tailSpec.ang\n rig.tailRoot = parent\n const segmentLength = tailSpec.len / tailSpec.n\n for (let index = 0; index < tailSpec.n; index++) {\n const radius1 = tailSpec.r * (1 - (index / tailSpec.n) * 0.6)\n const radius2 = tailSpec.r * (1 - ((index + 1) / tailSpec.n) * 0.6)\n tools.limb(parent, radius1, radius2, segmentLength, tailSpec.dark && index === tailSpec.n - 1 ? dark : main)\n tools.joint(parent, radius1, main, 0, 0, 0)\n rig.tail.push(parent)\n if (index < tailSpec.n - 1) {\n const next = tools.pivot(parent, 0, -segmentLength, 0)\n next.rotation.x = tailSpec.curl || 0\n parent = next\n } else if (tailSpec.tuft) tools.ball(parent, tailSpec.r * 2.2, dark, 0, -segmentLength - tailSpec.r, 0, 1, 1.8, 1)\n }\n if (tailSpec.bushy) tools.ball(rig.tail[0] as THREE_NS.Group, tailSpec.r * 2.2, dark, 0, -tailSpec.len * 0.55, 0, 1, (tailSpec.len * 2.2) / tailSpec.r / 6, 1)\n}\n\n/** Wounds, ribs and the blood of the jaw: shown only while the animal is infected (page: the end of `quad`). */\nconst buildInfectionParts = (THREE: Three, rig: AnimalRig, spec: QuadrupedSpec, parts: { bodyGroup: THREE_NS.Group; neck: THREE_NS.Group; blood: THREE_NS.Mesh; neckSpec: QuadrupedSpec['neck'] }): void => {\n const tools = createRigTools(THREE, rig)\n const { bodyGroup, neck, blood, neckSpec } = parts\n const { L, R: radius } = spec\n const sideX = radius * (spec.sx || 1)\n const sideY = radius * (spec.sy || 1)\n const list: THREE_NS.Object3D[] = [blood]\n const wound = (parent: THREE_NS.Object3D, size: number, x: number, y: number, z: number, sx: number, sy: number, sz: number): THREE_NS.Mesh =>\n tools.ball(parent, size, rig.woundMaterial || (rig.woundMaterial = new THREE.MeshStandardMaterial({ color: 0x5a120e, roughness: 0.45 })), x, y, z, sx || 1, sy || 1, sz || 1)\n list.push(wound(bodyGroup, radius * 0.28, sideX * 0.92, sideY * 0.15, L * 0.05, 0.35, 1, 1.3))\n list.push(wound(bodyGroup, radius * 0.2, -sideX * 0.9, sideY * 0.3, -L * 0.2, 0.35, 1, 1.1))\n list.push(wound(neck, neckSpec.r * 0.5, neckSpec.r * 0.85, neckSpec.len * 0.5, 0, 0.4, 1, 1))\n const bone = tools.material(0xd9cfb8, { roughness: 0.6 })\n for (let index = 0; index < RIB_COUNT; index++) {\n const rib = addMesh(THREE, bodyGroup, new THREE.TorusGeometry(radius * 0.95, radius * 0.035, 6, 16, Math.PI * 0.42), bone, sideX * 0.12, sideY * 0.05, L * 0.12 - index * L * 0.07, 0, Math.PI / 2, -0.3)\n rib.scale.set(1, 1.02 * (spec.sy || 1), spec.sx || 1)\n list.push(rib)\n }\n rig.infectionParts.push(...list)\n list.forEach((part) => (part.visible = false))\n}\n\n/** Builds a quadruped: body, four legs on hip-knee-foot pivots, neck, head with jaw and eyes, ears, tail, the species' extra parts, and the infection parts. */\nexport const buildQuadruped = (THREE: Three, rig: AnimalRig, options: { document: DocumentLike; extra?: QuadrupedExtra }): AnimalRig => {\n const spec = rig.species.body as QuadrupedSpec\n const tools = createRigTools(THREE, rig)\n const colors = spec.c\n const texture = spec.texture ? createCoatTexture(THREE, options.document, spec.texture) : undefined\n const main = tools.material(texture ? 0xffffff : colors.main, texture ? { map: texture } : undefined)\n const belly = tools.material(colors.belly)\n const dark = tools.material(colors.dark)\n const nose = tools.material(colors.nose || colors.dark, { roughness: 0.4 })\n rig.main = main\n rig.dark = dark\n const { bodyGroup, bodyHeight } = buildBodyAndLegs(THREE, rig, spec, main, belly, dark)\n const { neck, head, blood, neckSpec, headSpec } = buildHead(THREE, rig, spec, { bodyGroup, main, belly, nose })\n buildTail(THREE, rig, spec, { bodyGroup, main, dark })\n options.extra?.({ THREE, rig, tools }, { bodyGroup, head, neck, main, belly, dark, L: spec.L, Rb: spec.R, bodyY: bodyHeight, hd: headSpec, nk: neckSpec })\n if (rig.species.outcome === 'turns') buildInfectionParts(THREE, rig, spec, { bodyGroup, neck, blood, neckSpec })\n else blood.visible = false\n return rig\n}\n","import { addMesh, createRigTools } from './context'\nimport type { AnimalRig, Three } from './types'\n\nconst SEGMENT_COUNT = 34\nconst SEGMENT_LENGTH = 0.042\n\n/** Builds a snake: 34 spheres on their own groups along the trunk (the animation lays them on a path), a head with a jaw and a tongue. */\nexport const buildSnake = (THREE: Three, rig: AnimalRig): AnimalRig => {\n const tools = createRigTools(THREE, rig)\n rig.size = 0.5\n rig.segmentLength = SEGMENT_LENGTH\n rig.segmentCount = SEGMENT_COUNT\n const primary = tools.material(0x7a6a4a)\n const secondary = tools.material(0x3a2e22)\n const belly = tools.material(0xb8a888)\n rig.main = primary\n for (let index = 0; index < SEGMENT_COUNT; index++) {\n const t = index / (SEGMENT_COUNT - 1)\n const radius = 0.012 + 0.03 * Math.sin(Math.min(1, t * 1.15) * Math.PI) * (t < 0.1 ? 0.75 + t * 2.5 : 1) + (t < 0.05 ? 0.012 : 0)\n const material = index % 4 === 1 || index % 4 === 2 ? secondary : primary\n const group = new THREE.Group()\n rig.trunk.add(group)\n const sphere = tools.ball(group, radius, material, 0, radius, 0, 1, 0.78, 1.5)\n sphere.userData.joint = index % 3 === 0 ? 1 : 0\n if (index % 3 === 0) rig.joints.push(sphere)\n rig.segments.push({ group, radius })\n }\n const head = new THREE.Group()\n rig.trunk.add(head)\n rig.head = head\n tools.ball(head, 0.032, primary, 0, 0.03, 0.025, 1.25, 0.6, 1.45)\n tools.ball(head, 0.024, secondary, 0, 0.04, 0.0, 1.3, 0.4, 0.9)\n tools.ball(head, 0.02, primary, 0, 0.028, 0.06, 0.85, 0.55, 1.1)\n const jaw = new THREE.Group()\n jaw.position.set(0, 0.018, 0.0)\n head.add(jaw)\n rig.jaw = jaw\n tools.ball(jaw, 0.026, belly, 0, 0, 0.035, 1.1, 0.35, 1.5)\n tools.eyes(head, 0.006, 0.03, 0.04, 0.045)\n rig.tongue = addMesh(THREE, head, new THREE.BoxGeometry(0.004, 0.002, 0.05), tools.material(0xb02020), 0, 0.024, 0.1)\n return rig\n}\n","import { GAIT_PHASES } from '@adatechnology/game-animals'\nimport { markKitOrigin } from '@adatechnology/game-contracts'\nimport type { AnimalSpecies, AnimalVisualState, GaitPhases } from '@adatechnology/game-animals'\nimport { collectReferencedNodes, createNodeSkin } from '@adatechnology/game-props3d'\nimport { shapeGeometries } from '@adatechnology/game-shapes-three'\nimport type { NodeSkin } from '@adatechnology/game-props3d'\nimport type * as THREE_NS from 'three'\nimport { animateBird } from './animate-bird'\nimport { animateQuadruped } from './animate-quadruped'\nimport { animateSnake } from './animate-snake'\nimport type { AnimateParams } from './animate-types'\nimport { buildBird } from './bird'\nimport { createRig, createStream } from './context'\nimport { BIRD_EXTRAS, QUADRUPED_EXTRAS } from './extras'\nimport { buildQuadruped } from './quadruped'\nimport { buildSnake } from './snake'\nimport type { AnimalRig, AnimalsThreeOptions, DocumentLike, Three } from './types'\nimport { applyVisualState } from './visual-state'\nimport { setJointView, setSplit, updateSplit } from './views'\n\nexport type AnimalsThree = {\n /** Builds one animal from its species; consumes one number of the stream for the start phase unless `phase` is given. */\n build(species: AnimalSpecies, buildOptions?: { readonly phase?: number }): AnimalRig\n /** Advances the animation of one animal (steps, flight, wings, head, tail, a dead one lying down). */\n animate(rig: AnimalRig, params: AnimateParams): void\n setVisualState(rig: AnimalRig, state: AnimalVisualState): void\n setJointView(rig: AnimalRig, on: boolean): void\n setSplit(rig: AnimalRig, on: boolean): void\n updateSplit(rig: AnimalRig, delta: number): void\n /** One mesh that stands for the whole tree, for crowds: call `skin.update()` each frame after `animate`. */\n createSkin(rig: AnimalRig): NodeSkin\n readonly jointMaterial: THREE_NS.MeshStandardMaterial\n}\n\nconst countTriangles = (root: THREE_NS.Object3D): number => {\n let triangles = 0\n root.traverse((object) => {\n const mesh = object as THREE_NS.Mesh\n if (!mesh.isMesh) return\n const geometry = mesh.geometry\n triangles += geometry.index ? geometry.index.count / 3 : geometry.attributes.position.count / 3\n })\n return Math.round(triangles)\n}\n\n/**\n * The animals drawn with the host's three.js. The geometry (boxes, cylinders, cones, spheres, circles, tori, the lathe of the bodies) comes from\n * game-shapes through `shapeGeometries(host)`: the same bytes and classes as three's, from a template cache; everything else is the host's.\n */\nexport const createAnimalsThree = (host: Three, options: AnimalsThreeOptions = {}): AnimalsThree => {\n const THREE = shapeGeometries(host)\n const random = options.random ?? createStream(options.seed ?? 7)\n const documentOf = (): DocumentLike => {\n const found = options.document ?? (globalThis as { document?: DocumentLike }).document\n if (!found) throw new Error('game-animals-three: a species with a coat texture needs `options.document` (no global document found)')\n return found\n }\n const jointMaterial = new THREE.MeshStandardMaterial({ color: 0xe0a548, emissive: 0xe0a548, emissiveIntensity: 0.8, roughness: 0.4 })\n const gaits: Readonly<Record<'walk' | 'trot' | 'gallop', GaitPhases>> = GAIT_PHASES\n\n return {\n jointMaterial,\n build(species, buildOptions = {}) {\n const rig = createRig(THREE, species, { phase: buildOptions.phase ?? random() * 6 })\n if (species.body.kind === 'snake') buildSnake(THREE, rig)\n else if (species.body.kind === 'bird') buildBird(THREE, rig, { extra: BIRD_EXTRAS[species.id] })\n else buildQuadruped(THREE, rig, { document: species.body.texture ? documentOf() : (undefined as never), extra: QUADRUPED_EXTRAS[species.id] })\n rig.triangles = countTriangles(rig.root)\n markKitOrigin(rig.root, { owner: 'game-animals-three', kind: 'animal', id: species.id })\n rig.root.traverse((object) => {\n if ((object as THREE_NS.Mesh).isMesh) object.userData.speciesId = species.id\n })\n return rig\n },\n animate(rig, params) {\n const kind = rig.species.body.kind\n if (kind === 'snake') animateSnake(THREE, rig, params)\n else if (kind === 'bird') animateBird(rig, params)\n else animateQuadruped(rig, params, gaits)\n },\n setVisualState: (rig, state) => applyVisualState(THREE, rig, state),\n setJointView: (rig, on) => setJointView(rig, on, jointMaterial),\n setSplit,\n updateSplit,\n createSkin: (rig) => createNodeSkin(THREE, rig.root, { nodes: collectReferencedNodes(rig, ['species']) }),\n }\n}\n","import type { Group, Object3D, PointLight, SpotLight } from 'three'\n\nimport type { LightKind, LightPatch, LinearRgb, ThreeForLights } from './types'\n\nconst PARKED_Y = -50\nconst DEFAULT_DECAY = 2\nconst DEFAULT_ANGLE = Math.PI / 3\nconst POSITION_EPSILON = 1e-6\n\nexport type Vec3 = [number, number, number]\n\nexport type LightValues = {\n color: number | LinearRgb\n intensity: number\n distance: number\n decay: number\n position: Vec3\n target: Vec3 | undefined\n angle: number\n penumbra: number\n}\n\n/** Whoever owns a slot: a request, a source or a lease. */\nexport interface Holder {\n readonly kind: 'request' | 'source' | 'lease'\n readonly owner: string\n readonly lightKind: LightKind\n priority: number\n urgent: boolean\n values: LightValues\n slot: Slot | null\n alive: boolean\n onLost: (() => void) | undefined\n /** Sources only. */\n follow: Object3D | undefined\n shadow: boolean\n wanted: boolean\n distance: number\n /** Sources only: `values` with the world position and direction of `follow` of this frame. */\n resolved: LightValues\n}\n\nexport interface Slot {\n readonly index: number\n readonly kind: LightKind\n readonly shadow: boolean\n readonly light: PointLight | SpotLight\n holder: Holder | null\n pending: Holder | null\n leased: boolean\n /** 0..1 multiplier on the holder's intensity. */\n fade: number\n fadeTarget: number\n fadeOutMs: number\n baseIntensity: number\n lastPosition: Vec3\n}\n\nexport function createValues(base: Partial<LightValues> & Pick<LightValues, 'color' | 'intensity' | 'distance' | 'position'>): LightValues {\n return {\n decay: DEFAULT_DECAY,\n target: undefined,\n angle: DEFAULT_ANGLE,\n penumbra: 0,\n ...base,\n position: [...base.position],\n }\n}\n\nexport function mergeValues(values: LightValues, patch: LightPatch): void {\n if (patch.color !== undefined) values.color = patch.color\n if (patch.intensity !== undefined) values.intensity = patch.intensity\n if (patch.distance !== undefined) values.distance = patch.distance\n if (patch.decay !== undefined) values.decay = patch.decay\n if (patch.position !== undefined) values.position = [...patch.position]\n if (patch.target !== undefined) values.target = [...patch.target]\n if (patch.angle !== undefined) values.angle = patch.angle\n if (patch.penumbra !== undefined) values.penumbra = patch.penumbra\n}\n\nexport type ShadowSettings = { readonly bias?: number; readonly normalBias?: number; readonly near?: number }\n\nexport function createSlots(THREE: ThreeForLights, group: Group, kind: LightKind, count: number, shadows: number, mapSize: number, settings: ShadowSettings = {}): Slot[] {\n const slots: Slot[] = []\n for (let index = 0; index < count; index++) {\n const light = kind === 'point' ? new THREE.PointLight(0xffffff, 0, 0, DEFAULT_DECAY) : new THREE.SpotLight(0xffffff, 0, 0, DEFAULT_ANGLE, 0, DEFAULT_DECAY)\n light.position.set(0, PARKED_Y, 0)\n light.layers.enableAll()\n const shadow = index < shadows\n if (shadow) {\n light.castShadow = true\n light.shadow.autoUpdate = false\n light.shadow.mapSize.set(mapSize, mapSize)\n if (settings.bias !== undefined) light.shadow.bias = settings.bias\n if (settings.normalBias !== undefined) light.shadow.normalBias = settings.normalBias\n if (settings.near !== undefined) light.shadow.camera.near = settings.near\n }\n group.add(light)\n if ('target' in light) group.add(light.target)\n slots.push({ index, kind, shadow, light, holder: null, pending: null, leased: false, fade: 0, fadeTarget: 0, fadeOutMs: 0, baseIntensity: 0, lastPosition: [0, PARKED_Y, 0] })\n }\n return slots\n}\n\nexport function isFree(slot: Slot): boolean {\n return !slot.leased && slot.holder === null && slot.pending === null\n}\n\nexport function applyIntensity(slot: Slot): void {\n slot.light.intensity = slot.baseIntensity * slot.fade\n}\n\nexport function markShadow(slot: Slot): void {\n if (slot.shadow) slot.light.shadow.needsUpdate = true\n}\n\nexport function writeLight(THREE: ThreeForLights, slot: Slot, values: LightValues): void {\n const { light } = slot\n if (typeof values.color === 'number') light.color.set(values.color)\n else light.color.setRGB(values.color[0], values.color[1], values.color[2], THREE.LinearSRGBColorSpace)\n light.distance = values.distance\n light.decay = values.decay\n const [x, y, z] = values.position\n light.position.set(x, y, z)\n if ('angle' in light) {\n light.angle = values.angle\n light.penumbra = values.penumbra\n const [tx, ty, tz] = values.target ?? [x, y - 1, z]\n light.target.position.set(tx, ty, tz)\n }\n const last = slot.lastPosition\n if (slot.shadow && (Math.abs(last[0] - x) > POSITION_EPSILON || Math.abs(last[1] - y) > POSITION_EPSILON || Math.abs(last[2] - z) > POSITION_EPSILON)) markShadow(slot)\n slot.lastPosition = [x, y, z]\n slot.baseIntensity = values.intensity\n applyIntensity(slot)\n}\n\n/** The holder's writes apply from now on; its light fades in (requests that cut a victim start at full). */\nexport function install(THREE: ThreeForLights, slot: Slot, holder: Holder, startAtFull: boolean): void {\n slot.holder = holder\n slot.pending = null\n slot.fade = startAtFull ? 1 : 0\n slot.fadeTarget = 1\n holder.slot = slot\n markShadow(slot)\n writeLight(THREE, slot, holder.values)\n}\n\n/** Drops the holder: the light keeps its last values and fades to 0 (at once with `fadeOutMs` 0). */\nexport function vacate(slot: Slot, fadeOutMs: number): void {\n slot.holder = null\n slot.fadeTarget = 0\n slot.fadeOutMs = fadeOutMs\n if (fadeOutMs <= 0) {\n slot.fade = 0\n applyIntensity(slot)\n }\n}\n\nexport function stepFade(slot: Slot, dtSeconds: number, fadeInMs: number): void {\n if (slot.fade < slot.fadeTarget) {\n slot.fade = fadeInMs <= 0 ? slot.fadeTarget : Math.min(slot.fadeTarget, slot.fade + (dtSeconds * 1000) / fadeInMs)\n } else if (slot.fade > slot.fadeTarget) {\n slot.fade = slot.fadeOutMs <= 0 ? slot.fadeTarget : Math.max(slot.fadeTarget, slot.fade - (dtSeconds * 1000) / slot.fadeOutMs)\n }\n applyIntensity(slot)\n}\n","import { type Holder, type Slot, install, isFree, stepFade, vacate } from './slots'\nimport type { LightKind, ThreeForLights } from './types'\n\nexport type ArbiterCore = {\n readonly THREE: ThreeForLights\n readonly slots: Record<LightKind, readonly Slot[]>\n readonly urgentReserve: number\n readonly fadeMs: number\n readonly hysteresis: number\n preempted: number\n refused: number\n}\n\n/** The slots requests and sources compete for: not leased, and shadow slots only on their own pass. */\nexport function poolOf(core: ArbiterCore, kind: LightKind, shadow: boolean): Slot[] {\n return core.slots[kind].filter((slot) => !slot.leased && slot.shadow === shadow)\n}\n\nfunction contribution(holder: Holder): number {\n return holder.values.intensity / Math.max(1, holder.distance) ** 2\n}\n\n/** Lowest priority first; on a tie, the smallest perceived contribution. */\nfunction isWeaker(a: Holder, b: Holder): boolean {\n if (a.priority !== b.priority) return a.priority < b.priority\n return contribution(a) < contribution(b)\n}\n\nfunction pickFree(slots: readonly Slot[]): Slot | undefined {\n let best: Slot | undefined\n for (const slot of slots) if (isFree(slot) && (best === undefined || slot.fade < best.fade)) best = slot\n return best\n}\n\n/** Gives a free slot to the holder: at once when its light is dark or the holder is urgent, else after the slot finishes fading out. */\nfunction place(core: ArbiterCore, slot: Slot, holder: Holder): void {\n holder.slot = slot\n if (slot.fade > 0 && !holder.urgent) {\n slot.pending = holder\n slot.fadeTarget = 0\n return\n }\n install(core.THREE, slot, holder, holder.kind === 'request')\n}\n\n/** Takes the slot from its holder; a request is told and dies, a source goes back to the queue. */\nfunction evict(core: ArbiterCore, slot: Slot, challenger: Holder, cut: boolean): void {\n const victim = slot.holder\n if (victim === null) return\n victim.slot = null\n if (victim.kind === 'request') {\n victim.alive = false\n victim.onLost?.()\n }\n core.preempted++\n vacate(slot, cut ? 0 : core.fadeMs)\n if (cut) {\n install(core.THREE, slot, challenger, true)\n return\n }\n slot.pending = challenger\n challenger.slot = slot\n}\n\nfunction weakestHolder(slots: readonly Slot[]): Slot | undefined {\n let weakest: Slot | undefined\n for (const slot of slots) {\n if (slot.holder === null || slot.pending !== null) continue\n if (weakest === undefined || isWeaker(slot.holder, weakest.holder as Holder)) weakest = slot\n }\n return weakest\n}\n\n/** Slots free to a holder that has no right to the urgent reserve. */\nfunction freeBeyondReserve(core: ArbiterCore, pool: readonly Slot[], reserve: number): Slot | undefined {\n const free = pool.filter(isFree)\n return free.length > reserve ? pickFree(pool) : undefined\n}\n\nexport function admitRequest(core: ArbiterCore, holder: Holder): boolean {\n const pool = poolOf(core, holder.lightKind, false)\n const reserve = holder.lightKind === 'point' ? core.urgentReserve : 0\n const free = holder.urgent ? pickFree(pool) : freeBeyondReserve(core, pool, reserve)\n if (free !== undefined) {\n place(core, free, holder)\n return true\n }\n const victim = weakestHolder(pool)\n if (victim === undefined || (victim.holder as Holder).priority >= holder.priority) {\n core.refused++\n return false\n }\n evict(core, victim, holder, holder.urgent)\n return true\n}\n\nfunction beats(challenger: Holder, incumbent: Holder, hysteresis: number): boolean {\n if (challenger.priority !== incumbent.priority) return challenger.priority > incumbent.priority\n return challenger.distance < incumbent.distance * (1 - hysteresis)\n}\n\nfunction bestFirst(a: Holder, b: Holder): number {\n return b.priority - a.priority || a.distance - b.distance\n}\n\n/** Hands the pool's slots to the wanted sources: best first, an incumbent only yields to a clearly better challenger. */\nexport function arbitrateSources(core: ArbiterCore, pool: readonly Slot[], sources: readonly Holder[], reserve: number): void {\n for (const slot of pool) {\n const holder = slot.holder\n if (slot.pending?.kind === 'source' && !slot.pending.wanted) {\n slot.pending.slot = null\n slot.pending = null\n }\n if (holder?.kind === 'source' && !holder.wanted) {\n holder.slot = null\n vacate(slot, core.fadeMs)\n }\n }\n const challengers = sources.filter((source) => source.wanted && source.slot === null).sort(bestFirst)\n for (const challenger of challengers) {\n const free = freeBeyondReserve(core, pool, reserve)\n if (free !== undefined) {\n place(core, free, challenger)\n continue\n }\n const incumbentSlots = pool.filter((slot) => slot.holder?.kind === 'source')\n let worst: Slot | undefined\n for (const slot of incumbentSlots) {\n if (slot.pending !== null) continue\n if (worst === undefined || bestFirst(slot.holder as Holder, worst.holder as Holder) > 0) worst = slot\n }\n if (worst === undefined || !beats(challenger, worst.holder as Holder, core.hysteresis)) break\n evict(core, worst, challenger, false)\n }\n}\n\nexport function stepSlots(core: ArbiterCore, dtSeconds: number): void {\n for (const kind of ['point', 'spot'] as const) {\n for (const slot of core.slots[kind]) {\n stepFade(slot, dtSeconds, core.fadeMs)\n if (slot.pending !== null && slot.fade <= 0) install(core.THREE, slot, slot.pending, false)\n }\n }\n}\n","import type { Fog, FogBase, FogExp2, Scene } from 'three'\n\nimport type { ThreeLib } from './three-lib'\n\n/** Both exact in half precision (multiples of 32 under 65 504), so the neutral fog survives a `mediump` fragment shader. */\nexport const NEUTRAL_FOG_NEAR = 6.0e4\nexport const NEUTRAL_FOG_FAR = 6.4e4\n\nexport interface FogOptions {\n /** `clear()` leaves a neutral linear Fog instead of `null`: the fog presence and type never change, so no program recompiles. */\n readonly stable?: boolean\n /** Colour of the neutral fog. Default 0x000000. */\n readonly color?: number\n}\n\nexport interface FogController {\n useLinear(color: number, near: number, far: number): Fog\n useExp2(color: number, density: number): FogExp2\n /** Back to no fog: the original fog (or `null`); when `stable`, the host's own linear fog or the neutral one, never `null`. */\n clear(): void\n}\n\nfunction isLinearFog(fog: FogBase | null): fog is Fog {\n return (fog as Partial<Fog> | null)?.isFog === true\n}\n\nfunction isExp2Fog(fog: FogBase | null): fog is FogExp2 {\n return (fog as Partial<FogExp2> | null)?.isFogExp2 === true\n}\n\nexport function isNeutralFog(fog: unknown): boolean {\n const candidate = fog as Partial<Fog> | null | undefined\n return candidate?.isFog === true && candidate.near === NEUTRAL_FOG_NEAR && candidate.far === NEUTRAL_FOG_FAR\n}\n\n/** Writes into the scene's existing fog object when its type matches, as the weather layer does, so nothing is reallocated per change. */\nexport function createFog(THREE: Pick<ThreeLib, 'Fog' | 'FogExp2'>, scene: Scene, options: FogOptions = {}): FogController {\n const original = scene.fog\n const hostLinear = isLinearFog(original) ? { color: original.color.getHex(), near: original.near, far: original.far } : undefined\n const resting = hostLinear ?? { color: options.color ?? 0, near: NEUTRAL_FOG_NEAR, far: NEUTRAL_FOG_FAR }\n return {\n useLinear(color, near, far) {\n const current = scene.fog\n if (isLinearFog(current)) {\n current.color.set(color)\n current.near = near\n current.far = far\n return current\n }\n const fog = new THREE.Fog(color, near, far)\n scene.fog = fog\n return fog\n },\n useExp2(color, density) {\n const current = scene.fog\n if (isExp2Fog(current)) {\n current.color.set(color)\n current.density = density\n return current\n }\n const fog = new THREE.FogExp2(color, density)\n scene.fog = fog\n return fog\n },\n clear() {\n if (options.stable !== true) {\n scene.fog = original\n return\n }\n const current = scene.fog\n if (isLinearFog(current)) {\n current.color.set(resting.color)\n current.near = resting.near\n current.far = resting.far\n return\n }\n scene.fog = new THREE.Fog(resting.color, resting.near, resting.far)\n },\n }\n}\n","import type { Object3D, Scene } from 'three'\n\nimport { NEUTRAL_FOG_NEAR } from '../fog'\nimport type { FogState, LightCheck, RendererState, SealOptions } from './types'\n\ntype Flags = Record<string, unknown>\ntype Counter = { count: number; shadows: number }\n\nconst LIGHT_FLAGS = [\n ['point', 'isPointLight'],\n ['spot', 'isSpotLight'],\n ['hemi', 'isHemisphereLight'],\n ['dir', 'isDirectionalLight'],\n ['ambient', 'isAmbientLight'],\n ['rect', 'isRectAreaLight'],\n] as const\n\n/** The visible lights the way the renderer collects them: they decide the shader's light counts. */\nexport function lightSignature(scene: Object3D): string {\n const counters: Record<string, Counter> = {}\n for (const [name] of LIGHT_FLAGS) counters[name] = { count: 0, shadows: 0 }\n scene.traverseVisible((object) => {\n const flags = object as unknown as Flags\n for (const [name, flag] of LIGHT_FLAGS) {\n if (flags[flag] !== true) continue\n const counter = counters[name] as Counter\n counter.count++\n if ((flags as { castShadow?: boolean }).castShadow === true) counter.shadows++\n }\n })\n return LIGHT_FLAGS.map(([name]) => `${name}:${(counters[name] as Counter).count}/${(counters[name] as Counter).shadows}`).join(' ')\n}\n\nexport function fogStateOf(scene: Scene): FogState {\n const fog = scene.fog as Flags | null\n if (fog === null) return 'none'\n return fog.isFogExp2 === true ? 'exp2' : 'linear'\n}\n\n/** Every PointLight/SpotLight under `scene` that is not under `group`, as a path of child indices. */\nexport function findStrays(scene: Object3D, group: Object3D): string[] {\n const strays: string[] = []\n const visit = (object: Object3D, path: string): void => {\n if (object === group) return\n const flags = object as unknown as Flags\n if (flags.isPointLight === true || flags.isSpotLight === true) strays.push(`${path}${object.name === '' ? object.type : object.name}`)\n object.children.forEach((child, index) => visit(child, `${path}${index}/`))\n }\n visit(scene, '')\n return strays\n}\n\nexport type Sealed = {\n readonly lights: string\n readonly environment: string\n readonly toneMapping: number\n readonly outputColorSpace: string\n readonly targetColorSpace: string\n readonly shadowMapType: number\n readonly fogStates: readonly FogState[]\n readonly cameras: SealOptions['cameras']\n readonly target: SealOptions['target']\n}\n\nfunction environmentOf(scene: Scene): string {\n return scene.environment === null ? 'none' : `texture:${scene.environment.mapping}`\n}\n\nfunction targetColorSpaceOf(target: SealOptions['target']): string {\n return target ? target.texture.colorSpace : 'canvas'\n}\n\nexport function captureSealed(renderer: RendererState, scene: Scene, options: SealOptions): Sealed {\n return {\n lights: lightSignature(scene),\n environment: environmentOf(scene),\n toneMapping: renderer.toneMapping,\n outputColorSpace: renderer.outputColorSpace,\n targetColorSpace: targetColorSpaceOf(options.target),\n shadowMapType: renderer.shadowMap.type,\n fogStates: options.fogStates ?? [fogStateOf(scene)],\n cameras: options.cameras,\n target: options.target,\n }\n}\n\ntype Described = {\n readonly lights: string\n readonly fog: string\n readonly environment: string\n readonly toneMapping: number\n readonly outputColorSpace: string\n readonly targetColorSpace: string\n readonly shadowMapType: number\n}\n\nfunction describe(state: Described): string {\n const { lights, fog, environment, toneMapping, outputColorSpace, targetColorSpace, shadowMapType } = state\n return `${lights} | fog:${fog} env:${environment} tone:${toneMapping} out:${outputColorSpace}/${targetColorSpace} shadow:${shadowMapType}`\n}\n\nfunction farBeyondNeutralFog(scene: Scene, sealed: Sealed): boolean {\n const fog = scene.fog as { isFog?: boolean; near?: number } | null\n if (fog?.isFog !== true || fog.near !== NEUTRAL_FOG_NEAR) return false\n return (sealed.cameras ?? []).some((camera) => ((camera as unknown as { far?: number }).far ?? 0) >= NEUTRAL_FOG_NEAR)\n}\n\nexport function checkSealed(sealed: Sealed | null, renderer: RendererState, scene: Scene, group: Object3D): LightCheck {\n const strays = findStrays(scene, group)\n const lights = lightSignature(scene)\n if (sealed === null) return { ok: strays.length === 0, expected: lights, actual: lights, strays }\n const fog = fogStateOf(scene)\n const fogAllowed = sealed.fogStates.includes(fog)\n const expected = describe({ ...sealed, fog: fogAllowed ? fog : sealed.fogStates.join('|') })\n const actual = describe({\n lights,\n fog,\n environment: environmentOf(scene),\n toneMapping: renderer.toneMapping,\n outputColorSpace: renderer.outputColorSpace,\n targetColorSpace: targetColorSpaceOf(sealed.target),\n shadowMapType: renderer.shadowMap.type,\n })\n const ok = strays.length === 0 && expected === actual && !farBeyondNeutralFog(scene, sealed)\n return { ok, expected, actual, strays }\n}\n","import type { HemisphereLight, Object3D, Scene } from 'three'\n\nimport { type ArbiterCore, admitRequest, arbitrateSources, poolOf, stepSlots } from './arbiter'\nimport { type Sealed, captureSealed, checkSealed } from './scene-state'\nimport { type Holder, type Slot, type Vec3, createSlots, createValues, isFree, markShadow, mergeValues, vacate, writeLight } from './slots'\nimport type {\n LightBudget,\n LightBudgetOptions,\n LightHandle,\n LightKind,\n LightLease,\n LightPatch,\n LightRequest,\n LightSource,\n RendererState,\n SourceHandle,\n ThreeForLights,\n} from './types'\n\nconst DEFAULTS = { point: 16, spot: 4, pointShadows: 2, spotShadows: 0, shadowMapSize: 512, urgentReserve: 2, fadeMs: 120, hysteresis: 0.15, hemisphere: 0 }\nconst ZERO: Vec3 = [0, 0, 0]\nconst ALL_LAYERS_MASK = -1\n\nfunction newHolder(kind: Holder['kind'], lightKind: LightKind, request: LightRequest | LightSource, urgent: boolean): Holder {\n return {\n kind,\n owner: request.owner,\n lightKind,\n priority: request.priority,\n urgent,\n values: createValues({ color: request.color, intensity: request.intensity, distance: request.distance, position: [...request.position], ...optional(request) }),\n slot: null,\n alive: true,\n onLost: undefined,\n follow: undefined,\n shadow: false,\n wanted: true,\n distance: 0,\n resolved: createValues({ color: request.color, intensity: 0, distance: 0, position: ZERO }),\n }\n}\n\nfunction optional(request: LightRequest | LightSource): Partial<Holder['values']> {\n const values: Partial<Holder['values']> = {}\n if (request.decay !== undefined) values.decay = request.decay\n if (request.target !== undefined) values.target = [...request.target]\n if (request.angle !== undefined) values.angle = request.angle\n if (request.penumbra !== undefined) values.penumbra = request.penumbra\n return values\n}\n\nfunction isAttached(object: Object3D, root: Object3D): boolean {\n for (let node: Object3D | null = object; node !== null; node = node.parent) if (node === root) return true\n return false\n}\n\nexport function createLightBudget(THREE: ThreeForLights, scene: Object3D, options: LightBudgetOptions = {}): LightBudget {\n const opts = { ...DEFAULTS, ...options }\n const pointShadows = Math.min(opts.pointShadows, opts.point)\n const spotShadows = Math.min(opts.spotShadows, opts.spot)\n const group = new THREE.Group()\n group.name = 'light-budget'\n const core: ArbiterCore = {\n THREE,\n slots: {\n point: createSlots(THREE, group, 'point', opts.point, pointShadows, opts.shadowMapSize, options.shadowSettings),\n spot: createSlots(THREE, group, 'spot', opts.spot, spotShadows, opts.shadowMapSize, options.shadowSettings),\n },\n urgentReserve: opts.urgentReserve,\n fadeMs: opts.fadeMs,\n hysteresis: opts.hysteresis,\n preempted: 0,\n refused: 0,\n }\n const hemispheres: HemisphereLight[] = []\n for (let index = 0; index < opts.hemisphere; index++) {\n const light = new THREE.HemisphereLight(0xffffff, 0xffffff, 0)\n light.layers.enableAll()\n group.add(light)\n hemispheres.push(light)\n }\n scene.add(group)\n scene.children.splice(scene.children.indexOf(group), 1)\n scene.children.unshift(group)\n\n const sources = new Set<Holder>()\n const world = new THREE.Vector3()\n const direction = new THREE.Vector3()\n let sealed: Sealed | null = null\n let leasedHemispheres = 0\n\n const allSlots = (): Slot[] => [...core.slots.point, ...core.slots.spot]\n\n function writeHolder(holder: Holder): void {\n if (holder.slot !== null && holder.slot.holder === holder) writeLight(THREE, holder.slot, holder.values)\n }\n\n function makeHandle(holder: Holder, release: ((fadeMs: number | undefined) => void) | null): LightHandle {\n return {\n get slot() {\n return holder.slot?.index ?? -1\n },\n kind: holder.lightKind,\n get alive() {\n return holder.alive\n },\n update(patch: LightPatch) {\n if (!holder.alive) return\n mergeValues(holder.values, patch)\n writeHolder(holder)\n },\n release(releaseOptions) {\n if (!holder.alive || release === null) return\n holder.alive = false\n release(releaseOptions?.fadeMs)\n },\n }\n }\n\n function dropHolder(holder: Holder, fadeMs: number): void {\n const slot = holder.slot\n holder.slot = null\n if (slot === null) return\n if (slot.pending === holder) slot.pending = null\n else if (slot.holder === holder) vacate(slot, fadeMs)\n }\n\n function resolveSource(holder: Holder, focus: readonly [number, number, number]): void {\n const { follow, values, resolved } = holder\n holder.wanted = values.intensity > 0\n if (follow === undefined) {\n holder.distance = Math.hypot(values.position[0] - focus[0], values.position[1] - focus[1], values.position[2] - focus[2])\n Object.assign(resolved, values)\n return\n }\n holder.wanted = holder.wanted && isAttached(follow, scene)\n if (!holder.wanted) return\n follow.getWorldPosition(world)\n const [ox, oy, oz] = values.position\n if (ox !== 0 || oy !== 0 || oz !== 0) world.set(ox, oy, oz).applyMatrix4(follow.matrixWorld)\n const axis = follow.matrixWorld.elements\n direction.set(-(axis[8] as number), -(axis[9] as number), -(axis[10] as number))\n holder.distance = Math.hypot(world.x - focus[0], world.y - focus[1], world.z - focus[2])\n Object.assign(resolved, values)\n resolved.position = [world.x, world.y, world.z]\n resolved.target = values.target ?? [world.x + direction.x, world.y + direction.y, world.z + direction.z]\n }\n\n return {\n group,\n sizes: { point: opts.point, spot: opts.spot, pointShadows, spotShadows },\n acquire(request) {\n const holder = newHolder('request', request.kind ?? 'point', request, request.urgent === true)\n holder.onLost = request.onLost\n if (!admitRequest(core, holder)) return null\n return makeHandle(holder, (fadeMs) => dropHolder(holder, fadeMs ?? opts.fadeMs))\n },\n addSource(source) {\n const holder = newHolder('source', source.kind ?? 'point', source, false)\n holder.follow = source.follow\n holder.shadow = source.shadow === true\n sources.add(holder)\n return {\n get slot() {\n return holder.slot !== null && holder.slot.holder === holder ? holder.slot.index : -1\n },\n update(patch: Partial<LightSource>) {\n mergeValues(holder.values, patch)\n if (patch.priority !== undefined) holder.priority = patch.priority\n if ('follow' in patch) holder.follow = patch.follow\n if (holder.follow === undefined) writeHolder(holder)\n },\n remove() {\n holder.alive = false\n holder.wanted = false\n sources.delete(holder)\n dropHolder(holder, opts.fadeMs)\n },\n } satisfies SourceHandle\n },\n lease({ owner, point, shadows = 0 }) {\n const shadowSlots = core.slots.point.filter((slot) => slot.shadow && isFree(slot))\n const plainSlots = core.slots.point.filter((slot) => !slot.shadow && isFree(slot))\n if (shadowSlots.length < shadows || plainSlots.length < point - shadows) return null\n const taken = [...shadowSlots.slice(0, shadows), ...plainSlots.slice(0, point - shadows)]\n const holders = taken.map((slot) => {\n const holder = newHolder('lease', 'point', { owner, priority: Number.POSITIVE_INFINITY, color: 0xffffff, intensity: 0, distance: 0, position: ZERO }, false)\n slot.leased = true\n slot.holder = holder\n slot.fade = 1\n slot.fadeTarget = 1\n holder.slot = slot\n markShadow(slot)\n return holder\n })\n const handles = holders.map((holder) => makeHandle(holder, null))\n return {\n handles,\n invalidateShadow(index) {\n for (const [at, holder] of holders.entries()) if ((index === undefined || index === at) && holder.slot !== null) markShadow(holder.slot)\n },\n release() {\n for (const holder of holders) {\n const slot = holder.slot\n holder.alive = false\n holder.slot = null\n if (slot === null) continue\n slot.leased = false\n slot.holder = null\n vacate(slot, 0)\n }\n },\n } satisfies LightLease\n },\n leaseHemisphere() {\n const light = hemispheres[leasedHemispheres]\n if (light === undefined) return null\n leasedHemispheres++\n return light\n },\n present(focus, dtSeconds) {\n for (const holder of sources) resolveSource(holder, focus)\n for (const kind of ['point', 'spot'] as const) {\n const reserve = kind === 'point' ? core.urgentReserve : 0\n const kindSources = [...sources].filter((holder) => holder.lightKind === kind)\n arbitrateSources(core, poolOf(core, kind, true), kindSources.filter((holder) => holder.shadow), 0)\n arbitrateSources(core, poolOf(core, kind, false), kindSources.filter((holder) => !holder.shadow), reserve)\n }\n stepSlots(core, dtSeconds)\n for (const holder of sources) if (holder.wanted && holder.slot?.holder === holder) writeLight(THREE, holder.slot, holder.resolved)\n },\n seal(renderer, scene, sealOptions = {}) {\n sealed = captureSealed(renderer, scene, sealOptions)\n },\n check(renderer: RendererState, checkedScene: Scene) {\n const result = checkSealed(sealed, renderer, checkedScene, group)\n const layersIntact = allSlots().every((slot) => slot.light.visible && slot.light.layers.mask === ALL_LAYERS_MASK)\n const isFirst = checkedScene.children[0] === group\n if (!layersIntact) return { ...result, ok: false, actual: `${result.actual} | budget lights hidden or off a layer` }\n return isFirst ? result : { ...result, ok: false, actual: `${result.actual} | budget group is not the first child of the scene` }\n },\n stats() {\n const slots = allSlots()\n return {\n lit: slots.filter((slot) => slot.light.intensity > 0).length,\n free: slots.filter(isFree).length,\n leased: slots.filter((slot) => slot.leased).length,\n sources: sources.size,\n sourcesWithoutSlot: [...sources].filter((holder) => holder.wanted && holder.slot === null).length,\n preempted: core.preempted,\n refused: core.refused,\n }\n },\n dispose() {\n group.removeFromParent()\n for (const slot of allSlots()) slot.light.dispose()\n sources.clear()\n },\n }\n}\n","import type { Camera, Group, HemisphereLight, Object3D, Scene, WebGLRenderTarget } from 'three'\n\nimport type { ThreeLib } from '../three-lib'\n\nexport type LightKind = 'point' | 'spot'\nexport type LinearRgb = readonly [number, number, number]\nexport type FogState = 'none' | 'linear' | 'exp2'\nexport type ThreeForLights = Pick<\n ThreeLib,\n 'Group' | 'PointLight' | 'SpotLight' | 'HemisphereLight' | 'Object3D' | 'Vector3' | 'Color' | 'LinearSRGBColorSpace'\n>\n/** What the budget reads of a renderer: a structural subset, so a fake works in Node. */\nexport type RendererState = {\n readonly toneMapping: number\n readonly outputColorSpace: string\n readonly shadowMap: { readonly type: number }\n}\n\nexport interface LightBudgetOptions {\n /** Point lights created at boot. Default 16. */\n readonly point?: number\n /** Spot lights created at boot. Default 4. */\n readonly spot?: number\n /** How many of the point lights cast shadows (cube maps). Default 2. Fixed for the life of the budget. */\n readonly pointShadows?: number\n /** How many of the spot lights cast shadows. Default 0. */\n readonly spotShadows?: number\n /** Shadow map size of the shadow slots. Default 512. */\n readonly shadowMapSize?: number\n /** Bias, normal bias and camera near plane of the shadow slots. Defaults: three.js's (0, 0, 0.5). The house's room lights use -0.0012, 0.05 and 0.12. */\n readonly shadowSettings?: { readonly bias?: number; readonly normalBias?: number; readonly near?: number }\n /** Point slots only `urgent` requests may take while others are free. Default 2. */\n readonly urgentReserve?: number\n /** Milliseconds a light fades out when a source loses its slot, and fades in when it gets one. Default 120. */\n readonly fadeMs?: number\n /** A source keeps its slot until a challenger of the same priority is this much closer (0.15 = 15%). Default 0.15. */\n readonly hysteresis?: number\n /** HemisphereLights created at boot for adapters that flash the sky; leased with `leaseHemisphere`. Default 0. */\n readonly hemisphere?: number\n}\n\n/** A light that lives as long as the request: a blast, a rocket in flight, a muzzle flash, a flare in the hand. */\nexport interface LightRequest {\n readonly owner: string\n readonly kind?: LightKind\n /** Higher wins a slot; see LIGHT_PRIORITY. */\n readonly priority: number\n /** Hex (sRGB, like `new Color(hex)`) or linear RGB. */\n readonly color: number | LinearRgb\n readonly intensity: number\n readonly distance: number\n readonly decay?: number\n readonly position: readonly [number, number, number]\n /** Spot only. */\n readonly target?: readonly [number, number, number]\n readonly angle?: number\n readonly penumbra?: number\n /** Takes a slot at once: cuts the lowest victim instead of fading it, and may use the urgent reserve. */\n readonly urgent?: boolean\n /** Called once when a higher priority took the slot; the handle is dead from then on. */\n readonly onLost?: () => void\n}\n\nexport type LightPatch = Partial<Pick<LightRequest, 'color' | 'intensity' | 'distance' | 'decay' | 'position' | 'target' | 'angle' | 'penumbra'>>\n\nexport interface LightHandle {\n /** Index of this light among the scene's point (or spot) lights in the shader; stable while the handle lives. */\n readonly slot: number\n readonly kind: LightKind\n readonly alive: boolean\n update(patch: LightPatch): void\n /** Gives the slot back; with fadeMs the light fades first. Idempotent. */\n release(options?: { readonly fadeMs?: number }): void\n}\n\n/** A light that exists in the world for a long time and competes for a slot every frame: a lamp, a street light, a headlight. */\nexport interface LightSource extends Omit<LightRequest, 'urgent' | 'onLost'> {\n /** Wants a shadow slot; a shadow source competes for shadow slots only. */\n readonly shadow?: boolean\n /**\n * The object the light sits on. Its world position (and, for a spot, its world -z as the direction when `target` is absent) is read\n * once per `present`; `position` is then an offset in its local space. A source whose `follow` left the scene counts as off.\n */\n readonly follow?: Object3D\n}\n\nexport interface SourceHandle {\n /** Slot now, or -1 while it has none (its light is not drawn). */\n readonly slot: number\n update(patch: Partial<LightSource>): void\n remove(): void\n}\n\n/** Slots a client keeps for itself for good: the room lights of a house (their masks are indexed by slot). */\nexport interface LightLease {\n readonly handles: readonly LightHandle[]\n /** Redraws the shadow map of one leased slot (`handles[index]`), or of all of them, on the next render: the walls it shades changed. */\n invalidateShadow(index?: number): void\n release(): void\n}\n\nexport interface SealOptions {\n /** Fog states the game will switch between at run time. Default: only the state at seal time. */\n readonly fogStates?: readonly FogState[]\n /** Cameras whose `far` must stay under the neutral fog's near (read at every `check`). */\n readonly cameras?: readonly Camera[]\n /** Where the game really draws; its colour space is part of the sealed state. */\n readonly target?: WebGLRenderTarget | null\n}\n\nexport interface LightCheck {\n readonly ok: boolean\n readonly expected: string\n readonly actual: string\n /** Paths of PointLight/SpotLight found outside the budget's group. */\n readonly strays: readonly string[]\n}\n\nexport interface LightBudgetStats {\n readonly lit: number\n readonly free: number\n readonly leased: number\n readonly sources: number\n readonly sourcesWithoutSlot: number\n readonly preempted: number\n readonly refused: number\n}\n\nexport interface LightBudget {\n /** The group holding every light; first child of the scene. Never hidden, never removed while the budget lives. */\n readonly group: Group\n readonly sizes: { readonly point: number; readonly spot: number; readonly pointShadows: number; readonly spotShadows: number }\n acquire(request: LightRequest): LightHandle | null\n addSource(source: LightSource): SourceHandle\n /** Takes `point` slots (of which `shadows` shadow slots) for good. null when not enough unleased slots remain. */\n lease(options: { readonly owner: string; readonly point: number; readonly shadows?: number }): LightLease | null\n /** Takes one of the boot HemisphereLights for good. null when none is left. Its intensity starts at 0. */\n leaseHemisphere(owner: string): HemisphereLight | null\n /** Once per frame, with the camera (or the player) as focus. `dtSeconds` is real time: fades do not stop in pause. */\n present(focus: readonly [number, number, number], dtSeconds: number): void\n /** After this, any change in the program-key state of the scene is a bug and `check()` reports it. */\n seal(renderer: RendererState, scene: Scene, options?: SealOptions): void\n check(renderer: RendererState, scene: Scene): LightCheck\n stats(): LightBudgetStats\n dispose(): void\n}\n\nexport const LIGHT_PRIORITY = {\n blast: 900,\n rocket: 800,\n muzzle: 750,\n fire: 600,\n flare: 500,\n headlight: 400,\n weaponLight: 400,\n lamp: 200,\n street: 150,\n} as const\n","import type { LightBudget } from './types'\n\n/** For adapters that receive the budget as `unknown` through the host (the contract does not know three). */\nexport function isLightBudget(value: unknown): value is LightBudget {\n const candidate = value as Partial<LightBudget> | null | undefined\n return typeof candidate === 'object' && candidate !== null && typeof candidate.acquire === 'function' && typeof candidate.addSource === 'function' && typeof candidate.lease === 'function' && typeof candidate.present === 'function'\n}\n","import type { Material } from 'three'\n\n/** What `programKeysOf` reads of a renderer: `properties` is internal to three r160 and may be absent in another version. */\nexport type PropertiesReader = { readonly properties?: { get(object: object): unknown } }\n\n/**\n * Every program cache key three linked for `material` (one per state it was drawn in: fog type, shadow, light counts). Reads three's\n * internal `properties.get(material).programs` Map (r160). `[]` for a material never drawn; `undefined` when this three does not have\n * that internal, which sends the pools to their conservative mode.\n */\nexport function programKeysOf(renderer: PropertiesReader, material: Material): readonly string[] | undefined {\n const properties = renderer.properties\n if (typeof properties?.get !== 'function') return undefined\n const programs = (properties.get(material) as { programs?: unknown } | undefined)?.programs\n if (programs === undefined) return []\n if (!(programs instanceof Map)) return undefined\n return [...programs.keys()].map(String)\n}\n","import type { Material } from 'three'\n\nimport { type PropertiesReader, programKeysOf } from '../program-key'\n\n/** Materials kept alive and never disposed, one per program family: what keeps a program linked once its users are gone. */\nexport interface MaterialAnchors {\n /**\n * Keeps `material` if any of its program keys has no anchor yet; returns true when it was kept (the caller must then not dispose it).\n * A material whose keys are all covered returns false and may be disposed. A material never drawn is not kept.\n */\n offer(material: Material): boolean\n readonly size: number\n /** Where program keys are read from. Until bound (or when three has no readable keys) every offered material is kept. */\n bind(renderer: PropertiesReader): void\n}\n\nexport function createMaterialAnchors(renderer?: PropertiesReader): MaterialAnchors {\n const anchors = new Set<Material>()\n let reader = renderer\n\n function coveredKeys(): Set<string> {\n const covered = new Set<string>()\n if (reader === undefined) return covered\n for (const anchor of anchors) for (const key of programKeysOf(reader, anchor) ?? []) covered.add(key)\n return covered\n }\n\n return {\n get size() {\n return anchors.size\n },\n bind(next) {\n reader = next\n },\n offer(material) {\n if (anchors.has(material)) return true\n const keys = reader === undefined ? undefined : programKeysOf(reader, material)\n if (keys === undefined) {\n anchors.add(material)\n return true\n }\n if (keys.length === 0) return false\n const covered = coveredKeys()\n if (keys.every((key) => covered.has(key))) return false\n anchors.add(material)\n return true\n },\n }\n}\n","import type { Object3D } from 'three'\n\nimport type { ThreeLib } from './three-lib'\nimport type { WarmupProbe } from './warmup-types'\n\n/** The sprite probes the room builds by hand today: one sprite material with fog and one without. */\nexport function spriteProbes(THREE: Pick<ThreeLib, 'Group' | 'Sprite' | 'SpriteMaterial' | 'DataTexture'>): WarmupProbe {\n return {\n id: 'sprite',\n build() {\n const group = new THREE.Group()\n const texture = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1)\n texture.needsUpdate = true\n const materials = [true, false].map((fog) => new THREE.SpriteMaterial({ map: texture, fog, transparent: true }))\n for (const material of materials) group.add(new THREE.Sprite(material))\n return { group, materials, dispose: () => texture.dispose() }\n },\n }\n}\n\nexport function hasMissingDistanceMaterial(root: Object3D): string | undefined {\n let found: string | undefined\n root.traverse((object) => {\n const mesh = object as { isInstancedMesh?: boolean; castShadow?: boolean; customDepthMaterial?: unknown; customDistanceMaterial?: unknown }\n if (found === undefined && mesh.isInstancedMesh === true && mesh.castShadow === true && mesh.customDepthMaterial && !mesh.customDistanceMaterial) found = object.name || object.type\n })\n return found\n}\n","import type { BufferGeometry, Camera, Material, Object3D, Scene } from 'three'\n\nimport { fogStateOf, lightSignature } from './lights/scene-state'\nimport type { FogState } from './lights/types'\nimport { createMaterialAnchors } from './pools/anchors'\nimport { hasMissingDistanceMaterial } from './probes'\nimport type { ThreeLib } from './three-lib'\nimport type { EffectWarmup, ProgramWarmup, WarmupContext, WarmupOptions, WarmupProbe, WarmupReport, WarmupRenderer } from './warmup-types'\n\nconst EFFECT_STEP_SECONDS = 1 / 50\nconst DEFAULT_EFFECT_FRAMES = 3\nconst WARM_FOG_DENSITY = 0.01\nconst WARM_FOG_FAR = 100\n\ntype LinkCounter = { count: number }\ntype BuiltProbe = { readonly probe: WarmupProbe; readonly built: ReturnType<WarmupProbe['build']> }\n\nfunction register<TItem>(items: Set<TItem>, item: TItem): () => void {\n items.add(item)\n return () => void items.delete(item)\n}\n\nfunction countLinks(context: WarmupContext, counters: WeakMap<WarmupContext, LinkCounter>): void {\n const existing = counters.get(context)\n if (existing !== undefined) {\n existing.count = 0\n return\n }\n const counter: LinkCounter = { count: 0 }\n counters.set(context, counter)\n const original = context.linkProgram.bind(context)\n context.linkProgram = (program) => {\n counter.count++\n original(program)\n }\n}\n\nfunction failedProgram(renderer: WarmupRenderer): string | undefined {\n for (const program of renderer.info.programs ?? []) {\n const diagnostics = (program as { diagnostics?: { runnable?: boolean; programLog?: string } }).diagnostics\n if (diagnostics?.runnable === false) return diagnostics.programLog ?? 'program failed to link'\n }\n return undefined\n}\n\n/**\n * Finishes every program the renderer holds: three.js links a program when a material first compiles, but reads its link status, its logs and its\n * uniform locations only on the program's first draw (`getUniforms`), and that read waits for the driver to end the link: ~7 ms a program on the\n * RTX 5070 laptop, 43 ms for the six shadow-receiving programs the horde chain's first blast drew for the first time (perf.mjs, 2026-11-03).\n * A program linked here and drawn only in play (a hidden part of a vehicle, an effect of another area) pays that at load instead.\n */\nfunction finishPrograms(renderer: WarmupRenderer): number {\n let finished = 0\n for (const program of renderer.info.programs ?? []) {\n const linked = program as { getUniforms?: () => unknown; getAttributes?: () => unknown }\n if (typeof linked.getUniforms !== 'function') continue\n linked.getUniforms()\n linked.getAttributes?.()\n finished++\n }\n return finished\n}\n\nexport function createProgramWarmup(THREE: Pick<ThreeLib, 'Group' | 'Sprite' | 'SpriteMaterial' | 'DataTexture' | 'Fog' | 'FogExp2' | 'Mesh' | 'Points' | 'LineSegments' | 'BufferGeometry' | 'BufferAttribute'>): ProgramWarmup {\n const probes = new Set<WarmupProbe>()\n const effects = new Set<EffectWarmup>()\n const textures = new Set<Parameters<WarmupRenderer['initTexture']>[0]>()\n const anchors = createMaterialAnchors()\n const linkCounters = new WeakMap<WarmupContext, LinkCounter>()\n\n function setFog(scene: Scene, state: FogState): void {\n const color = scene.fog?.color.getHex() ?? 0\n if (state === 'none') scene.fog = null\n else if (state === 'linear' && (scene.fog as { isFog?: boolean } | null)?.isFog !== true) scene.fog = new THREE.Fog(color, 1, WARM_FOG_FAR)\n else if (state === 'exp2' && (scene.fog as { isFogExp2?: boolean } | null)?.isFogExp2 !== true) scene.fog = new THREE.FogExp2(color, WARM_FOG_DENSITY)\n }\n\n function draw(renderer: WarmupRenderer, scene: Scene, camera: Camera, target: WarmupOptions['target']): void {\n renderer.setRenderTarget(target ?? null)\n renderer.compile(scene, camera)\n renderer.render(scene, camera)\n }\n\n function playEffect(renderer: WarmupRenderer, scene: Scene, camera: Camera, effect: EffectWarmup, target: WarmupOptions['target']): void {\n effect.play()\n try {\n for (let frame = 0; frame < (effect.frames ?? DEFAULT_EFFECT_FRAMES); frame++) {\n effect.step(EFFECT_STEP_SECONDS)\n renderer.setRenderTarget(target ?? null)\n renderer.render(scene, camera)\n }\n } finally {\n effect.stop()\n }\n }\n\n /** One object per material, of the kind its program is for, each a single triangle: drawn once so that the program is used, not only linked. */\n function materialsProbe(materials: readonly Material[]): BuiltProbe {\n const geometry = new THREE.BufferGeometry()\n geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array([0, 0, 0, 0.001, 0, 0, 0, 0.001, 0]), 3))\n const group = new THREE.Group()\n for (const material of materials) group.add(drawableOf(material, geometry))\n return { probe: { id: 'materials', build: () => ({ group, materials: [], dispose: () => geometry.dispose() }) }, built: { group, materials: [], dispose: () => geometry.dispose() } }\n }\n\n function drawableOf(material: Material, geometry: BufferGeometry): Object3D {\n const flags = material as { isSpriteMaterial?: boolean; isPointsMaterial?: boolean; isLineBasicMaterial?: boolean }\n const object: Object3D =\n flags.isSpriteMaterial === true ? new THREE.Sprite(material as never)\n : flags.isPointsMaterial === true ? new THREE.Points(geometry, material)\n : flags.isLineBasicMaterial === true ? new THREE.LineSegments(geometry, material)\n : new THREE.Mesh(geometry, material)\n object.frustumCulled = false\n return object\n }\n\n function buildProbes(materials: readonly Material[] | undefined): BuiltProbe[] {\n const built = [...probes].map((probe) => ({ probe, built: probe.build() }))\n if (materials !== undefined && materials.length > 0) built.push(materialsProbe(materials))\n return built\n }\n\n function addProbes(scene: Scene, built: readonly BuiltProbe[]): void {\n for (const { probe, built: item } of built) {\n const missing = hasMissingDistanceMaterial(item.group)\n if (missing !== undefined) throw new Error(`warmup probe \"${probe.id}\": instanced mesh \"${missing}\" casts shadows with a customDepthMaterial but no customDistanceMaterial`)\n scene.add(item.group)\n }\n }\n\n function removeProbes(scene: Scene, built: readonly BuiltProbe[]): void {\n for (const { built: item } of built) {\n scene.remove(item.group)\n item.dispose()\n for (const material of item.materials) anchors.offer(material)\n }\n }\n\n function drawStates(renderer: WarmupRenderer, scene: Scene, camera: Camera, states: readonly FogState[], target: WarmupOptions['target']): void {\n for (const state of states) {\n setFog(scene, state)\n draw(renderer, scene, camera, target)\n }\n }\n\n /** Makes every instanced batch draw at least once: three skips an empty instanced batch, and a driver finishes a program's state on its first real draw (a 200 ms stall). Returns the undo. */\n function forceDraws(scene: Scene): () => void {\n const undo: Array<() => void> = []\n scene.traverse((object) => {\n const drawable = object as Object3D & { isInstancedMesh?: boolean; count?: number; geometry?: { isInstancedBufferGeometry?: boolean; instanceCount?: number } }\n const { geometry } = drawable\n if (geometry?.isInstancedBufferGeometry === true && geometry.instanceCount === 0) {\n geometry.instanceCount = 1\n undo.push(() => (geometry.instanceCount = 0))\n }\n if (drawable.isInstancedMesh === true && drawable.count === 0) {\n drawable.count = 1\n undo.push(() => (drawable.count = 0))\n }\n })\n return () => undo.forEach((restore) => restore())\n }\n\n return {\n anchors,\n register: (probe) => register(probes, probe),\n registerEffect: (effect) => register(effects, effect),\n registerTexture: (texture) => register(textures, texture),\n warmUp(renderer, scene, camera, options = {}): WarmupReport {\n const started = performance.now()\n const { lights, target } = options\n anchors.bind(renderer)\n const originalFog = scene.fog\n const states = options.fogStates ?? [fogStateOf(scene)]\n lights?.seal(renderer, scene, { fogStates: options.fogStates, cameras: [camera], target })\n scene.traverse((object) => {\n const light = object as { castShadow?: boolean; shadow?: { needsUpdate: boolean } }\n if (light.castShadow === true && light.shadow !== undefined) light.shadow.needsUpdate = true\n })\n const built = buildProbes(options.materials)\n let restoreDraws = (): void => undefined\n try {\n addProbes(scene, built)\n restoreDraws = forceDraws(scene)\n drawStates(renderer, scene, camera, states, target)\n scene.fog = originalFog\n for (const effect of effects) playEffect(renderer, scene, camera, effect, target)\n for (const texture of textures) renderer.initTexture(texture)\n } finally {\n restoreDraws()\n scene.fog = originalFog\n removeProbes(scene, built)\n }\n // also what lets failedProgram see a broken link: three only fills `diagnostics` on the first use\n finishPrograms(renderer)\n const context = renderer.getContext()\n const uniformVectors = Number(context.getParameter(context.MAX_FRAGMENT_UNIFORM_VECTORS))\n const failure = failedProgram(renderer)\n if (failure !== undefined) {\n throw new Error(`a shader program failed to link (MAX_FRAGMENT_UNIFORM_VECTORS ${uniformVectors}, lights: ${lightSignature(scene)}): ${failure}`)\n }\n countLinks(context, linkCounters)\n // the GPU process is still working through all of the above: wait here, at load, or the first upload of play stalls on it\n if (context.RGBA !== undefined && context.UNSIGNED_BYTE !== undefined) context.readPixels?.(0, 0, 1, 1, context.RGBA, context.UNSIGNED_BYTE, new Uint8Array(4))\n return {\n programs: renderer.info.programs?.length ?? 0,\n geometries: renderer.info.memory.geometries,\n textures: renderer.info.memory.textures,\n milliseconds: performance.now() - started,\n lightSignature: lightSignature(scene),\n uniformVectors,\n anchors: anchors.size,\n }\n },\n linkedSince(renderer) {\n return linkCounters.get(renderer.getContext())?.count ?? 0\n },\n }\n}\n","const POOLED = '__pooled'\n\ntype Disposable = { dispose(): void; userData: Record<string, unknown> }\n\nfunction isDevelopment(): boolean {\n return typeof process === 'undefined' || process.env.NODE_ENV !== 'production'\n}\n\n/** Marks a pool resource and, in development, warns when something disposes it directly instead of releasing it to the pool. */\nexport function markPooled(resource: Disposable, key: string, owner: string): { disposeForPool(): void } {\n resource.userData[POOLED] = key\n const original = resource.dispose.bind(resource)\n let fromPool = false\n if (isDevelopment()) {\n resource.dispose = () => {\n if (!fromPool) console.warn(`[game-render-three] ${owner} \"${key}\" was disposed directly; release it to its pool instead`)\n original()\n }\n }\n return {\n disposeForPool() {\n fromPool = true\n original()\n fromPool = false\n },\n }\n}\n\nexport function pooledKey(resource: { userData: Record<string, unknown> }): string | undefined {\n const key = resource.userData[POOLED]\n return typeof key === 'string' ? key : undefined\n}\n\n/** A number as a short stable string, rounded to `step`. */\nexport function roundedNumber(value: number, step: number): string {\n return String(Number((Math.round(value / step) * step).toFixed(6)))\n}\n","import type { BufferGeometry } from 'three'\n\nimport { markPooled, pooledKey, roundedNumber } from './pooled'\n\nexport interface GeometryPool {\n /** The geometry for `key`, built once with `build`. Counts one use. */\n acquire(key: string, build: () => BufferGeometry): BufferGeometry\n release(geometry: BufferGeometry): void\n /** Never disposed while the pool lives (unit shapes, effect geometries warmed at load). */\n retain(key: string): void\n /** Disposes unretained geometries with no use, oldest first, keeping at most `keep` idle ones. Only at load points, never in play. */\n trim(keep?: number): number\n stats(): { readonly entries: number; readonly retained: number; readonly inUse: number }\n dispose(): void\n}\n\ntype Entry = { geometry: BufferGeometry; uses: number; retained: boolean; idleSince: number; disposeForPool(): void }\n\nconst DEFAULT_KEY_STEP = 1e-3\n\n/** Builds a key from a kind and its numbers, rounded to `step`, so near-equal sizes share one geometry. */\nexport function geometryKey(kind: string, params: readonly number[], step: number = DEFAULT_KEY_STEP): string {\n return [kind, ...params.map((value) => roundedNumber(value, step))].join('|')\n}\n\nexport function createGeometryPool(): GeometryPool {\n const entries = new Map<string, Entry>()\n let clock = 0\n\n function disposeEntry(key: string, entry: Entry): void {\n entries.delete(key)\n entry.disposeForPool()\n }\n\n return {\n acquire(key, build) {\n let entry = entries.get(key)\n if (entry === undefined) {\n const geometry = build()\n entry = { geometry, uses: 0, retained: false, idleSince: 0, ...markPooled(geometry, key, 'geometry') }\n entries.set(key, entry)\n }\n entry.uses++\n return entry.geometry\n },\n release(geometry) {\n const key = pooledKey(geometry)\n const entry = key === undefined ? undefined : entries.get(key)\n if (entry === undefined || entry.uses === 0) return\n entry.uses--\n if (entry.uses === 0) entry.idleSince = ++clock\n },\n retain(key) {\n const entry = entries.get(key)\n if (entry !== undefined) entry.retained = true\n },\n trim(keep = 0) {\n const idle = [...entries].filter(([, entry]) => entry.uses === 0 && !entry.retained).sort((a, b) => a[1].idleSince - b[1].idleSince)\n const excess = idle.slice(0, Math.max(0, idle.length - keep))\n for (const [key, entry] of excess) disposeEntry(key, entry)\n return excess.length\n },\n stats() {\n const all = [...entries.values()]\n return { entries: all.length, retained: all.filter((entry) => entry.retained).length, inUse: all.filter((entry) => entry.uses > 0).length }\n },\n dispose() {\n for (const [key, entry] of [...entries]) disposeEntry(key, entry)\n },\n }\n}\n","import type { Material } from 'three'\n\nimport type { PropertiesReader } from '../program-key'\nimport { type MaterialAnchors, createMaterialAnchors } from './anchors'\nimport { markPooled, pooledKey, roundedNumber } from './pooled'\n\nexport interface MaterialPool {\n /**\n * The material for `key`, built once. Shared: callers must not change it. Per-instance variety goes into instance attributes, not\n * into new materials.\n */\n acquire<TMaterial extends Material>(key: string, build: () => TMaterial): TMaterial\n release(material: Material): void\n /** Declares a variant that must never be disposed (its program stays linked). Effects register theirs at load. */\n register<TMaterial extends Material>(key: string, build: () => TMaterial): TMaterial\n /**\n * A material the caller owns and changes per frame (a puff's opacity): not shared. On `release` the first one of its program family is\n * handed to the anchors, so disposing the others never frees the program.\n */\n own<TMaterial extends Material>(build: () => TMaterial): TMaterial\n /** The materials declared with `register`: the ones that must never be disposed and whose programs the warmup draws once. */\n registeredMaterials?(): readonly Material[]\n /** Disposes unregistered materials with no use, oldest first, keeping at most `keep` idle ones; a family's anchor is kept alive. */\n trim(keep?: number): number\n stats(): { readonly entries: number; readonly registered: number; readonly inUse: number; readonly families: number }\n /**\n * The end of the pool: disposes every material it made, in use or not, registered or anchored (the guarantee that a program stays linked\n * holds while the pool lives, not after it). The anchors object itself is not touched, and the pool is empty and usable again afterwards.\n */\n dispose(): void\n}\n\nexport type MaterialPoolOptions = { readonly anchors?: MaterialAnchors; readonly renderer?: PropertiesReader }\n\ntype Entry = { material: Material; uses: number; registered: boolean; idleSince: number; disposeForPool(): void }\n\nconst DEFAULT_KEY_STEP = 1e-3\n\n/** Key from the material class and every parameter that changes its look, sorted, numbers rounded. */\nexport function materialKey(type: string, params: Readonly<Record<string, unknown>>): string {\n const parts = Object.keys(params)\n .sort()\n .map((name) => {\n const value = params[name]\n return `${name}=${typeof value === 'number' ? roundedNumber(value, DEFAULT_KEY_STEP) : JSON.stringify(value)}`\n })\n return [type, ...parts].join('|')\n}\n\nexport function createMaterialPool(options: MaterialPoolOptions = {}): MaterialPool {\n const anchors = options.anchors ?? createMaterialAnchors()\n if (options.renderer !== undefined) anchors.bind(options.renderer)\n const entries = new Map<string, Entry>()\n const owned = new Map<Material, () => void>()\n let clock = 0\n\n function createEntry(key: string, material: Material, registered: boolean): Entry {\n const entry: Entry = { material, uses: 0, registered, idleSince: 0, ...markPooled(material, key, 'material') }\n entries.set(key, entry)\n return entry\n }\n\n /** Disposes the material unless it is the anchor of a program family. */\n function retire(material: Material, disposeForPool: () => void): boolean {\n if (anchors.offer(material)) return false\n disposeForPool()\n return true\n }\n\n return {\n acquire(key, build) {\n const entry = entries.get(key) ?? createEntry(key, build(), false)\n entry.uses++\n return entry.material as ReturnType<typeof build>\n },\n register(key, build) {\n const entry = entries.get(key) ?? createEntry(key, build(), true)\n entry.registered = true\n return entry.material as ReturnType<typeof build>\n },\n own(build) {\n const material = build()\n owned.set(material, markPooled(material, 'own', 'material').disposeForPool)\n return material\n },\n release(material) {\n const disposeOwned = owned.get(material)\n if (disposeOwned !== undefined) {\n owned.delete(material)\n retire(material, disposeOwned)\n return\n }\n const key = pooledKey(material)\n const entry = key === undefined ? undefined : entries.get(key)\n if (entry === undefined || entry.uses === 0) return\n entry.uses--\n if (entry.uses === 0) entry.idleSince = ++clock\n },\n trim(keep = 0) {\n const idle = [...entries].filter(([, entry]) => entry.uses === 0 && !entry.registered).sort((a, b) => a[1].idleSince - b[1].idleSince)\n let disposed = 0\n for (const [key, entry] of idle.slice(0, Math.max(0, idle.length - keep))) {\n entries.delete(key)\n if (retire(entry.material, entry.disposeForPool)) disposed++\n }\n return disposed\n },\n registeredMaterials() {\n return [...entries.values()].filter((entry) => entry.registered).map((entry) => entry.material)\n },\n stats() {\n const all = [...entries.values()]\n return {\n entries: all.length,\n registered: all.filter((entry) => entry.registered).length,\n inUse: all.filter((entry) => entry.uses > 0).length,\n families: anchors.size,\n }\n },\n dispose() {\n for (const entry of entries.values()) entry.disposeForPool()\n for (const disposeOwned of owned.values()) disposeOwned()\n entries.clear()\n owned.clear()\n },\n }\n}\n","import type { GeometryPool } from './geometry'\nimport type { MaterialPool } from './material'\n\n/** The pools a host lends its adapters (`host.pools`): geometries and materials that outlive the effect that asked for them. */\nexport interface RenderPools {\n readonly geometries: GeometryPool\n readonly materials: MaterialPool\n}\n\n/** For adapters that receive the pools as `unknown` through the host (the contract does not know three). */\nexport function isRenderPools(value: unknown): value is RenderPools {\n const candidate = value as Partial<RenderPools> | null | undefined\n return typeof candidate === 'object' && candidate !== null && typeof candidate.geometries?.acquire === 'function' && typeof candidate.materials?.acquire === 'function'\n}\n","/** The part of a WebGL2 context the pacer reads; the renderer's `getContext()` satisfies it. */\nexport type FramePacerContext = {\n readonly SYNC_GPU_COMMANDS_COMPLETE: number\n readonly TIMEOUT_EXPIRED: number\n fenceSync(condition: number, flags: number): WebGLSync | null\n clientWaitSync(sync: WebGLSync, flags: number, timeout: number): number\n deleteSync(sync: WebGLSync | null): void\n flush(): void\n}\n\nexport type FramePacerOptions = {\n /** Most frames in a row the pacer may hold back before it lets one through (default 3), so a stuck fence cannot freeze the picture. */\n readonly maxSkips?: number\n /** Frames the GPU may be behind before a draw is held back (default 2). The browser reports a fence only some 20 ms after the GPU finished it, so 1 would cap every GPU at one frame per report. */\n readonly framesInFlight?: number\n}\n\nexport type FramePacer = {\n /** True when fewer than `framesInFlight` drawn frames are unfinished: draw this frame. False: the GPU is too far behind, skip the draw and keep simulating. */\n ready(): boolean\n /** Fences the frame just drawn; call it right after the render. */\n submitted(): void\n /** Draws held back so far. */\n readonly skipped: number\n dispose(): void\n}\n\nconst DEFAULT_MAX_SKIPS = 3\nconst DEFAULT_FRAMES_IN_FLIGHT = 2\n\n/**\n * Keeps the GPU a few frames behind the CPU at most. A frame loop that draws as fast as it can (no vsync, as the perf runs do, or a slow GPU) fills the driver's queue and\n * then blocks in the compositor for the length of the whole queue: a run of 5 ms frames and a 150 ms one. Holding back the draw while the last frame is unfinished spends that\n * time on the next simulation step instead, and the longest frame is the GPU's own. A context without fences (WebGL1) is never held back.\n */\nexport function createFramePacer(context: FramePacerContext | null, options: FramePacerOptions = {}): FramePacer {\n const maxSkips = options.maxSkips ?? DEFAULT_MAX_SKIPS\n const framesInFlight = options.framesInFlight ?? DEFAULT_FRAMES_IN_FLIGHT\n const fences: WebGLSync[] = []\n let skippedInRow = 0\n let skipped = 0\n const dropOldest = (): void => {\n const oldest = fences.shift()\n if (oldest && context) context.deleteSync(oldest)\n }\n return {\n ready() {\n if (!context) return true\n while (fences.length > 0 && context.clientWaitSync(fences[0] as WebGLSync, 0, 0) !== context.TIMEOUT_EXPIRED) dropOldest()\n if (fences.length >= framesInFlight && skippedInRow < maxSkips) {\n skippedInRow++\n skipped++\n return false\n }\n skippedInRow = 0\n if (fences.length >= framesInFlight) dropOldest()\n return true\n },\n submitted() {\n if (!context || typeof context.fenceSync !== 'function') return\n const fence = context.fenceSync(context.SYNC_GPU_COMMANDS_COMPLETE, 0)\n if (fence) fences.push(fence)\n context.flush()\n },\n get skipped() {\n return skipped\n },\n dispose() {\n while (fences.length > 0) dropOldest()\n },\n }\n}\n","import type { Bone, BufferAttribute, BufferGeometry, Material, Matrix3, Matrix4, Mesh, Object3D, SkinnedMesh } from 'three'\n\nimport type { ThreeLib } from './three-lib'\n\n/** The layer a batched mesh is moved to: no camera draws it, a raycaster that enables every layer still finds it. */\nexport const BATCHED_LAYER = 30\n\nconst MATRIX_EPSILON = 1e-6\n\nexport type StaticBatcherOptions = {\n /** Which meshes may be batched (default every shown mesh with one material and no instancing, skinning or morphing). */\n readonly include?: (mesh: Mesh) => boolean\n /** Milliseconds of merging per `update` (default all at once, in the call): batches are made a material at a time over the next frames. */\n readonly budgetMs?: number\n /**\n * The meshes move on their own (a body posed bone by bone): each batch is skinned to its meshes, so a mesh carries its part of the batch\n * wherever it goes, and only a new geometry, material, dent or hiding gives it its own draw back.\n */\n readonly followMeshes?: boolean\n}\n\nexport type StaticBatcherStats = { readonly batches: number; readonly batched: number; readonly released: number }\n\nexport type StaticBatcher = {\n /** The batch meshes, a child of the root. */\n readonly group: Object3D\n /**\n * Gives back to its own draw every batched mesh that changed since the batch was made (moved, dented, hidden, given another geometry or\n * material); returns how many it released. Call it once per frame, before the render.\n */\n update(): number\n stats(): StaticBatcherStats\n /** Every mesh draws on its own again and the batches are disposed. */\n dispose(): void\n}\n\ntype Source = {\n readonly mesh: Mesh\n readonly geometry: BufferGeometry\n readonly material: Material\n readonly version: number\n readonly local: Matrix4\n /** The mesh's world matrix when last seen unchanged: equal again, nothing moved and the local check is skipped. */\n readonly world: Matrix4\n readonly mask: number\n readonly batch: Batch\n readonly start: number\n readonly count: number\n released: boolean\n}\n\ntype Batch = { readonly mesh: Mesh; live: number; readonly index: { array: ArrayLike<number> & { [n: number]: number }; needsUpdate: boolean } }\n\nconst ATTRIBUTES = ['position', 'normal', 'uv', 'color'] as const\n\n/** Visible all the way up to `root`; a mesh no longer under it (its model was rebuilt) is not shown. */\nconst isShown = (object: Object3D, root: Object3D): boolean => {\n for (let node: Object3D | null = object; node; node = node.parent) {\n if (node === root) return true\n if (!node.visible) return false\n }\n return false\n}\n\nconst isBatchable = (mesh: Mesh): boolean => {\n const flags = mesh as Mesh & { isInstancedMesh?: boolean; isSkinnedMesh?: boolean }\n if (flags.isInstancedMesh === true || flags.isSkinnedMesh === true || Array.isArray(mesh.material)) return false\n const geometry = mesh.geometry\n return geometry.attributes.position !== undefined && Object.keys(geometry.morphAttributes).length === 0\n}\n\nconst signatureOf = (mesh: Mesh): string => {\n const { attributes } = mesh.geometry\n const present = ATTRIBUTES.filter((name) => attributes[name] !== undefined).map((name) => `${name}${attributes[name]!.itemSize}`)\n return `${(mesh.material as Material).uuid}|${present.join(',')}|${mesh.castShadow ? 1 : 0}${mesh.receiveShadow ? 1 : 0}`\n}\n\nconst sameMatrix = (a: Matrix4, b: Matrix4): boolean => a.elements.every((value, index) => Math.abs(value - (b.elements[index] as number)) < MATRIX_EPSILON)\n\n/**\n * Draws the still, undamaged meshes of an object (a vehicle, a piece of furniture) as one batch per material, and gives each mesh its own draw\n * back the moment it changes. The meshes stay where they are: what hits, dents or cuts them works on them as before, they only leave the\n * camera's layer while batched.\n */\nexport function createStaticBatcher(THREE: ThreeLib, root: Object3D, options: StaticBatcherOptions = {}): StaticBatcher {\n const include = options.include ?? (() => true)\n const follow = options.followMeshes === true\n root.updateMatrixWorld(true)\n const inverse = new THREE.Matrix4().copy(root.matrixWorld).invert()\n const groups = new Map<string, Mesh[]>()\n root.traverse((object) => {\n const mesh = object as Mesh\n if (!mesh.isMesh || !isShown(mesh, root) || !isBatchable(mesh) || !include(mesh)) return\n const key = signatureOf(mesh)\n groups.set(key, [...(groups.get(key) ?? []), mesh])\n })\n const group = new THREE.Group()\n group.name = 'static-batches'\n const sources: Source[] = []\n const pending = [...groups.values()].filter((meshes) => meshes.length >= 2)\n const mergeSome = (deadline: number): void => {\n while (pending.length > 0) {\n const meshes = pending.shift() as Mesh[]\n // a mesh that changed while it waited is left to draw on its own\n if (meshes.every((mesh) => isShown(mesh, root))) sources.push(...mergeGroup(THREE, { meshes, inverse: new THREE.Matrix4().copy(root.matrixWorld).invert(), group, follow }))\n if (performance.now() >= deadline) return\n }\n }\n if (options.budgetMs === undefined) mergeSome(Number.POSITIVE_INFINITY)\n root.add(group)\n const scratch = new THREE.Matrix4()\n let released = 0\n\n const release = (source: Source): void => {\n source.released = true\n source.mesh.layers.mask = source.mask\n const { index } = source.batch\n const first = index.array[source.start] as number\n for (let at = source.start; at < source.start + source.count; at++) index.array[at] = first\n index.needsUpdate = true\n // a batch whose meshes all draw on their own again is an empty draw: it leaves the screen\n if (--source.batch.live === 0) source.batch.mesh.visible = false\n released++\n }\n const hasChanged = (source: Source): boolean => {\n const { mesh } = source\n if (mesh.geometry !== source.geometry || mesh.material !== source.material || !isShown(mesh, root)) return true\n if ((mesh.geometry.attributes.position as BufferAttribute).version !== source.version) return true\n if (follow || sameMatrix(mesh.matrixWorld, source.world)) return false\n if (!sameMatrix(scratch.multiplyMatrices(inverse, mesh.matrixWorld), source.local)) return true\n // the whole object moved, the mesh did not move within it\n source.world.copy(mesh.matrixWorld)\n return false\n }\n\n return {\n group,\n update() {\n if (pending.length > 0) mergeSome(performance.now() + (options.budgetMs ?? Number.POSITIVE_INFINITY))\n const before = released\n inverse.copy(root.matrixWorld).invert()\n for (const source of sources) if (!source.released && hasChanged(source)) release(source)\n return released - before\n },\n stats: () => ({ batches: group.children.length, batched: sources.filter((source) => !source.released).length, released }),\n dispose() {\n for (const source of sources) if (!source.released) source.mesh.layers.mask = source.mask\n group.removeFromParent()\n for (const child of group.children) {\n ;(child as Mesh).geometry.dispose()\n ;(child as SkinnedMesh).skeleton?.dispose()\n }\n },\n }\n}\n\n/** Copies `attribute` into `target` at `offset` vertices: positions through `matrix`, normals through its normal matrix, the rest as they are. */\nfunction copyAttribute(name: string, attribute: BufferAttribute, target: Float32Array, offset: number, matrix: Matrix4, normalMatrix: Matrix3): void {\n const size = attribute.itemSize\n const source = attribute.array as ArrayLike<number>\n const flat = !(attribute as { isInterleavedBufferAttribute?: boolean }).isInterleavedBufferAttribute && source.length === attribute.count * size\n if (name !== 'position' && name !== 'normal') {\n if (flat) target.set(source as Float32Array, offset * size)\n else for (let at = 0; at < attribute.count; at++) for (let k = 0; k < size; k++) target[(offset + at) * size + k] = attribute.getComponent(at, k)\n return\n }\n const e = (name === 'position' ? matrix : normalMatrix).elements\n const isNormal = name === 'normal'\n for (let at = 0; at < attribute.count; at++) {\n const x = attribute.getX(at)\n const y = attribute.getY(at)\n const z = attribute.getZ(at)\n const out = (offset + at) * 3\n if (isNormal) {\n const nx = e[0]! * x + e[3]! * y + e[6]! * z\n const ny = e[1]! * x + e[4]! * y + e[7]! * z\n const nz = e[2]! * x + e[5]! * y + e[8]! * z\n const length = Math.hypot(nx, ny, nz) || 1\n target[out] = nx / length\n target[out + 1] = ny / length\n target[out + 2] = nz / length\n } else {\n target[out] = e[0]! * x + e[4]! * y + e[8]! * z + e[12]!\n target[out + 1] = e[1]! * x + e[5]! * y + e[9]! * z + e[13]!\n target[out + 2] = e[2]! * x + e[6]! * y + e[10]! * z + e[14]!\n }\n }\n}\n\ntype MergeGroupParams = { readonly meshes: readonly Mesh[]; readonly inverse: Matrix4; readonly group: Object3D; readonly follow: boolean }\n\n/** A skinned batch whose bone `n` is mesh `n` itself (no inverse, no bind offset): every vertex lands where its own mesh puts it. */\nfunction skinnedBatch(THREE: ThreeLib, merged: BufferGeometry, material: Material, meshes: readonly Mesh[]): Mesh {\n const vertices = (merged.attributes.position as BufferAttribute).count\n const skinIndex = new Uint16Array(vertices * 4)\n const skinWeight = new Float32Array(vertices * 4)\n let at = 0\n meshes.forEach((mesh, bone) => {\n for (let k = 0; k < (mesh.geometry.attributes.position as BufferAttribute).count; k++, at++) {\n skinIndex[at * 4] = bone\n skinWeight[at * 4] = 1\n }\n })\n merged.setAttribute('skinIndex', new THREE.BufferAttribute(skinIndex, 4))\n merged.setAttribute('skinWeight', new THREE.BufferAttribute(skinWeight, 4))\n const mesh = new THREE.SkinnedMesh(merged, material)\n const identities = meshes.map(() => new THREE.Matrix4())\n mesh.bind(new THREE.Skeleton(meshes as unknown as Bone[], identities), new THREE.Matrix4())\n // the parts move apart as the body is posed: the rest-pose bounds would cull the moved ones\n mesh.frustumCulled = false\n return mesh\n}\n\nfunction mergeGroup(THREE: ThreeLib, { meshes, inverse: rootInverse, group, follow }: MergeGroupParams): Source[] {\n const identity = new THREE.Matrix4()\n const inverse = follow ? identity : rootInverse\n const first = meshes[0] as Mesh\n const names = ATTRIBUTES.filter((name) => first.geometry.attributes[name] !== undefined)\n const pieces = meshes.map((mesh) => {\n const local = new THREE.Matrix4().multiplyMatrices(inverse, mesh.matrixWorld)\n const vertices = (mesh.geometry.attributes.position as BufferAttribute).count\n const indexCount = mesh.geometry.index ? mesh.geometry.index.count : vertices\n return { mesh, local, vertices, indexCount }\n })\n const totalVertices = pieces.reduce((sum, piece) => sum + piece.vertices, 0)\n const totalIndices = pieces.reduce((sum, piece) => sum + piece.indexCount, 0)\n const merged = new THREE.BufferGeometry()\n const normalMatrix = new THREE.Matrix3()\n for (const name of names) {\n const size = first.geometry.attributes[name]!.itemSize\n const array = new Float32Array(totalVertices * size)\n let offset = 0\n for (const piece of pieces) {\n const matrix = follow ? identity : piece.local\n copyAttribute(name, piece.mesh.geometry.attributes[name] as BufferAttribute, array, offset, matrix, normalMatrix.getNormalMatrix(matrix))\n offset += piece.vertices\n }\n merged.setAttribute(name, new THREE.BufferAttribute(array, size))\n }\n const indexArray = totalVertices > 65535 ? new Uint32Array(totalIndices) : new Uint16Array(totalIndices)\n const indexAttribute = new THREE.BufferAttribute(indexArray, 1)\n merged.setIndex(indexAttribute)\n const mesh = follow ? skinnedBatch(THREE, merged, first.material as Material, meshes) : new THREE.Mesh(merged, first.material)\n mesh.name = `batch:${(first.material as Material).name || (first.material as Material).type}`\n mesh.castShadow = first.castShadow\n mesh.receiveShadow = first.receiveShadow\n mesh.raycast = () => undefined\n merged.computeBoundingBox()\n merged.computeBoundingSphere()\n group.add(mesh)\n const batch: Batch = { mesh, live: pieces.length, index: indexAttribute as unknown as Batch['index'] }\n const sources: Source[] = []\n let vertexOffset = 0\n let indexOffset = 0\n for (const piece of pieces) {\n const index = piece.mesh.geometry.index\n if (index) for (let at = 0; at < piece.indexCount; at++) indexArray[indexOffset + at] = index.getX(at) + vertexOffset\n else for (let at = 0; at < piece.indexCount; at++) indexArray[indexOffset + at] = at + vertexOffset\n sources.push({\n mesh: piece.mesh,\n geometry: piece.mesh.geometry,\n material: piece.mesh.material as Material,\n version: (piece.mesh.geometry.attributes.position as BufferAttribute).version,\n local: piece.local,\n world: piece.mesh.matrixWorld.clone(),\n mask: piece.mesh.layers.mask,\n batch,\n start: indexOffset,\n count: piece.indexCount,\n released: false,\n })\n piece.mesh.layers.set(BATCHED_LAYER)\n vertexOffset += piece.vertices\n indexOffset += piece.indexCount\n }\n return sources\n}\n","import { visualStateOf } from '@adatechnology/game-animals'\nimport type { AnimalSpecies } from '@adatechnology/game-animals'\nimport type { ModuleScenario } from '@adatechnology/game-contracts'\n\nimport type { AnimalsThree } from '../animals-three'\nimport type { OutbreakLike } from './types'\n\n/** What a scenario needs of the room: the pass/fail line and time. The test room's `ScenarioContext` has both. */\nexport type AnimalsContext = { expect(condition: boolean, label: string, data?: Record<string, unknown>): void; step(seconds: number): void }\n\nexport type ScenarioDeps = {\n readonly text: Readonly<Record<string, string>>\n species(): readonly AnimalSpecies[]\n ensure(): AnimalsThree\n outbreak(): OutbreakLike | null\n rebuildOutbreak(enabled: boolean): OutbreakLike | null\n skins(): { count: number; meshes: { drawCalls: number; triangles: number }; nodes: number; rich: number }\n clearSkins(): void\n}\n\nconst SKIN_COUNT = 40\n\nexport const createScenarios = (deps: ScenarioDeps): ModuleScenario<AnimalsContext>[] => {\n const scenario = (id: string, run: ModuleScenario<AnimalsContext>['run']): ModuleScenario<AnimalsContext> => ({ id, name: deps.text[id] ?? id, run })\n const rate = () => 0.5\n return [\n scenario('visual_states', ({ expect }) => {\n const animals = deps.ensure()\n for (const species of deps.species()) {\n const rig = animals.build(species)\n animals.setVisualState(rig, 'infected')\n expect(rig.infected && !rig.dead, `${species.id}: infected`)\n animals.setVisualState(rig, 'dead')\n expect(rig.dead && !rig.infected, `${species.id}: dead`)\n animals.setVisualState(rig, 'alive')\n expect(!rig.dead && !rig.infected, `${species.id}: alive again`)\n }\n }),\n scenario('gait_moves_legs', ({ expect }) => {\n const animals = deps.ensure()\n const species = deps.species().find((entry) => entry.body.kind === 'quadruped')\n if (!species) return expect(false, 'no quadruped among the species')\n const rig = animals.build(species)\n animals.animate(rig, { time: 0, delta: 0, clip: 'walk' })\n const before = rig.legs.map((leg) => leg.hip.rotation.x)\n animals.animate(rig, { time: 0.4, delta: 0.4, clip: 'walk' })\n expect(rig.legs.some((leg, index) => leg.hip.rotation.x !== before[index]), `${species.id}: a walk step swings a hip`)\n }),\n scenario('outbreak_rule', ({ expect }) => {\n const turns = deps.species().find((entry) => entry.outcome === 'turns')\n const immune = deps.species().find((entry) => entry.outcome === 'immune')\n const on = deps.rebuildOutbreak(true)\n if (!on || !turns) return expect(false, 'no outbreak rule or no species that turns')\n expect(on.bite(turns.id, rate, 0) !== undefined, `a bite on ${turns.id} starts a fate with the rule on`)\n if (immune) expect(on.bite(immune.id, rate, 0) === undefined, `${immune.id} shrugs the bite off`)\n const off = deps.rebuildOutbreak(false)\n expect(off?.bite(turns.id, rate, 0) === undefined, 'with the rule off a bite does nothing')\n deps.rebuildOutbreak(true)\n }),\n scenario('carcass_lure', ({ expect }) => {\n const outbreak = deps.rebuildOutbreak(true)\n const dies = deps.species().find((entry) => entry.outbreak?.carcass)\n if (!outbreak || !dies) return expect(false, 'no outbreak rule or no species that leaves a carcass')\n const carcass = outbreak.carcass(dies.id, [0, 0, 0], 0)\n expect(carcass !== undefined, `${dies.id} leaves a carcass`)\n if (!carcass) return\n expect(outbreak.lures(carcass, carcass.lureRadiusMetres - 1, 1), 'a zombie inside the radius is drawn')\n expect(!outbreak.lures(carcass, carcass.lureRadiusMetres + 1, 1), 'a zombie outside it is not')\n }),\n scenario('bite_to_turn_timer', ({ expect }) => {\n const outbreak = deps.rebuildOutbreak(true)\n const turns = deps.species().find((entry) => entry.outcome === 'turns')\n if (!outbreak || !turns) return expect(false, 'no outbreak rule or no species that turns')\n const fate = outbreak.bite(turns.id, rate, 0)\n if (!fate) return expect(false, 'the bite took')\n expect(outbreak.stateOf(fate, 0) === 'incubating', 'it incubates at first')\n expect(outbreak.stateOf(fate, fate.dueAt + 1) === 'turned', 'it has turned once the time is up (the bench clock runs it accelerated)')\n expect(visualStateOf(turns.outcome, true) === 'infected', 'and shows the infected look')\n }),\n scenario('skins_40', ({ expect }) => {\n const result = deps.skins()\n expect(result.count === SKIN_COUNT, `${SKIN_COUNT} animals are in the field`, { count: result.count })\n expect(result.meshes.drawCalls <= SKIN_COUNT + 16, 'the skins are about one draw call each', result.meshes)\n expect(result.nodes > 0, 'each skin keeps the nodes the animation moves', { nodes: result.nodes })\n deps.clearSkins()\n }),\n ]\n}\n","export type AnimalsText = {\n group: string\n name: string\n controls: Record<'visual_state' | 'gait' | 'speed' | 'outbreak' | 'time_scale', string>\n hints: Record<'visual_state' | 'gait' | 'outbreak' | 'time_scale', string>\n options: { visual_state: Record<string, string>; gait: Record<string, string> }\n actions: Record<'bite_all' | 'recover' | 'call' | 'skin_40' | 'clear_skins', string>\n layers: Record<'joints' | 'split' | 'sound_range' | 'carcass_lure', string>\n metrics: Record<'animals' | 'draw_calls' | 'triangles' | 'nodes_per_skin' | 'incubating' | 'turned' | 'dead', string>\n scenarios: Record<string, string>\n}\n\nexport const TEXT: Record<string, AnimalsText> = {\n en: {\n group: 'Animals',\n name: 'Animals area',\n controls: { visual_state: 'Visual state', gait: 'Gait', speed: 'Animation speed', outbreak: 'Outbreak rule', time_scale: 'Bite clock' },\n hints: {\n visual_state: 'The look of every animal: alive, infected (grey coat, glowing eyes, wounds) or dead (lying down, a pool of blood)',\n gait: 'idle, walk or run; the cycle of each species runs at its own rate',\n outbreak: 'Off: a bite does nothing. On: a species that turns incubates and turns, one that dies dies, the rest shrug it off',\n time_scale: 'Game seconds per real second for the bite timers (60: ten minutes pass in ten seconds)',\n },\n options: { visual_state: { alive: 'Alive', infected: 'Infected', dead: 'Dead' }, gait: { idle: 'Idle', walk: 'Walk', run: 'Run' } },\n actions: { bite_all: 'Bite every animal', recover: 'Heal every animal', call: 'Make the animals with a call call (hen, lapwing, cow, dog)', skin_40: 'Add 40 animals as node skins', clear_skins: 'Remove the skins' },\n layers: { joints: 'Joints', split: 'Split view', sound_range: 'Sound range', carcass_lure: 'Carcass lure radius' },\n metrics: { animals: 'Animals', draw_calls: 'Draw calls', triangles: 'Triangles', nodes_per_skin: 'Nodes per skin', incubating: 'Incubating', turned: 'Turned', dead: 'Dead' },\n scenarios: {\n visual_states: 'Alive, infected and dead for every species',\n gait_moves_legs: 'The gait moves the legs',\n outbreak_rule: 'Outbreak rule on and off',\n carcass_lure: 'A carcass lures zombies within its radius',\n bite_to_turn_timer: 'Bite, incubation, turn (accelerated)',\n skins_40: '40 animals as node skins',\n },\n },\n 'pt-BR': {\n group: 'Animais',\n name: 'Área de animais',\n controls: { visual_state: 'Estado visual', gait: 'Andar', speed: 'Velocidade da animação', outbreak: 'Regra de surto', time_scale: 'Relógio da mordida' },\n hints: {\n visual_state: 'O aspecto de todos os bichos: vivo, infectado (pelo cinza, olhos brilhando, feridas) ou morto (deitado, poça de sangue)',\n gait: 'idle, walk ou run; o ciclo de cada espécie anda no seu ritmo',\n outbreak: 'Desligada: a mordida não faz nada. Ligada: a espécie que vira incuba e vira, a que morre morre, as outras não sentem',\n time_scale: 'Segundos de jogo por segundo real nos prazos da mordida (60: dez minutos passam em dez segundos)',\n },\n options: { visual_state: { alive: 'Vivo', infected: 'Infectado', dead: 'Morto' }, gait: { idle: 'Parado', walk: 'Andando', run: 'Correndo' } },\n actions: { bite_all: 'Morder todos os bichos', recover: 'Curar todos os bichos', call: 'Fazer os bichos com chamado chamarem (galinha, quero-quero, vaca, cachorro)', skin_40: 'Pôr 40 bichos como pele de nós', clear_skins: 'Tirar as peles' },\n layers: { joints: 'Juntas', split: 'Vista aberta', sound_range: 'Alcance do som', carcass_lure: 'Raio de atração da carcaça' },\n metrics: { animals: 'Bichos', draw_calls: 'Chamadas de desenho', triangles: 'Triângulos', nodes_per_skin: 'Nós por pele', incubating: 'Incubando', turned: 'Viraram', dead: 'Morreram' },\n scenarios: {\n visual_states: 'Vivo, infectado e morto em cada espécie',\n gait_moves_legs: 'O andar mexe as pernas',\n outbreak_rule: 'Regra de surto ligada e desligada',\n carcass_lure: 'A carcaça atrai zumbis dentro do raio',\n bite_to_turn_timer: 'Mordida, incubação, vira (acelerado)',\n skins_40: '40 bichos como pele de nós',\n },\n },\n}\n","import { discardObject3D } from '@adatechnology/game-shapes-three'\nimport { lineUpLength, visualStateOf } from '@adatechnology/game-animals'\nimport type { AnimalSpecies, AnimalVisualState } from '@adatechnology/game-animals'\nimport type { BenchChange, ModuleArea, ModuleControl, ModuleHost, ModuleLayer, ModuleMetric, ModuleScenario, TestableItem, TestableModule } from '@adatechnology/game-contracts'\nimport type { NodeSkin } from '@adatechnology/game-props3d'\nimport { createStaticBatcher } from '@adatechnology/game-render-three'\nimport type { StaticBatcher } from '@adatechnology/game-render-three'\nimport type * as THREE_NS from 'three'\n\nimport { createAnimalSound } from '../audio/sound'\nimport type { AnimalSound } from '../audio/sound'\nimport { createAnimalsThree } from '../animals-three'\nimport type { AnimalsThree } from '../animals-three'\nimport type { AnimalRig, Three } from '../types'\nimport { createScenarios } from './scenarios'\nimport type { AnimalsContext, ScenarioDeps } from './scenarios'\nimport { TEXT } from './text'\nimport type { AnimalsTestOptions, OutbreakLike } from './types'\n\nexport const ANIMALS_AREA_ID = 'animals_area'\nexport const ANIMAL_ITEM_PREFIX = 'animal_'\nconst SKIN_COUNT = 40\nconst SKIN_FIELD = { x0: -18, z0: -10, columns: 8, gap: 4.5 }\nconst DEFAULT_TIME_SCALE = 60\n\ntype Fate = NonNullable<ReturnType<OutbreakLike['bite']>>\ntype Placed = { readonly id: string; readonly rig: AnimalRig; fate?: Fate; counted?: 'turned' | 'dead'; batcher?: StaticBatcher }\n/** Milliseconds a frame the batches of a new animal may take to be made. */\nconst BATCH_BUDGET_MS = 1\n\n/**\n * The animals test area as a `TestableModule`: the species of the pack in a row, each drawn by the Bichos do Vale builders, with the alive / infected / dead looks, the gait,\n * the joints and split views, the rings for how far its sound carries and how far a carcass lures, the outbreak rule with an accelerated bite clock, and a field of 40 animals\n * drawn as node skins. The species and the outbreak rule come in by option, so this package knows no game.\n */\nexport function createAnimalsTestModule(options: AnimalsTestOptions = {}): TestableModule<AnimalsContext> & { readonly placed: ReadonlyMap<string, unknown> } {\n const locale = options.locale ?? 'pt-BR'\n const text = TEXT[locale] ?? (TEXT['pt-BR'] as (typeof TEXT)[string])\n const species = options.species ?? []\n const settings = { visual: 'alive' as AnimalVisualState, gait: 'walk', speed: 1, outbreak: true, timeScale: DEFAULT_TIME_SCALE }\n let three: Three | null = null\n let animals: AnimalsThree | null = null\n let host: ModuleHost | null = null\n let outbreak: OutbreakLike | null = null\n let sound: AnimalSound | null = null\n let time = 0\n let clock = 0\n let skinGroup: THREE_NS.Group | null = null\n let skins: { rig: AnimalRig; skin: NodeSkin }[] = []\n let rings: THREE_NS.Object3D[] = []\n const placed = new Map<string, Placed>()\n const random = (): number => 0.5\n const drawSeed = options.seed ?? 7\n const bitten = () => ({ turned: [...placed.values()].filter((p) => p.counted === 'turned').length, dead: [...placed.values()].filter((p) => p.counted === 'dead').length })\n\n const ensure = (THREE: Three): AnimalsThree => {\n three = THREE\n animals ??= createAnimalsThree(THREE, { seed: drawSeed, ...(options.document ? { document: options.document } : {}) })\n return animals\n }\n const rebuildOutbreak = (): void => {\n outbreak = options.createOutbreak ? options.createOutbreak({ species, enabled: settings.outbreak, timeScale: settings.timeScale }) : null\n }\n rebuildOutbreak()\n\n /** Draws the animal's parts one batch per material, posed with it; a new look (other materials) makes the batches again. */\n const batch = (entry: Placed): void => {\n entry.batcher?.dispose()\n if (three) entry.batcher = createStaticBatcher(three, entry.rig.root, { followMeshes: true, budgetMs: BATCH_BUDGET_MS })\n }\n\n const look = (entry: Placed, state: AnimalVisualState): void => {\n animals?.setVisualState(entry.rig, state)\n batch(entry)\n }\n\n const item = (animal: AnimalSpecies): TestableItem => ({\n id: `${ANIMAL_ITEM_PREFIX}${animal.id}`,\n name: animal.name[locale as 'en'] ?? animal.id,\n movable: false,\n build: (THREE: unknown) => {\n const rig = ensure(THREE as Three).build(animal)\n // the room puts the tree in its scene right after this; trees that never reach it are dropped at the next frame\n const entry: Placed = { id: rig.root.uuid, rig }\n placed.set(rig.root.uuid, entry)\n if (settings.visual !== 'alive') animals?.setVisualState(rig, settings.visual)\n batch(entry)\n return rig.root\n },\n })\n\n const statusOf = (entry: Placed): void => {\n if (!entry.fate || !outbreak) return\n const state = outbreak.stateOf(entry.fate, clock)\n if (state === 'incubating') return\n entry.counted = state === 'turned' ? 'turned' : 'dead'\n look(entry, visualStateOf(entry.rig.species.outcome, true, state === 'dead'))\n }\n\n const clearRings = (): void => {\n discardObject3D(rings)\n rings = []\n }\n const refreshLayers = (): void => {\n if (!host?.bench || !animals || !three) return\n clearRings()\n for (const entry of placed.values()) {\n animals.setJointView(entry.rig, host.bench.layer('joints'))\n animals.setSplit(entry.rig, host.bench.layer('split'))\n }\n const ring = (radius: number, color: number, parent: THREE_NS.Object3D): void => {\n const mesh = new (three as Three).Mesh(new (three as Three).RingGeometry(radius - 0.15, radius, 96).rotateX(-Math.PI / 2), new (three as Three).MeshBasicMaterial({ color, transparent: true, opacity: 0.55, depthWrite: false }))\n mesh.position.y = 0.03\n parent.add(mesh)\n rings.push(mesh)\n }\n for (const entry of placed.values()) {\n if (!entry.rig.root.parent) continue\n const radius = entry.rig.species.soundRangeMetres\n if (host.bench.layer('sound_range') && radius) ring(radius, 0x44aaff, entry.rig.root)\n const lure = entry.rig.species.outbreak?.carcass?.lureRadiusMetres\n if (host.bench.layer('carcass_lure') && lure) ring(lure, 0xff5533, entry.rig.root)\n }\n }\n\n const addSkins = (): void => {\n if (!animals || !three || !host) return\n skinGroup ??= new three.Group()\n if (!skinGroup.parent) (host.scene as THREE_NS.Object3D).add(skinGroup)\n for (let index = skins.length; index < SKIN_COUNT; index++) {\n const animal = species[index % Math.max(1, species.length)] as AnimalSpecies\n const rig = animals.build(animal)\n const skin = animals.createSkin(rig)\n rig.root.position.set(SKIN_FIELD.x0 + (index % SKIN_FIELD.columns) * SKIN_FIELD.gap, 0, SKIN_FIELD.z0 - Math.floor(index / SKIN_FIELD.columns) * SKIN_FIELD.gap)\n // the tree still drives the animation, but only the skin is drawn\n rig.root.traverse((object) => {\n if ((object as THREE_NS.Mesh).isMesh) object.visible = false\n })\n skinGroup.add(rig.root, skin.mesh)\n skins.push({ rig, skin })\n }\n }\n const clearSkins = (): void => {\n if (skinGroup) discardObject3D(skinGroup)\n skinGroup = null\n skins = []\n }\n\n const applyVisual = (): void => {\n for (const entry of placed.values()) {\n if (entry.fate) continue\n look(entry, settings.visual)\n }\n }\n\n const onChange = (change: BenchChange): void => {\n if (change.kind === 'layer') return refreshLayers()\n if (change.kind === 'action') {\n if (change.id === 'bite_all') {\n for (const entry of placed.values()) {\n entry.fate = outbreak?.bite(entry.rig.species.id, random, clock)\n if (entry.fate) look(entry, visualStateOf(entry.rig.species.outcome, true))\n }\n } else if (change.id === 'recover') {\n for (const entry of placed.values()) {\n delete entry.fate\n delete entry.counted\n look(entry, settings.visual)\n }\n } else if (change.id === 'call' && sound) {\n for (const [id, entry] of placed) {\n const at = entry.rig.root.getWorldPosition(new (three as Three).Vector3())\n sound.call(entry.rig.species.id, [at.x, at.y + 0.5, at.z], { key: id })\n }\n } else if (change.id === 'skin_40') addSkins()\n else if (change.id === 'clear_skins') clearSkins()\n return\n }\n switch (change.id) {\n case 'visual_state': settings.visual = String(change.value) as AnimalVisualState; applyVisual(); break\n case 'gait': settings.gait = String(change.value); break\n case 'speed': settings.speed = Number(change.value); break\n case 'outbreak': settings.outbreak = Boolean(change.value); if (outbreak) outbreak.enabled = settings.outbreak; break\n case 'time_scale': settings.timeScale = Number(change.value); rebuildOutbreak(); break\n default: break\n }\n }\n\n const frame = (dt: number): void => {\n time += dt * settings.speed\n clock += dt\n sound?.update(dt)\n for (const [id, entry] of placed) {\n if (!entry.rig.root.parent) {\n entry.batcher?.dispose()\n placed.delete(id)\n continue\n }\n statusOf(entry)\n animals?.animate(entry.rig, { time, delta: dt * settings.speed, clip: settings.gait, split: host?.bench?.layer('split') ?? false })\n animals?.updateSplit(entry.rig, dt)\n entry.batcher?.update()\n }\n for (const { rig, skin } of skins) {\n animals?.animate(rig, { time, delta: dt * settings.speed, clip: settings.gait })\n rig.root.updateMatrixWorld(true)\n skin.update()\n }\n }\n\n const countMeshes = (only?: 'skins'): { drawCalls: number; triangles: number } => {\n let drawCalls = 0\n let triangles = 0\n const count = (root: THREE_NS.Object3D): void =>\n root.traverse((object) => {\n const mesh = object as THREE_NS.Mesh\n if (!mesh.isMesh || !mesh.visible) return\n drawCalls += 1\n const geometry = mesh.geometry\n triangles += geometry.index ? geometry.index.count / 3 : geometry.attributes.position.count / 3\n })\n if (only !== 'skins') for (const entry of placed.values()) count(entry.rig.root)\n for (const { skin } of skins) count(skin.mesh)\n return { drawCalls, triangles: Math.round(triangles) }\n }\n\n const deps: ScenarioDeps = {\n text: text.scenarios,\n species: () => species,\n ensure: () => ensure((three ?? host?.THREE) as Three),\n outbreak: () => outbreak,\n rebuildOutbreak: (enabled) => {\n settings.outbreak = enabled\n rebuildOutbreak()\n return outbreak\n },\n skins: () => {\n ensure((three ?? host?.THREE) as Three)\n addSkins()\n return { count: skins.length, meshes: countMeshes('skins'), nodes: skins[0]?.skin.nodes.length ?? 0, rich: skins[0]?.rig.triangles ?? 0 }\n },\n clearSkins,\n }\n\n return {\n id: ANIMALS_AREA_ID,\n name: text.name,\n ground: { surface: 'lawn' },\n group: text.group,\n get placed() {\n return placed\n },\n items: () => species.map(item),\n area: (): ModuleArea => {\n // the page's line-up: each animal given room by its length (and a gap), the row centred on the origin\n const gap = 0.85\n let cursor = 0\n const spots = species.map((animal) => {\n const length = lineUpLength(animal.body)\n const x = cursor + length / 2\n cursor += length + gap\n return { animal, x }\n })\n const offset = -(cursor - gap) / 2\n return {\n placements: spots.map(({ animal, x }) => ({ item: `${ANIMAL_ITEM_PREFIX}${animal.id}`, position: [x + offset, 0, 0], rotationY: animal.body.kind === 'snake' ? 2.2 : 1.1 })),\n view: { eye: [0, 4, Math.max(10, (cursor - gap) * 0.55)], target: [0, 0.6, 0] },\n }\n },\n controls: (): ModuleControl[] => [\n { kind: 'choice', id: 'visual_state', name: text.controls.visual_state, hint: text.hints.visual_state, options: (['alive', 'infected', 'dead'] as const).map((id) => ({ id, name: text.options.visual_state[id] as string })), default: 'alive' },\n { kind: 'choice', id: 'gait', name: text.controls.gait, hint: text.hints.gait, options: (['idle', 'walk', 'run'] as const).map((id) => ({ id, name: text.options.gait[id] as string })), default: 'walk' },\n { kind: 'number', id: 'speed', name: text.controls.speed, min: 0, max: 4, step: 0.25, default: 1 },\n { kind: 'boolean', id: 'outbreak', name: text.controls.outbreak, hint: text.hints.outbreak, default: true },\n { kind: 'number', id: 'time_scale', name: text.controls.time_scale, hint: text.hints.time_scale, min: 1, max: 600, step: 1, default: DEFAULT_TIME_SCALE },\n { kind: 'action', id: 'bite_all', name: text.actions.bite_all },\n { kind: 'action', id: 'recover', name: text.actions.recover },\n { kind: 'action', id: 'call', name: text.actions.call },\n { kind: 'action', id: 'skin_40', name: text.actions.skin_40, slow: true },\n { kind: 'action', id: 'clear_skins', name: text.actions.clear_skins },\n ],\n layers: (): ModuleLayer[] => (['joints', 'split', 'sound_range', 'carcass_lure'] as const).map((id) => ({ id, name: text.layers[id], default: false })),\n metrics: (): ModuleMetric[] => [\n { id: 'animals', name: text.metrics.animals, read: () => placed.size + skins.length },\n { id: 'draw_calls', name: text.metrics.draw_calls, read: () => countMeshes().drawCalls },\n { id: 'triangles', name: text.metrics.triangles, read: () => countMeshes().triangles },\n { id: 'nodes_per_skin', name: text.metrics.nodes_per_skin, read: () => skins[0]?.skin.nodes.length ?? 0 },\n { id: 'incubating', name: text.metrics.incubating, read: () => [...placed.values()].filter((p) => p.fate && !p.counted).length },\n { id: 'turned', name: text.metrics.turned, read: () => bitten().turned },\n { id: 'dead', name: text.metrics.dead, read: () => bitten().dead },\n ],\n scenarios: (): ModuleScenario<AnimalsContext>[] => createScenarios(deps),\n attach(lent) {\n host = lent\n ensure(lent.THREE as Three)\n sound = lent.audio && options.sound !== false ? createAnimalSound(lent.audio, options.sound ?? {}) : null\n const stopChange = lent.bench?.onChange(onChange)\n const stopFrame = lent.onFrame(frame)\n return () => {\n stopChange?.()\n stopFrame()\n clearRings()\n clearSkins()\n sound?.dispose()\n sound = null\n host = null\n }\n },\n }\n\n}\n"],"mappings":"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","names":["object","landing","list","EN","make","landing","mesh","color","width","height","depth","indexArray","sign","list","num","equals","area","ring","bs","layeroffset","built","make","object","list","list","object","document","object","mesh","color","material","geometry","normalMatrix","PI","PI","list","sphere","object","mesh","object","mesh","normalMatrix","indexArray","SKIN_COUNT","ring","color","mesh","object"]}
|